feat(O-T3): extract GL infrastructure to AcDream.App

Phase O Task 3 — verbatim-copy GL infra from Chorizite.OpenGLSDLBackend
into src/AcDream.App/Rendering/Wb/ (namespace AcDream.App.Rendering.Wb).

18 files extracted (all namespace-changed; no algorithm changes):
  OpenGLGraphicsDevice, ManagedGLTexture, ManagedGLTextureArray,
  ManagedGLVertexBuffer, ManagedGLIndexBuffer, ManagedGLVertexArray,
  ManagedGLFrameBuffer, ManagedGLUniformBuffer, GLSLShader, GLHelpers,
  GLStateScope, GpuMemoryTracker, SceneData, DebugRenderSettings,
  TextureParameters, TextureFormatExtensions, BufferUsageExtensions,
  EmbeddedResourceReader.

3 internals promoted to public (O-D9):
  EmbeddedResourceReader, TextureFormatExtensions, BufferUsageExtensions.

SixLabors.ImageSharp not reachable: TextureHelpers was placed in
AcDream.Core (no GL/ImageSharp dep); only the GL types went to App.

TextureHelpers.GetCompressedLayerSize added to AcDream.Core.Rendering.Wb
(was in Chorizite.OpenGLSDLBackend.Lib.TextureHelpers; uses
Chorizite.Core.Render.Enums.TextureFormat which Core gets transitively
via the still-present WB project refs).

T3/T4 boundary interims:
  - WbMeshAdapter._graphicsDevice stays Chorizite.OpenGLSDLBackend.OpenGLGraphicsDevice
    (T4 will swap it when ObjectMeshManager is extracted).
  - OpenGLGraphicsDevice.ParticleBatcher deferred to null! (T4 extracts
    ParticleBatcher alongside ObjectMeshManager; can't pass `this` of our
    new type to the WB-original ctor before T4).
  - ManagedGLTextureArray uses our TextureHelpers via explicit alias.
  - IUniformBuffer is in Chorizite.Core.dll under Chorizite.OpenGLSDLBackend
    namespace (unusual packaging); resolved via type alias.
  - AcDream.App.csproj gets explicit Chorizite.Core 0.0.18 PackageReference
    (IUniformBuffer + other Chorizite.Core types now used directly in App).

Build green. Test baseline 1147+8 maintained (1902 passing, 8 pre-existing
MotionInterpreterTests failures unrelated to T3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-21 16:00:31 +02:00
parent 16bc10c99d
commit 4cc38805b5
21 changed files with 3018 additions and 4 deletions

View file

@ -14,6 +14,10 @@
<InternalsVisibleTo Include="AcDream.App.Tests" />
</ItemGroup>
<ItemGroup>
<!-- T3: extracted GL infra (ManagedGLUniformBuffer, OpenGLGraphicsDevice) directly
implement IUniformBuffer from Chorizite.Core; NuGet PackageReferences are not
forwarded from ProjectReferences so we must declare it explicitly here. -->
<PackageReference Include="Chorizite.Core" Version="0.0.18" />
<PackageReference Include="Silk.NET.OpenGL" Version="2.23.0" />
<PackageReference Include="Silk.NET.OpenGL.Extensions.ARB" Version="2.23.0" />
<PackageReference Include="Silk.NET.Windowing" Version="2.23.0" />

View file

@ -0,0 +1,27 @@
using Chorizite.Core.Render.Enums;
using Silk.NET.OpenGL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AcDream.App.Rendering.Wb {
public static class BufferUsageExtensions {
/// <summary>
/// Converts a BufferUsage to a GL BufferUsageARB
/// </summary>
/// <param name="usage"></param>
/// <returns></returns>
public static GLEnum ToGL(this BufferUsage usage) {
switch (usage) {
case BufferUsage.Static:
return GLEnum.StaticDraw;
case BufferUsage.Dynamic:
return GLEnum.DynamicDraw;
default:
return GLEnum.StaticDraw;
}
}
}
}

View file

@ -0,0 +1,28 @@
using System.Numerics;
namespace AcDream.App.Rendering.Wb {
// Extracted verbatim from WorldBuilder.Shared/Models/DebugRenderSettings.cs.
// LandscapeColorsSettings dependency (editor-only, CommunityToolkit.Mvvm) stripped;
// default color values inlined from LandscapeColorsSettings field initializers.
public class DebugRenderSettings {
public bool ShowBoundingBoxes { get; set; } = false;
public bool SelectVertices { get; set; } = true;
public bool SelectBuildings { get; set; } = true;
public bool SelectStaticObjects { get; set; } = true;
public bool SelectScenery { get; set; } = false;
public bool SelectEnvCells { get; set; } = true;
public bool SelectEnvCellStaticObjects { get; set; } = true;
public bool SelectPortals { get; set; } = true;
public bool ShowDisqualifiedScenery { get; set; } = true;
public bool EnableAnisotropicFiltering { get; set; } = true;
// Default colors inlined from LandscapeColorsSettings field initializers
public Vector4 VertexColor { get; set; } = new Vector4(0.7882353f, 0.34901962f, 0.2901961f, 1.0f);
public Vector4 BuildingColor { get; set; } = new Vector4(0.76862746f, 0.5803922f, 0.25882354f, 1.0f);
public Vector4 StaticObjectColor { get; set; } = new Vector4(0.37254903f, 0.88235295f, 0.9019608f, 1.0f);
public Vector4 SceneryColor { get; set; } = new Vector4(0.45490196f, 0.72156864f, 0.32156864f, 1.0f);
public Vector4 EnvCellColor { get; set; } = new Vector4(0.5294118f, 0.44705883f, 0.7882353f, 1.0f);
public Vector4 EnvCellStaticObjectColor { get; set; } = new Vector4(0f, 0.49803922f, 1f, 1.0f);
public Vector4 PortalColor { get; set; } = new Vector4(1f, 0f, 1f, 1.0f);
}
}

View file

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace AcDream.App.Rendering.Wb {
public static class EmbeddedResourceReader {
internal static string GetEmbeddedResource(string filename) {
var assembly = Assembly.GetExecutingAssembly();
var resourceName = "Chorizite.OpenGLSDLBackend." + filename;
using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException($"Could not find embedded resource '{resourceName}'");
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
}
}

View file

@ -0,0 +1,254 @@
using Microsoft.Extensions.Logging;
using Silk.NET.Core.Native;
using Silk.NET.OpenGL;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace AcDream.App.Rendering.Wb {
public static class GLHelpers {
public static OpenGLGraphicsDevice? Device { get; set; }
public static ILogger? Logger { get; set; }
public static void Init(OpenGLGraphicsDevice device, ILogger logger) {
Logger = logger;
Device = device;
}
#if DEBUG
private static bool _loggedVersion = false;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CheckErrors(GL gl, bool logErrors = false, [CallerMemberName] string callerName = "",
[CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) {
var error = gl.GetError();
if (error != GLEnum.NoError) {
if (!_loggedVersion) {
_loggedVersion = true;
var version = gl.GetStringS(GLEnum.Version);
var vendor = gl.GetStringS(GLEnum.Vendor);
var renderer = gl.GetStringS(GLEnum.Renderer);
Logger?.LogInformation($"GL Version: {version}, Vendor: {vendor}, Renderer: {renderer}");
}
string errorDetails = GetErrorDetails(error);
string location = $"{System.IO.Path.GetFileName(callerFile)}::{callerName}:{callerLine}";
var program = (uint)gl.GetInteger(GLEnum.CurrentProgram);
var vao = gl.GetInteger(GLEnum.VertexArrayBinding);
var activeTex = gl.GetInteger(GLEnum.ActiveTexture);
var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
string extraInfo = "";
if (program != 0) {
bool isProgram = gl.IsProgram(program);
gl.GetProgram(program, GLEnum.LinkStatus, out int linkStatus);
gl.GetProgram(program, GLEnum.DeleteStatus, out int deleteStatus);
gl.GetProgram(program, GLEnum.ValidateStatus, out int validateStatus);
extraInfo = $", IsProg: {isProgram}, Link: {linkStatus}, Del: {deleteStatus}, Valid: {validateStatus}";
}
string message = $"OpenGL Error: {error} ({errorDetails}) at {location}. Thread: {threadId}, Program: {program}{extraInfo}, VAO: {vao}, ActiveTex: {activeTex}";
Logger?.LogError(message);
throw new Exception(message);
}
}
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CheckErrors(GL gl, bool logErrors = false, string callerName = "",
string callerFile = "", int callerLine = 0) {
}
#endif
public static string GetErrorDetails(GLEnum error) {
return error switch {
GLEnum.InvalidEnum => "Invalid enum - An unacceptable value is specified for an enumerated argument",
GLEnum.InvalidValue => "Invalid value - A numeric argument is out of range",
GLEnum.InvalidOperation =>
"Invalid operation - The specified operation is not allowed in the current state",
GLEnum.StackOverflow => "Stack overflow - An operation would cause an internal stack to overflow",
GLEnum.StackUnderflow => "Stack underflow - An operation would cause an internal stack to underflow",
GLEnum.OutOfMemory => "Out of memory - There is not enough memory left to execute the command",
GLEnum.InvalidFramebufferOperation =>
"Invalid framebuffer operation - The framebuffer object is not complete",
GLEnum.ContextLost => "Context lost - The OpenGL context has been lost due to a graphics card reset",
_ => "Unknown error"
};
}
#if DEBUG
/// <summary>
/// Checks for OpenGL errors and provides context-specific information
/// </summary>
public static void CheckErrorsWithContext(GL gl, string context, [CallerMemberName] string callerName = "",
[CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) {
var error = gl.GetError();
if (error != GLEnum.NoError) {
string errorDetails = GetErrorDetails(error);
string location = $"{System.IO.Path.GetFileName(callerFile)}::{callerName}:{callerLine}";
string message = $"OpenGL Error: {error} ({errorDetails})\nContext: {context}\nLocation: {location}";
Logger?.LogError(message);
throw new Exception(message);
}
}
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CheckErrorsWithContext(GL gl, string context, string callerName = "",
string callerFile = "", int callerLine = 0) {
}
#endif
/// <summary>
/// Gets detailed information about the current texture state for debugging
/// </summary>
public static string GetTextureDebugInfo(GL gl, GLEnum target) {
var info = new System.Text.StringBuilder();
info.AppendLine($"Texture Debug Info for {target}:");
try {
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
GetTextureParameter.TextureWidth, out int width);
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
GetTextureParameter.TextureHeight, out int height);
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
GetTextureParameter.TextureDepthExt, out int depth);
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
GetTextureParameter.TextureInternalFormat, out int format);
info.AppendLine($" Dimensions: {width}x{height}x{depth}");
info.AppendLine($" Internal Format: {(InternalFormat)format}");
gl.GetTexParameter(target, GetTextureParameter.TextureMinFilter, out int minFilter);
gl.GetTexParameter(target, GetTextureParameter.TextureMagFilter, out int magFilter);
info.AppendLine($" Min Filter: {(TextureMinFilter)minFilter}");
info.AppendLine($" Mag Filter: {(TextureMagFilter)magFilter}");
// Get max mipmap level
gl.GetTexParameter(target, GetTextureParameter.TextureMaxLevelSgis, out int maxLevel);
info.AppendLine($" Max Level: {maxLevel}");
// Check completeness
int maxMipLevel = (int)Math.Floor(Math.Log2(Math.Max(width, height)));
info.AppendLine($" Calculated Max Mip Level: {maxMipLevel}");
}
catch (Exception ex) {
info.AppendLine($" Error getting texture info: {ex.Message}");
}
return info.ToString();
}
/// <summary>
/// Validates texture completeness for mipmapping
/// </summary>
public static bool ValidateTextureMipmapStatus(GL gl, GLEnum target, out string errorMessage) {
try {
gl.GetTexLevelParameter(target, 0, GetTextureParameter.TextureWidth, out int width);
gl.GetTexLevelParameter(target, 0, GetTextureParameter.TextureHeight, out int height);
gl.GetTexLevelParameter(target, 0, GetTextureParameter.TextureInternalFormat, out int format);
if (width == 0 || height == 0) {
errorMessage = "Texture has zero dimensions";
return false;
}
// Check if format is valid for mipmap generation
var internalFormat = (InternalFormat)format;
if (IsCompressedFormat(internalFormat)) {
errorMessage = $"Compressed format {internalFormat} does not support automatic mipmap generation";
return false;
}
errorMessage = String.Empty;
return true;
}
catch (Exception ex) {
errorMessage = $"Exception during validation: {ex.Message}";
return false;
}
}
private static bool IsCompressedFormat(InternalFormat format) {
return format == InternalFormat.CompressedRgbaS3TCDxt1Ext ||
format == InternalFormat.CompressedRgbaS3TCDxt3Ext ||
format == InternalFormat.CompressedRgbaS3TCDxt5Ext ||
format == InternalFormat.CompressedRgbS3TCDxt1Ext ||
format == InternalFormat.CompressedSrgbAlphaS3TCDxt1Ext ||
format == InternalFormat.CompressedSrgbAlphaS3TCDxt3Ext ||
format == InternalFormat.CompressedSrgbAlphaS3TCDxt5Ext;
}
/// <summary>
/// Logs current OpenGL state for debugging
/// </summary>
public static void LogGLState(GL gl, string context = "") {
var state = new System.Text.StringBuilder();
state.AppendLine($"=== OpenGL State ({context}) ===");
try {
state.AppendLine(
$"Active Texture Unit: GL_TEXTURE{gl.GetInteger(GetPName.ActiveTexture) - (int)GLEnum.Texture0}");
state.AppendLine($"Bound 2D Array Texture: {gl.GetInteger(GetPName.TextureBinding2DArray)}");
state.AppendLine($"Current Program: {gl.GetInteger(GetPName.CurrentProgram)}");
gl.GetInteger(GetPName.MaxTextureSize, out int maxTexSize);
state.AppendLine($"Max Texture Size: {maxTexSize}");
gl.GetInteger(GetPName.Max3DTextureSize, out int max3DSize);
state.AppendLine($"Max 3D Texture Size: {max3DSize}");
gl.GetInteger(GetPName.MaxArrayTextureLayers, out int maxLayers);
state.AppendLine($"Max Array Texture Layers: {maxLayers}");
}
catch (Exception ex) {
state.AppendLine($"Error getting GL state: {ex.Message}");
}
state.AppendLine("======================");
Logger?.LogInformation(state.ToString());
}
/// <summary>
/// Explicit defaults to prevent Avalonia state leakage into our custom rendering pipeline.
/// Call this at the start of complex render cycles immediately inside a GLStateScope.
/// </summary>
public static void SetupDefaultRenderState(GL gl) {
gl.BindSampler(0, 0);
gl.BindSampler(1, 0);
gl.BindSampler(2, 0);
gl.ActiveTexture(TextureUnit.Texture1);
gl.BindTexture(TextureTarget.Texture2D, 0);
gl.ActiveTexture(TextureUnit.Texture2);
gl.BindTexture(TextureTarget.Texture2D, 0);
gl.ActiveTexture(TextureUnit.Texture0); // End on Texture0
gl.BindTexture(TextureTarget.Texture2D, 0);
gl.BindVertexArray(0);
gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0);
gl.UseProgram(0);
gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
gl.PixelStore(PixelStoreParameter.UnpackRowLength, 0);
gl.PixelStore(PixelStoreParameter.UnpackSkipRows, 0);
gl.PixelStore(PixelStoreParameter.UnpackSkipPixels, 0);
gl.Disable(EnableCap.StencilTest);
gl.BlendColor(0, 0, 0, 0);
gl.PolygonMode(GLEnum.FrontAndBack, PolygonMode.Fill);
// Disable Avalonia/Skia specific states
gl.Disable(EnableCap.SampleAlphaToCoverage);
gl.Disable(EnableCap.SampleAlphaToOne);
gl.Disable(EnableCap.Multisample);
gl.Disable((EnableCap)GLEnum.PrimitiveRestart);
gl.LineWidth(1.0f);
gl.PolygonOffset(0f, 0f);
gl.Disable(EnableCap.PolygonOffsetFill);
gl.Disable((EnableCap)GLEnum.ProgramPointSize);
}
}
}

View file

@ -0,0 +1,271 @@
using Chorizite.Core.Render;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace AcDream.App.Rendering.Wb {
public unsafe class GLSLShader : BaseShader, IDisposable {
private OpenGLGraphicsDevice _device;
private Dictionary<string, int> _uniformLocations = [];
private Dictionary<int, object> _uniformValues = [];
private readonly object _lock = new();
private GL GL => _device.GL;
public uint Program { get; protected set; }
public bool HasUniform(string name) {
lock (_lock) {
return GetUniformLocation(Program, name) != -1;
}
}
public GLSLShader(OpenGLGraphicsDevice device, string name, string vertSource, string fragSource, ILogger log) : base(name, vertSource, fragSource, log) {
_device = device;
Load(vertSource, fragSource);
}
public GLSLShader(OpenGLGraphicsDevice device, string name, string shaderDirectory, ILogger log) : base(name, shaderDirectory, log) {
_device = device;
Load();
}
public override void Dispose() {
Unload();
base.Dispose();
}
private int GetUniformLocation(uint program, string name) {
lock (_lock) {
if (!_uniformLocations.ContainsKey(name)) {
_uniformLocations.Add(name, GL.GetUniformLocation(program, name));
}
return _uniformLocations[name];
}
}
public override void SetUniform(string location, Matrix4x4 m) {
lock (_lock) {
int loc = GetUniformLocation(Program, location);
if (loc == -1) return;
if (_uniformValues.TryGetValue(loc, out var val) && val is Matrix4x4 mCached && mCached == m) {
return;
}
_uniformValues[loc] = m;
GL.UniformMatrix4(loc, 1, false, (float*)&m);
}
}
public override void SetUniform(string location, int v) {
lock (_lock) {
int loc = GetUniformLocation((uint)Program, location);
if (loc == -1) return;
if (_uniformValues.TryGetValue(loc, out var val) && val is int vCached && vCached == v) {
return;
}
_uniformValues[loc] = v;
GL.Uniform1(loc, v);
}
}
public override void SetUniform(string location, Vector2 vec) {
lock (_lock) {
int loc = GetUniformLocation((uint)Program, location);
if (loc == -1) return;
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector2 vCached && vCached == vec) {
return;
}
_uniformValues[loc] = vec;
GL.Uniform2(loc, vec);
}
}
public override void SetUniform(string location, Vector3 vec) {
lock (_lock) {
int loc = GetUniformLocation((uint)Program, location);
if (loc == -1) return;
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector3 vCached && vCached == vec) {
return;
}
_uniformValues[loc] = vec;
GL.Uniform3(loc, vec);
}
}
public override void SetUniform(string location, Vector3[] vecs) {
lock (_lock) {
int loc = GetUniformLocation((uint)Program, location);
if (loc == -1) return;
fixed (float* v = &vecs[0].X) {
GL.Uniform3(loc, (uint)vecs.Length, v);
}
}
}
public override void SetUniform(string location, Vector4 vec) {
lock (_lock) {
int loc = GetUniformLocation((uint)Program, location);
if (loc == -1) return;
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector4 vCached && vCached == vec) {
return;
}
_uniformValues[loc] = vec;
GL.Uniform4(loc, vec);
}
}
public override void SetUniform(string location, float v) {
lock (_lock) {
int loc = GetUniformLocation((uint)Program, location);
if (loc == -1) return;
if (_uniformValues.TryGetValue(loc, out var val) && val is float vCached && vCached == v) {
return;
}
_uniformValues[loc] = v;
GL.Uniform1(loc, v);
}
}
public override void SetUniform(string location, float[] vs) {
lock (_lock) {
fixed (float* v = &vs[0]) {
GL.Uniform1(GetUniformLocation((uint)Program, location), (uint)vs.Length, v);
}
}
}
public override void Load(string vertShaderSource, string fragShaderSource) {
if (string.IsNullOrWhiteSpace(vertShaderSource) || string.IsNullOrWhiteSpace(fragShaderSource)) {
_log.LogError($"Shader {Name} has no source code!");
return;
}
if (_device.HasOpenGL43 && _device.HasBindless) {
string replacement = "#version 430 core\n#extension GL_ARB_bindless_texture : require";
vertShaderSource = vertShaderSource.Replace("#version 330 core", replacement);
fragShaderSource = fragShaderSource.Replace("#version 330 core", replacement);
}
uint vertexShader = CompileShader(ShaderType.VertexShader, Name, vertShaderSource);
uint fragmentShader = CompileShader(ShaderType.FragmentShader, Name, fragShaderSource);
var prog = GL.CreateProgram();
GLHelpers.CheckErrors(GL, true);
GL.AttachShader(prog, vertexShader);
GLHelpers.CheckErrors(GL, true);
GL.AttachShader(prog, fragmentShader);
GLHelpers.CheckErrors(GL, true);
GL.LinkProgram(prog);
GLHelpers.CheckErrors(GL, true);
GL.GetProgram(prog, GLEnum.LinkStatus, out int success);
GLHelpers.CheckErrors(GL);
if (success != 1) {
var infoLog = GL.GetProgramInfoLog(prog);
_log.LogError($"Error: shader {Name} link failed: {infoLog}");
GL.DeleteProgram(prog);
return;
}
else {
_log.LogTrace($"{(Program != 0 ? "Reloaded" : "Loaded")} shader: {Name}");
}
// Bind SceneData uniform block to point 0 if it exists
var sceneDataIndex = GL.GetUniformBlockIndex(prog, "SceneData");
if (sceneDataIndex != uint.MaxValue) {
GL.UniformBlockBinding(prog, sceneDataIndex, 0);
GLHelpers.CheckErrors(GL);
}
GL.DeleteShader(vertexShader);
GLHelpers.CheckErrors(GL);
GL.DeleteShader(fragmentShader);
GLHelpers.CheckErrors(GL);
if (Program != 0) {
Unload();
}
_uniformLocations.Clear();
_uniformValues.Clear();
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Shader);
Program = prog;
ProgramId = prog;
NeedsLoad = false;
GLHelpers.CheckErrors(GL);
}
private uint CompileShader(ShaderType shaderType, string name, string shaderSource) {
uint shader = GL.CreateShader(shaderType);
GLHelpers.CheckErrors(GL);
GL.ShaderSource(shader, shaderSource);
GLHelpers.CheckErrors(GL);
GL.CompileShader(shader);
GLHelpers.CheckErrors(GL);
GL.GetShader(shader, ShaderParameterName.CompileStatus, out int success);
GLHelpers.CheckErrors(GL);
if (success != 1) {
var infoLog = GL.GetShaderInfoLog(shader);
_log.LogError($"Error: {name}:{shaderType} compilation failed: {infoLog}");
}
return shader;
}
public override void Bind() {
lock (_lock) {
SetActive();
if (Program != 0) {
GL.UseProgram((uint)Program);
}
}
}
public override void Unbind() {
lock (_lock) {
GL.UseProgram(0);
GLHelpers.CheckErrors(GL);
}
}
protected override void Unload() {
lock (_lock) {
if (Program != 0) {
var prog = Program;
Program = 0;
ProgramId = 0;
_device.QueueGLAction(gl => {
gl.DeleteProgram(prog);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Shader);
});
}
}
}
}
}

View file

@ -0,0 +1,230 @@
using Silk.NET.OpenGL;
using System;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// A RAII scope for saving and restoring OpenGL state.
/// </summary>
public unsafe struct GLStateScope : IDisposable {
private readonly GL _gl;
private fixed int _viewport[4];
private bool _scissorTest;
private fixed int _scissorBox[4];
private bool _depthTest;
private int _depthFunc;
private bool _depthMask;
private bool _cullFace;
private int _cullFaceMode;
private int _frontFace;
private bool _blend;
private int _blendSrc;
private int _blendDst;
private int _blendEquation;
// Extended state
private int _blendSrcAlpha;
private int _blendDstAlpha;
private int _blendEquationAlpha;
private fixed byte _colorMask[4];
private fixed float _clearColor[4];
private float _clearDepth;
private int _currentProgram;
private int _vertexArrayBinding;
private int _arrayBufferBinding;
private int _elementArrayBufferBinding;
private int _activeTexture;
private int _textureBinding2D;
private bool _stencilTest;
private int _stencilFunc;
private int _stencilRef;
private int _stencilValueMask;
private int _stencilFail;
private int _stencilPassDepthFail;
private int _stencilPassDepthPass;
private int _stencilWritemask;
private int _unpackAlignment;
private int _packAlignment;
private int _drawFramebufferBinding;
// Skia / Avalonia extra state protections
private fixed float _blendColor[4];
private int _polygonMode;
private bool _sampleAlphaToCoverage;
private bool _multisample;
private bool _primitiveRestart;
private int _readFramebufferBinding;
private int _uniformBufferBinding0;
private float _lineWidth;
private bool _programPointSize;
private int _samplerBinding0;
private int _samplerBinding1;
private int _samplerBinding2;
private int _unpackRowLength;
private int _unpackSkipRows;
private int _unpackSkipPixels;
private bool _sampleAlphaToOne;
private bool _isDisposed;
/// <summary>
/// Captures the current OpenGL state.
/// </summary>
/// <param name="gl"></param>
public GLStateScope(GL gl) {
_gl = gl;
_isDisposed = false;
fixed (int* v = _viewport) _gl.GetInteger(GetPName.Viewport, v);
_scissorTest = _gl.IsEnabled(EnableCap.ScissorTest);
fixed (int* s = _scissorBox) _gl.GetInteger(GetPName.ScissorBox, s);
_depthTest = _gl.IsEnabled(EnableCap.DepthTest);
_gl.GetInteger(GetPName.DepthFunc, out _depthFunc);
byte depthMask = 0;
_gl.GetBoolean((GetPName)GLEnum.DepthWritemask, (bool*)&depthMask);
_depthMask = depthMask != 0;
_cullFace = _gl.IsEnabled(EnableCap.CullFace);
_gl.GetInteger(GetPName.CullFaceMode, out _cullFaceMode);
_gl.GetInteger(GetPName.FrontFace, out _frontFace);
_blend = _gl.IsEnabled(EnableCap.Blend);
_gl.GetInteger(GetPName.BlendSrcRgb, out _blendSrc);
_gl.GetInteger(GetPName.BlendDstRgb, out _blendDst);
_gl.GetInteger(GetPName.BlendSrcAlpha, out _blendSrcAlpha);
_gl.GetInteger(GetPName.BlendDstAlpha, out _blendDstAlpha);
_gl.GetInteger(GetPName.BlendEquationRgb, out _blendEquation);
_gl.GetInteger(GetPName.BlendEquationAlpha, out _blendEquationAlpha);
fixed (byte* c = _colorMask) _gl.GetBoolean((GetPName)GLEnum.ColorWritemask, (bool*)c);
fixed (float* cc = _clearColor) _gl.GetFloat(GetPName.ColorClearValue, cc);
_gl.GetFloat(GetPName.DepthClearValue, out _clearDepth);
_gl.GetInteger(GetPName.CurrentProgram, out _currentProgram);
_gl.GetInteger(GetPName.VertexArrayBinding, out _vertexArrayBinding);
_gl.GetInteger(GetPName.ArrayBufferBinding, out _arrayBufferBinding);
_gl.GetInteger(GetPName.ElementArrayBufferBinding, out _elementArrayBufferBinding);
_gl.GetInteger(GetPName.ActiveTexture, out _activeTexture);
_gl.GetInteger(GetPName.TextureBinding2D, out _textureBinding2D);
_stencilTest = _gl.IsEnabled(EnableCap.StencilTest);
_gl.GetInteger(GetPName.StencilFunc, out _stencilFunc);
_gl.GetInteger(GetPName.StencilRef, out _stencilRef);
_gl.GetInteger(GetPName.StencilValueMask, out _stencilValueMask);
_gl.GetInteger(GetPName.StencilFail, out _stencilFail);
_gl.GetInteger(GetPName.StencilPassDepthFail, out _stencilPassDepthFail);
_gl.GetInteger(GetPName.StencilPassDepthPass, out _stencilPassDepthPass);
_gl.GetInteger(GetPName.StencilWritemask, out _stencilWritemask);
_gl.GetInteger(GetPName.UnpackAlignment, out _unpackAlignment);
_gl.GetInteger(GetPName.PackAlignment, out _packAlignment);
_gl.GetInteger(GetPName.DrawFramebufferBinding, out _drawFramebufferBinding);
fixed (float* bc = _blendColor) _gl.GetFloat(GetPName.BlendColor, bc);
_gl.GetInteger(GetPName.PolygonMode, out _polygonMode);
_sampleAlphaToCoverage = _gl.IsEnabled(EnableCap.SampleAlphaToCoverage);
_multisample = _gl.IsEnabled(EnableCap.Multisample);
_primitiveRestart = _gl.IsEnabled((EnableCap)GLEnum.PrimitiveRestart);
_gl.GetInteger(GetPName.ReadFramebufferBinding, out _readFramebufferBinding);
_gl.GetInteger(GetPName.UniformBufferBinding, out _uniformBufferBinding0);
_gl.GetFloat(GetPName.LineWidth, out _lineWidth);
_programPointSize = _gl.IsEnabled((EnableCap)GLEnum.ProgramPointSize);
_gl.ActiveTexture(TextureUnit.Texture0);
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding0);
_gl.ActiveTexture(TextureUnit.Texture1);
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding1);
_gl.ActiveTexture(TextureUnit.Texture2);
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding2);
_gl.ActiveTexture((TextureUnit)_activeTexture);
_gl.GetInteger((GetPName)GLEnum.UnpackRowLength, out _unpackRowLength);
_gl.GetInteger((GetPName)GLEnum.UnpackSkipRows, out _unpackSkipRows);
_gl.GetInteger((GetPName)GLEnum.UnpackSkipPixels, out _unpackSkipPixels);
_sampleAlphaToOne = _gl.IsEnabled(EnableCap.SampleAlphaToOne);
}
/// <summary>
/// Restores only the scissor state from the scope.
/// </summary>
public void RestoreScissor() {
if (_scissorTest) _gl.Enable(EnableCap.ScissorTest);
else _gl.Disable(EnableCap.ScissorTest);
_gl.Scissor(_scissorBox[0], _scissorBox[1], (uint)_scissorBox[2], (uint)_scissorBox[3]);
}
/// <summary>
/// Restores the captured OpenGL state.
/// </summary>
public void Dispose() {
if (_isDisposed) return;
// Restoring state
if (_currentProgram != 0) _gl.UseProgram((uint)_currentProgram); else _gl.UseProgram(0);
_gl.BindVertexArray((uint)_vertexArrayBinding);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBufferBinding);
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, (uint)_elementArrayBufferBinding);
_gl.BindBuffer(GLEnum.UniformBuffer, (uint)_uniformBufferBinding0);
_gl.ActiveTexture((TextureUnit)_activeTexture);
_gl.BindTexture(TextureTarget.Texture2D, (uint)_textureBinding2D);
if (_stencilTest) _gl.Enable(EnableCap.StencilTest); else _gl.Disable(EnableCap.StencilTest);
_gl.StencilFunc((StencilFunction)_stencilFunc, _stencilRef, (uint)_stencilValueMask);
_gl.StencilOp((StencilOp)_stencilFail, (StencilOp)_stencilPassDepthFail, (StencilOp)_stencilPassDepthPass);
_gl.StencilMask((uint)_stencilWritemask);
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, _unpackAlignment);
_gl.PixelStore(PixelStoreParameter.PackAlignment, _packAlignment);
_gl.PixelStore(PixelStoreParameter.UnpackRowLength, _unpackRowLength);
_gl.PixelStore(PixelStoreParameter.UnpackSkipRows, _unpackSkipRows);
_gl.PixelStore(PixelStoreParameter.UnpackSkipPixels, _unpackSkipPixels);
_gl.ClearColor(_clearColor[0], _clearColor[1], _clearColor[2], _clearColor[3]);
_gl.ClearDepth(_clearDepth);
_gl.Viewport(_viewport[0], _viewport[1], (uint)_viewport[2], (uint)_viewport[3]);
RestoreScissor();
if (_depthTest) _gl.Enable(EnableCap.DepthTest); else _gl.Disable(EnableCap.DepthTest);
_gl.DepthFunc((DepthFunction)_depthFunc);
_gl.DepthMask(_depthMask);
if (_cullFace) _gl.Enable(EnableCap.CullFace); else _gl.Disable(EnableCap.CullFace);
_gl.CullFace((TriangleFace)_cullFaceMode);
_gl.FrontFace((FrontFaceDirection)_frontFace);
if (_blend) _gl.Enable(EnableCap.Blend); else _gl.Disable(EnableCap.Blend);
_gl.BlendFuncSeparate((BlendingFactor)_blendSrc, (BlendingFactor)_blendDst, (BlendingFactor)_blendSrcAlpha, (BlendingFactor)_blendDstAlpha);
_gl.BlendEquationSeparate((BlendEquationModeEXT)_blendEquation, (BlendEquationModeEXT)_blendEquationAlpha);
_gl.BlendColor(_blendColor[0], _blendColor[1], _blendColor[2], _blendColor[3]);
_gl.ColorMask(_colorMask[0] != 0, _colorMask[1] != 0, _colorMask[2] != 0, _colorMask[3] != 0);
_gl.PolygonMode(GLEnum.FrontAndBack, (PolygonMode)_polygonMode);
if (_sampleAlphaToCoverage) _gl.Enable(EnableCap.SampleAlphaToCoverage); else _gl.Disable(EnableCap.SampleAlphaToCoverage);
if (_sampleAlphaToOne) _gl.Enable(EnableCap.SampleAlphaToOne); else _gl.Disable(EnableCap.SampleAlphaToOne);
if (_multisample) _gl.Enable(EnableCap.Multisample); else _gl.Disable(EnableCap.Multisample);
if (_primitiveRestart) _gl.Enable((EnableCap)GLEnum.PrimitiveRestart); else _gl.Disable((EnableCap)GLEnum.PrimitiveRestart);
if (_programPointSize) _gl.Enable((EnableCap)GLEnum.ProgramPointSize); else _gl.Disable((EnableCap)GLEnum.ProgramPointSize);
_gl.LineWidth(_lineWidth);
_gl.BindSampler(0, (uint)_samplerBinding0);
_gl.BindSampler(1, (uint)_samplerBinding1);
_gl.BindSampler(2, (uint)_samplerBinding2);
_gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, (uint)_drawFramebufferBinding);
_gl.BindFramebuffer(FramebufferTarget.ReadFramebuffer, (uint)_readFramebufferBinding);
_isDisposed = true;
}
}
}

View file

@ -0,0 +1,83 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// Resource types for GPU memory tracking.
/// </summary>
public enum GpuResourceType {
Texture,
Buffer,
Shader,
VAO,
FBO,
RBO,
Other
}
/// <summary>
/// Details about a GPU resource type.
/// </summary>
public record GpuResourceDetails(GpuResourceType Type, int Count, long Bytes);
/// <summary>
/// Details about a specific named buffer.
/// </summary>
public record NamedBufferDetails(string Name, long CapacityBytes, long UsedBytes);
/// <summary>
/// Tracks manual VRAM allocations for buffers and textures.
/// </summary>
public static class GpuMemoryTracker {
private static long _allocatedBytes;
private static readonly long[] _allocatedBytesByType = new long[Enum.GetValues<GpuResourceType>().Length];
private static readonly int[] _resourceCountsByType = new int[Enum.GetValues<GpuResourceType>().Length];
private static readonly ConcurrentDictionary<string, NamedBufferDetails> _namedBuffers = new();
public static long AllocatedBytes => Interlocked.Read(ref _allocatedBytes);
public static int VaoCount => _resourceCountsByType[(int)GpuResourceType.VAO];
public static int ShaderCount => _resourceCountsByType[(int)GpuResourceType.Shader];
public static int BufferCount => _resourceCountsByType[(int)GpuResourceType.Buffer];
public static int TextureCount => _resourceCountsByType[(int)GpuResourceType.Texture];
public static int FboCount => _resourceCountsByType[(int)GpuResourceType.FBO];
public static int RboCount => _resourceCountsByType[(int)GpuResourceType.RBO];
public static void TrackAllocation(long sizeInBytes, GpuResourceType type = GpuResourceType.Other) {
Interlocked.Add(ref _allocatedBytes, sizeInBytes);
Interlocked.Add(ref _allocatedBytesByType[(int)type], sizeInBytes);
}
public static void TrackDeallocation(long sizeInBytes, GpuResourceType type = GpuResourceType.Other) {
Interlocked.Add(ref _allocatedBytes, -sizeInBytes);
Interlocked.Add(ref _allocatedBytesByType[(int)type], -sizeInBytes);
}
public static void TrackResourceAllocation(GpuResourceType type) => Interlocked.Increment(ref _resourceCountsByType[(int)type]);
public static void TrackResourceDeallocation(GpuResourceType type) => Interlocked.Decrement(ref _resourceCountsByType[(int)type]);
public static void TrackNamedBuffer(string name, long capacityBytes, long usedBytes) {
_namedBuffers[name] = new NamedBufferDetails(name, capacityBytes, usedBytes);
}
public static void UntrackNamedBuffer(string name) {
_namedBuffers.TryRemove(name, out _);
}
public static IEnumerable<NamedBufferDetails> GetNamedBufferDetails() => _namedBuffers.Values.OrderBy(b => b.Name);
public static IEnumerable<GpuResourceDetails> GetDetails() {
var types = Enum.GetValues<GpuResourceType>();
foreach (var type in types) {
yield return new GpuResourceDetails(
type,
_resourceCountsByType[(int)type],
Interlocked.Read(ref _allocatedBytesByType[(int)type])
);
}
}
}
}

View file

@ -0,0 +1,104 @@
using Chorizite.Core.Render;
using Silk.NET.OpenGL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// Implementation of a framebuffer for OpenGL ES 3.0 using Silk.NET.
/// </summary>
public class ManagedGLFramebuffer : IFramebuffer {
private readonly OpenGLGraphicsDevice _device;
private GL _gl => _device.GL;
private readonly uint _fboId;
private readonly uint _depthStencilRenderbuffer; // 0 if not used
private readonly ITexture _texture;
private readonly int _width;
private readonly int _height;
public ITexture Texture => _texture;
public IntPtr NativeHandle => new IntPtr(_fboId);
public ManagedGLFramebuffer(OpenGLGraphicsDevice device, ITexture texture, int width, int height, bool hasDepthStencil) {
_device = device;
_texture = texture;
_width = width;
_height = height;
// Generate and bind the framebuffer
_fboId = _gl.GenFramebuffer();
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.FBO);
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fboId);
// Attach the texture as the color attachment
_gl.FramebufferTexture2D(
FramebufferTarget.Framebuffer,
FramebufferAttachment.ColorAttachment0,
TextureTarget.Texture2D,
(uint)texture.NativePtr.ToInt32(),
0
);
// Create and attach a depth-stencil renderbuffer if requested
if (true || hasDepthStencil) {
_depthStencilRenderbuffer = _gl.GenRenderbuffer();
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.RBO);
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _depthStencilRenderbuffer);
_gl.RenderbufferStorage(
RenderbufferTarget.Renderbuffer,
InternalFormat.Depth24Stencil8,
(uint)width,
(uint)height
);
_gl.FramebufferRenderbuffer(
FramebufferTarget.Framebuffer,
FramebufferAttachment.DepthStencilAttachment,
RenderbufferTarget.Renderbuffer,
_depthStencilRenderbuffer
);
GpuMemoryTracker.TrackAllocation(_width * _height * 4, GpuResourceType.RBO); // Depth24Stencil8 is 4 bytes per pixel
}
// Check framebuffer completeness
var status = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
if (status != GLEnum.FramebufferComplete) {
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
_gl.DeleteFramebuffer(_fboId);
if (_depthStencilRenderbuffer != 0) {
_gl.DeleteRenderbuffer(_depthStencilRenderbuffer);
}
throw new InvalidOperationException($"Framebuffer creation failed: {status}");
}
var error = _gl.GetError();
if (error != GLEnum.NoError) {
throw new InvalidOperationException($"OpenGL error during framebuffer setup: {error}");
}
// Unbind the framebuffer
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
}
public void Dispose() {
var fboId = _fboId;
var depthStencilRenderbuffer = _depthStencilRenderbuffer;
var width = _width;
var height = _height;
_device.QueueGLAction(gl => {
if (fboId != 0) {
gl.DeleteFramebuffer(fboId);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.FBO);
}
if (depthStencilRenderbuffer != 0) {
gl.DeleteRenderbuffer(depthStencilRenderbuffer);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.RBO);
GpuMemoryTracker.TrackDeallocation(width * height * 4, GpuResourceType.RBO);
}
});
}
}
}

View file

@ -0,0 +1,185 @@
using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
using Chorizite.OpenGLSDLBackend.Lib;
using Silk.NET.OpenGL;
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// OpenGL index buffer
/// </summary>
public unsafe class ManagedGLIndexBuffer : IIndexBuffer {
private uint bufferId;
private readonly OpenGLGraphicsDevice _device;
private void* _mappedPtr;
private GL GL => _device.GL;
/// <inheritdoc />
public int Size { get; private set; }
/// <inheritdoc />
public BufferUsage Usage { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="ManagedGLIndexBuffer"/> class.
/// </summary>
/// <param name="usage">Buffer usage</param>
/// <param name="size">The size of the buffer, in bytes</param>
public unsafe ManagedGLIndexBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
_device = device;
Size = size;
Usage = usage;
// Generate the buffer
bufferId = GL.GenBuffer();
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
GLHelpers.CheckErrors(GL);
// Allocate the buffer with the specified size but no initial data
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
if (_device.HasBufferStorage) {
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
GL.BufferStorage(GLEnum.ElementArrayBuffer, (uint)Size, (void*)0, flags);
_mappedPtr = GL.MapBufferRange(GLEnum.ElementArrayBuffer, 0, (nuint)Size, MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
} else {
GL.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)Size, (void*)0, Usage.ToGL());
}
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackAllocation(Size, GpuResourceType.Buffer);
}
/// <inheritdoc />
public void SetData(uint[] data) {
SetData(data.AsSpan());
}
/// <inheritdoc />
public unsafe void SetData(Span<uint> data) {
uint dataSize = (uint)data.Length * sizeof(uint);
// Ensure the buffer size is sufficient
if (dataSize > Size) {
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
}
if (_mappedPtr != null) {
Span<uint> mappedSpan = new Span<uint>(_mappedPtr, data.Length);
data.CopyTo(mappedSpan);
} else {
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
fixed (uint* dataPtr = &data[0]) {
GL.BufferData(GLEnum.ElementArrayBuffer, dataSize, (void*)dataPtr, Usage.ToGL());
}
GLHelpers.CheckErrors(GL);
GL.BindBuffer(GLEnum.ElementArrayBuffer, 0);
GLHelpers.CheckErrors(GL);
}
}
/// <inheritdoc />
public unsafe void SetSubData(Span<uint> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) {
if (Usage != BufferUsage.Dynamic) {
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
}
if (lengthElements <= 0) {
lengthElements = data.Length - sourceOffsetElements;
}
uint dataSizeBytes = (uint)lengthElements * sizeof(uint);
if (dataSizeBytes == 0) {
return;
}
// Make sure we're not trying to write past the end of the buffer
if (destinationOffsetBytes + dataSizeBytes > Size) {
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
}
if (_mappedPtr != null) {
Span<uint> mappedSpan = new Span<uint>((byte*)_mappedPtr + destinationOffsetBytes, lengthElements);
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
} else {
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
fixed (uint* dataPtr = &data[sourceOffsetElements]) {
GL.BufferSubData(
GLEnum.ElementArrayBuffer,
destinationOffsetBytes,
dataSizeBytes,
(void*)dataPtr);
GLHelpers.CheckErrors(GL);
}
}
}
/// <inheritdoc />
public unsafe void SetSubData(uint[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) {
if (Usage != BufferUsage.Dynamic) {
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
}
if (lengthElements <= 0) {
lengthElements = data.Length - sourceOffsetElements;
}
uint dataSizeBytes = (uint)lengthElements * sizeof(uint);
if (dataSizeBytes == 0) {
return;
}
// Make sure we're not trying to write past the end of the buffer
if (destinationOffsetBytes + dataSizeBytes > Size) {
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
}
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
fixed (uint* dataPtr = &data[sourceOffsetElements]) {
GL.BufferSubData(
GLEnum.ElementArrayBuffer,
destinationOffsetBytes,
dataSizeBytes,
(void*)dataPtr);
GLHelpers.CheckErrors(GL);
}
}
/// <inheritdoc />
public void Bind() {
BaseObjectRenderManager.CurrentIBO = 0;
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
}
/// <inheritdoc />
public void Unbind() {
GL.BindBuffer(GLEnum.ElementArrayBuffer, 0);
GLHelpers.CheckErrors(GL);
}
public unsafe void Dispose() {
_device.QueueGLAction(GL => {
if (bufferId != 0) {
GL.DeleteBuffer(bufferId);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
bufferId = 0;
_mappedPtr = null;
}
});
}
}
}

View file

@ -0,0 +1,204 @@
using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
using Chorizite.OpenGLSDLBackend.Lib;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb {
public unsafe class ManagedGLTexture : ITexture {
private uint _texture;
private readonly OpenGLGraphicsDevice _device;
private GL GL => (_device as OpenGLGraphicsDevice).GL;
/// <inheritdoc/>
public IntPtr NativePtr => (IntPtr)_texture;
/// <inheritdoc/>
public int Width { get; private set; }
/// <inheritdoc/>
public int Height { get; private set; }
public TextureFormat Format => TextureFormat.RGBA8;
public ulong BindlessHandle { get; private set; }
public ulong BindlessWrapHandle { get; private set; }
public ulong BindlessClampHandle { get; private set; }
/// <inheritdoc/>
public ManagedGLTexture(OpenGLGraphicsDevice device, byte[]? source, int width, int height, TextureParameters? texParams = null) {
var p = texParams ?? TextureParameters.Default;
_device = device;
_texture = GL.GenTexture();
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Texture);
Width = width;
Height = height;
GL.BindTexture(GLEnum.Texture2D, _texture);
GLHelpers.CheckErrors(GL);
int maxDimension = Math.Max(width, height);
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
if (_device.HasTextureStorage) {
GL.TexStorage2D(GLEnum.Texture2D, (uint)mipLevels, GLEnum.Rgba8, (uint)width, (uint)height);
GLHelpers.CheckErrors(GL);
}
else {
GL.TexImage2D(GLEnum.Texture2D, 0, (int)InternalFormat.Rgba8, (uint)width, (uint)height, 0, PixelFormat.Rgba, (PixelType)0x1401, (void*)0);
GLHelpers.CheckErrors(GL);
}
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureWrapS, (int)p.WrapS);
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureWrapT, (int)p.WrapT);
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureMinFilter, (int)p.MinFilter);
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureMagFilter, (int)p.MagFilter);
GLHelpers.CheckErrors(GL);
if (p.EnableAnisotropicFiltering && _device.RenderSettings.EnableAnisotropicFiltering)
{
float maxAnisotropy = 0f;
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out maxAnisotropy);
if (maxAnisotropy > 0)
{
GL.TexParameter(GLEnum.Texture2D, GLEnum.TextureMaxAnisotropy, maxAnisotropy);
}
}
if (p.EnableMipmaps) {
GL.GenerateMipmap(GLEnum.Texture2D);
}
GLHelpers.CheckErrors(GL);
GL.BindTexture(GLEnum.Texture2D, 0);
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackAllocation(CalculateSize(), GpuResourceType.Texture);
if (_device.HasBindless && _device.BindlessExtension != null) {
BindlessHandle = _device.BindlessExtension.GetTextureHandle(_texture);
BindlessWrapHandle = _device.BindlessExtension.GetTextureSamplerHandle(_texture, _device.WrapSampler);
BindlessClampHandle = _device.BindlessExtension.GetTextureSamplerHandle(_texture, _device.ClampSampler);
_device.BindlessExtension.MakeTextureHandleResident(BindlessHandle);
_device.BindlessExtension.MakeTextureHandleResident(BindlessWrapHandle);
_device.BindlessExtension.MakeTextureHandleResident(BindlessClampHandle);
}
}
private long CalculateSize() {
int maxDimension = Math.Max(Width, Height);
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
long totalSize = 0;
for (int i = 0; i < mipLevels; i++) {
int w = Math.Max(1, Width >> i);
int h = Math.Max(1, Height >> i);
totalSize += (long)w * h * 4;
}
return totalSize;
}
/// <inheritdoc/>
public ManagedGLTexture(OpenGLGraphicsDevice device, string file) {
throw new NotImplementedException();
}
public void SetData(Rectangle rectangle, byte[] data) {
if (_texture == 0) return;
GLHelpers.CheckErrors(GL);
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
BaseObjectRenderManager.CurrentAtlas = 0;
GL.GetInteger(GLEnum.TextureBinding2D, out int oldBinding);
GL.BindTexture(GLEnum.Texture2D, _texture);
bool wasResident = false;
if (BindlessHandle != 0 && _device.BindlessExtension != null && _device.BindlessExtension.IsTextureHandleResident(BindlessHandle)) {
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessHandle);
wasResident = true;
}
fixed (byte* ptr = data) {
GL.TexSubImage2D(
GLEnum.Texture2D,
0, // level
rectangle.X,
rectangle.Y,
(uint)rectangle.Width,
(uint)rectangle.Height,
PixelFormat.Rgba,
PixelType.UnsignedByte,
ptr
);
}
// Generate mipmaps if needed
GL.GenerateMipmap(GLEnum.Texture2D);
if (wasResident && BindlessHandle != 0 && _device.BindlessExtension != null) {
_device.BindlessExtension.MakeTextureHandleResident(BindlessHandle);
}
GL.BindTexture(GLEnum.Texture2D, (uint)oldBinding);
GL.ActiveTexture((GLEnum)oldActiveTexture);
GLHelpers.CheckErrors(GL);
}
public void Bind(int slot = 0) {
if (slot == 0) {
BaseObjectRenderManager.CurrentAtlas = 0;
}
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
GLEnum targetTextureUnit = GLEnum.Texture0 + slot;
bool changedUnit = (GLEnum)oldActiveTexture != targetTextureUnit;
if (changedUnit) {
GL.ActiveTexture(targetTextureUnit);
}
GL.BindSampler((uint)slot, 0);
GL.BindTexture(GLEnum.Texture2D, (uint)NativePtr);
if (changedUnit) {
GL.ActiveTexture((GLEnum)oldActiveTexture);
}
GLHelpers.CheckErrors(GL);
}
public void Unbind() {
GL.BindTexture(GLEnum.Texture2D, 0);
GLHelpers.CheckErrors(GL);
}
protected void ReleaseTexture() {
_device.QueueGLAction(GL => {
if (_device.BindlessExtension != null) {
if (BindlessHandle != 0) {
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessHandle);
BindlessHandle = 0;
}
if (BindlessWrapHandle != 0) {
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessWrapHandle);
BindlessWrapHandle = 0;
}
if (BindlessClampHandle != 0) {
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessClampHandle);
BindlessClampHandle = 0;
}
}
if (_texture != 0) {
GL.DeleteTexture(_texture);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
GpuMemoryTracker.TrackDeallocation(CalculateSize(), GpuResourceType.Texture);
}
GLHelpers.CheckErrors(GL);
_texture = 0;
});
}
public void Dispose() {
ReleaseTexture();
}
}
}

View file

@ -0,0 +1,436 @@
using AcDream.Core.Rendering.Wb;
using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
using Chorizite.OpenGLSDLBackend.Lib;
// Use our extracted TextureHelpers (T3), not the WB original — disambiguate explicitly
using TextureHelpers = AcDream.Core.Rendering.Wb.TextureHelpers;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
using System.Runtime.InteropServices;
namespace AcDream.App.Rendering.Wb {
public class ManagedGLTextureArray : ITextureArray {
private readonly bool[] _usedLayers;
private readonly GL GL;
private readonly OpenGLGraphicsDevice _device;
private readonly ILogger _logger;
private static int _nextId = 0;
private bool _needsMipmapRegeneration = false;
private readonly bool _isCompressed;
private int _mipmapDirtyCount = 0;
private readonly object _mipmapLock = new object();
private uint _pboId;
private int _pboSize;
private readonly List<TextureLayerUpdate> _pendingUpdates = new();
private struct TextureLayerUpdate {
public int Layer;
public int Offset;
public int Size;
public PixelFormat? UploadPixelFormat;
public PixelType? UploadPixelType;
}
public int Slot { get; } = _nextId++;
public int Width { get; private set; }
public int Height { get; private set; }
public int Size { get; private set; }
public TextureFormat Format { get; private set; }
public nint NativePtr { get; private set; }
public ulong BindlessHandle { get; private set; }
public ulong BindlessWrapHandle { get; private set; }
public ulong BindlessClampHandle { get; private set; }
public long TotalSizeInBytes => CalculateTotalSize();
public ManagedGLTextureArray(OpenGLGraphicsDevice graphicsDevice, TextureFormat format, int width, int height,
int size, ILogger logger, TextureParameters? texParams = null) {
var p = texParams ?? TextureParameters.Default;
if (width <= 0 || height <= 0 || size <= 0) {
throw new ArgumentException($"Invalid texture array dimensions: {width}x{height}x{size}");
}
Format = format;
Width = width;
Height = height;
Size = size;
_usedLayers = new bool[size];
_device = graphicsDevice;
GL = graphicsDevice.GL;
_logger = logger;
_isCompressed = IsCompressedFormat(format);
GLHelpers.CheckErrors(GL);
NativePtr = (nint)GL.GenTexture();
if (NativePtr == 0) {
throw new InvalidOperationException("Failed to generate texture array.");
}
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Texture);
GLHelpers.CheckErrors(GL);
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
GLHelpers.CheckErrors(GL);
int maxDimension = Math.Max(width, height);
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
GL.TexStorage3D(GLEnum.Texture2DArray, (uint)mipLevels, format.ToGL(), (uint)width, (uint)height,
(uint)size);
GLHelpers.CheckErrorsWithContext(GL,
$"Creating texture array storage (Format={format}, Size={width}x{height}x{size}, MipLevels={mipLevels})");
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMinFilter,
(int)p.MinFilter);
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMaxLevel, (int)mipLevels - 1);
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMagFilter, (int)p.MagFilter);
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapS, (int)p.WrapS);
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapT, (int)p.WrapT);
if (p.EnableAnisotropicFiltering && graphicsDevice.RenderSettings.EnableAnisotropicFiltering) {
float maxAnisotropy = 0f;
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out maxAnisotropy);
if (maxAnisotropy > 0) {
GL.TexParameter(GLEnum.Texture2DArray, GLEnum.TextureMaxAnisotropy, maxAnisotropy);
}
}
// Set texture swizzle for single-channel formats
if (format == TextureFormat.A8) {
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleR, (int)GLEnum.One);
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleG, (int)GLEnum.One);
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleB, (int)GLEnum.One);
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleA, (int)GLEnum.Red);
}
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackAllocation(CalculateTotalSize(), GpuResourceType.Texture);
if (_device.HasBindless && _device.BindlessExtension != null) {
BindlessHandle = _device.BindlessExtension.GetTextureHandle((uint)NativePtr);
BindlessWrapHandle = _device.BindlessExtension.GetTextureSamplerHandle((uint)NativePtr, _device.WrapSampler);
BindlessClampHandle = _device.BindlessExtension.GetTextureSamplerHandle((uint)NativePtr, _device.ClampSampler);
_device.BindlessExtension.MakeTextureHandleResident(BindlessHandle);
_device.BindlessExtension.MakeTextureHandleResident(BindlessWrapHandle);
_device.BindlessExtension.MakeTextureHandleResident(BindlessClampHandle);
}
_pboId = GL.GenBuffer();
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
}
public long CalculateTotalSize() {
int maxDimension = Math.Max(Width, Height);
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
long layerSize = GetExpectedDataSize();
long totalSize = 0;
for (int i = 0; i < mipLevels; i++) {
int w = Math.Max(1, Width >> i);
int h = Math.Max(1, Height >> i);
if (_isCompressed) {
totalSize += TextureHelpers.GetCompressedLayerSize(w, h, Format) * Size;
}
else {
totalSize += (long)w * h * (layerSize / (Width * Height)) * Size;
}
}
return totalSize;
}
private bool IsCompressedFormat(TextureFormat format) {
return format == TextureFormat.DXT1 ||
format == TextureFormat.DXT3 ||
format == TextureFormat.DXT5;
}
public void Bind(int slot = 0) {
if (NativePtr == 0) {
return;
}
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
GLEnum targetTextureUnit = GLEnum.Texture0 + slot;
bool changedUnit = (GLEnum)oldActiveTexture != targetTextureUnit;
if (changedUnit) {
GL.ActiveTexture(targetTextureUnit);
}
GL.BindSampler((uint)slot, 0);
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
if (changedUnit) {
GL.ActiveTexture((GLEnum)oldActiveTexture);
}
GLHelpers.CheckErrors(GL);
}
public unsafe int AddLayer(byte[] data) {
return AddLayer(data, null, null);
}
public unsafe int AddLayer(byte[] data, PixelFormat? uploadPixelFormat, PixelType? uploadPixelType) {
for (int i = 0; i < _usedLayers.Length; i++) {
if (!_usedLayers[i]) {
UpdateLayerInternal(i, data, uploadPixelFormat, uploadPixelType);
_usedLayers[i] = true;
return i;
}
}
throw new InvalidOperationException(
$"No free layers available in texture array (Slot={Slot}, Size={Width}x{Height}x{Size}).");
}
public unsafe int AddLayer(Span<byte> data) {
return AddLayer(data.ToArray());
}
public void UpdateLayer(int layer, byte[] data) {
UpdateLayer(layer, data, null, null);
}
public void UpdateLayer(int layer, byte[] data, PixelFormat? uploadPixelFormat, PixelType? uploadPixelType) {
UpdateLayerInternal(layer, data, uploadPixelFormat, uploadPixelType);
_usedLayers[layer] = true;
}
private unsafe void UpdateLayerInternal(int layer, byte[] data, PixelFormat? uploadPixelFormat,
PixelType? uploadPixelType) {
if (NativePtr == 0) {
throw new InvalidOperationException("Texture array not created.");
}
if (layer < 0 || layer >= Size) {
throw new ArgumentOutOfRangeException(nameof(layer),
$"Layer index {layer} is out of range [0, {Size - 1}] (Slot={Slot}).");
}
int currentPboOffset = 0;
lock (_mipmapLock) {
if (_pendingUpdates.Count > 0) {
var lastUpdate = _pendingUpdates[^1];
currentPboOffset = lastUpdate.Offset + lastUpdate.Size;
}
// Align to 4 bytes for safety
currentPboOffset = (currentPboOffset + 3) & ~3;
if (currentPboOffset + data.Length > _pboSize) {
// Flush existing updates first because BufferData will orphan/clear the PBO
if (_pendingUpdates.Count > 0) {
ProcessDirtyUpdatesInternal();
}
currentPboOffset = 0;
int newSize = Math.Max(_pboSize * 2, data.Length);
newSize = Math.Max(newSize, GetExpectedDataSize() * 4); // Initial size 4 layers
GL.BindBuffer(GLEnum.PixelUnpackBuffer, _pboId);
GL.BufferData(GLEnum.PixelUnpackBuffer, (nuint)newSize, (void*)0, GLEnum.StreamDraw);
if (_pboSize > 0) {
GpuMemoryTracker.TrackDeallocation(_pboSize, GpuResourceType.Buffer);
}
_pboSize = newSize;
GpuMemoryTracker.TrackAllocation(_pboSize, GpuResourceType.Buffer);
}
else {
GL.BindBuffer(GLEnum.PixelUnpackBuffer, _pboId);
}
fixed (byte* ptr = data) {
GL.BufferSubData(GLEnum.PixelUnpackBuffer, (nint)currentPboOffset, (nuint)data.Length, ptr);
}
GL.BindBuffer(GLEnum.PixelUnpackBuffer, 0);
_pendingUpdates.Add(new TextureLayerUpdate {
Layer = layer,
Offset = currentPboOffset,
Size = data.Length,
UploadPixelFormat = uploadPixelFormat,
UploadPixelType = uploadPixelType
});
_needsMipmapRegeneration = true;
_mipmapDirtyCount++;
}
}
public void ProcessDirtyUpdates() {
lock (_mipmapLock) {
ProcessDirtyUpdatesInternal();
}
}
private unsafe void ProcessDirtyUpdatesInternal() {
if (_pendingUpdates.Count == 0 && !_needsMipmapRegeneration) return;
GLHelpers.CheckErrors(GL);
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
BaseObjectRenderManager.CurrentAtlas = 0;
GL.GetInteger(GLEnum.TextureBinding2DArray, out int oldBinding);
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
bool wasResident = false;
if (BindlessHandle != 0 && _device.BindlessExtension != null && _device.BindlessExtension.IsTextureHandleResident(BindlessHandle)) {
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessHandle);
wasResident = true;
}
if (_pendingUpdates.Count > 0) {
GL.BindBuffer(GLEnum.PixelUnpackBuffer, _pboId);
foreach (var update in _pendingUpdates) {
if (_isCompressed) {
var internalFormat = Format.ToCompressedGL();
GL.CompressedTexSubImage3D(GLEnum.Texture2DArray, 0, 0, 0, update.Layer,
(uint)Width, (uint)Height, 1, internalFormat, (uint)update.Size, (void*)update.Offset);
}
else {
var pixelFormat = update.UploadPixelFormat ?? Format.ToPixelFormat();
var pixelType = update.UploadPixelType ?? Format.ToPixelType();
GL.TexSubImage3D(GLEnum.Texture2DArray, 0, 0, 0, update.Layer, (uint)Width, (uint)Height, 1,
pixelFormat, pixelType, (void*)update.Offset);
}
}
GL.BindBuffer(GLEnum.PixelUnpackBuffer, 0);
_pendingUpdates.Clear();
}
if (_needsMipmapRegeneration && _mipmapDirtyCount > 0) {
if (_isCompressed) {
_logger.LogDebug("Skipping automatic mipmap generation for compressed texture array (Slot={Slot})", Slot);
}
else if (!GLHelpers.ValidateTextureMipmapStatus(GL, GLEnum.Texture2DArray, out var errorMessage)) {
_logger.LogWarning("Mipmap validation failed for texture array (Slot={Slot}): {Error}", Slot, errorMessage);
}
else {
try {
GL.GenerateMipmap(GLEnum.Texture2DArray);
}
catch (Exception ex) {
_logger.LogWarning(ex, "Failed to generate mipmaps for texture array (Slot={Slot}).", Slot);
}
}
_mipmapDirtyCount = 0;
_needsMipmapRegeneration = false;
}
if (wasResident && BindlessHandle != 0 && _device.BindlessExtension != null) {
_device.BindlessExtension.MakeTextureHandleResident(BindlessHandle);
}
GL.BindTexture(GLEnum.Texture2DArray, (uint)oldBinding);
GL.ActiveTexture((GLEnum)oldActiveTexture);
GLHelpers.CheckErrors(GL);
}
private void ClearLayerForMipmap(int layer) {
// Upload a single black/transparent pixel to make layer defined
byte[] clearData = new byte[GetExpectedDataSize()];
Array.Clear(clearData, 0, clearData.Length); // Zero-fill (black/transparent)
UpdateLayerInternal(layer, clearData, null, null);
}
private int GetExpectedDataSize() {
if (_isCompressed) {
return TextureHelpers.GetCompressedLayerSize(Width, Height, Format);
}
return Format switch {
TextureFormat.RGBA8 => Width * Height * 4,
TextureFormat.RGB8 => Width * Height * 3,
TextureFormat.A8 => Width * Height * 1,
TextureFormat.Rgba32f => Width * Height * 16,
_ => throw new NotSupportedException($"Unsupported format {Format}")
};
}
public void RemoveLayer(int layer) {
if (layer < 0 || layer >= Size) {
throw new ArgumentOutOfRangeException(nameof(layer),
$"Layer index {layer} is out of range [0, {Size - 1}] (Slot={Slot}).");
}
if (!_usedLayers[layer]) {
throw new InvalidOperationException($"Layer {layer} is already free (Slot={Slot}).");
}
_usedLayers[layer] = false;
// Make layer defined for mipmap completeness (uncompressed only)
if (!_isCompressed) {
ClearLayerForMipmap(layer);
}
lock (_mipmapLock) {
_mipmapDirtyCount++; // Mark dirty to regen
_needsMipmapRegeneration = true;
}
}
public bool IsLayerUsed(int layer) {
if (layer < 0 || layer >= Size) return false;
return _usedLayers[layer];
}
public int GetUsedLayerCount() {
return _usedLayers.Count(x => x);
}
public void Unbind() {
GL.BindTexture(GLEnum.Texture2DArray, 0);
GLHelpers.CheckErrors(GL);
}
public void GenerateMipmaps() {
_needsMipmapRegeneration = true;
lock (_mipmapLock) {
_mipmapDirtyCount++;
}
}
public void Dispose() {
_device.QueueGLAction(GL => {
if (_device.BindlessExtension != null) {
if (BindlessHandle != 0) {
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessHandle);
BindlessHandle = 0;
}
if (BindlessWrapHandle != 0) {
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessWrapHandle);
BindlessWrapHandle = 0;
}
if (BindlessClampHandle != 0) {
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessClampHandle);
BindlessClampHandle = 0;
}
}
if (NativePtr != 0) {
GL.DeleteTexture((uint)NativePtr);
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackDeallocation(CalculateTotalSize(), GpuResourceType.Texture);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
NativePtr = 0;
}
if (_pboId != 0) {
GL.DeleteBuffer(_pboId);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
if (_pboSize > 0) {
GpuMemoryTracker.TrackDeallocation(_pboSize, GpuResourceType.Buffer);
}
_pboId = 0;
}
});
}
}
}

View file

@ -0,0 +1,143 @@
using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
using Silk.NET.OpenGL;
using System.Runtime.InteropServices;
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
// IUniformBuffer is in Chorizite.Core.dll but under the Chorizite.OpenGLSDLBackend namespace
using IUniformBuffer = Chorizite.OpenGLSDLBackend.IUniformBuffer;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// OpenGL uniform buffer
/// </summary>
public unsafe class ManagedGLUniformBuffer : IUniformBuffer {
private uint bufferId;
private readonly OpenGLGraphicsDevice _device;
private GL GL => _device.GL;
/// <inheritdoc />
public int Size { get; private set; }
/// <inheritdoc />
public BufferUsage Usage { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="ManagedGLUniformBuffer"/> class.
/// </summary>
/// <param name="device">Graphics device</param>
/// <param name="usage">Buffer usage</param>
/// <param name="size">The size of the buffer, in bytes</param>
public unsafe ManagedGLUniformBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
_device = device;
Size = size;
Usage = usage;
// Generate the buffer
bufferId = GL.GenBuffer();
if (bufferId == 0) {
throw new Exception("Failed to generate uniform buffer.");
}
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
GLHelpers.CheckErrors(GL);
// Allocate the buffer with the specified size
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
GLHelpers.CheckErrors(GL);
GL.BufferData(
GLEnum.UniformBuffer,
(uint)Size,
(void*)0, // No initial data
GLEnum.DynamicDraw);
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackAllocation(Size, GpuResourceType.Buffer);
}
/// <inheritdoc />
public unsafe void SetData<T>(T[] data) where T : unmanaged {
SetData(data.AsSpan());
}
/// <inheritdoc />
public unsafe void SetData<T>(Span<T> data) where T : unmanaged {
uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf<T>();
// Ensure the buffer size is sufficient
if (dataSize > Size) {
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
}
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
fixed (T* ptr = data) {
GL.BufferSubData(GLEnum.UniformBuffer, 0, (nuint)dataSize, ptr);
}
}
/// <inheritdoc />
public unsafe void SetSubData<T>(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : unmanaged {
SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
}
/// <inheritdoc />
public unsafe void SetSubData<T>(Span<T> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : unmanaged {
if (lengthElements <= 0) {
lengthElements = data.Length - sourceOffsetElements;
}
uint dataSizeBytes = (uint)lengthElements * (uint)Marshal.SizeOf<T>();
// Validate buffer bounds
if (destinationOffsetBytes + dataSizeBytes > Size) {
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
}
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
fixed (T* ptr = data.Slice(sourceOffsetElements, lengthElements)) {
GL.BufferSubData(GLEnum.UniformBuffer, (nint)destinationOffsetBytes, (nuint)dataSizeBytes, ptr);
}
}
/// <summary>
/// Sets a single piece of data in the buffer.
/// </summary>
public unsafe void SetData<T>(ref T data) where T : unmanaged {
fixed (T* pData = &data) {
SetData(new Span<T>(pData, 1));
}
}
/// <summary>
/// Binds the buffer to the specified binding point.
/// </summary>
/// <param name="bindingPoint">The binding point to bind to</param>
public void Bind(uint bindingPoint) {
GL.BindBufferBase(GLEnum.UniformBuffer, bindingPoint, bufferId);
GLHelpers.CheckErrors(GL);
}
/// <inheritdoc />
public void Bind() {
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
GLHelpers.CheckErrors(GL);
}
/// <inheritdoc />
public void Unbind() {
GL.BindBuffer(GLEnum.UniformBuffer, 0);
GLHelpers.CheckErrors(GL);
}
public void Dispose() {
_device.QueueGLAction(GL => {
if (bufferId != 0) {
GL.DeleteBuffer(bufferId);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
bufferId = 0;
}
});
}
}
}

View file

@ -0,0 +1,77 @@
using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
using Silk.NET.OpenGL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VertexAttribType = Silk.NET.OpenGL.VertexAttribType;
namespace AcDream.App.Rendering.Wb {
public unsafe class ManagedGLVertexArray : IVertexArray {
private readonly OpenGLGraphicsDevice _device;
private GL GL => _device.GL;
private uint _vaoId = 0;
public ManagedGLVertexArray(OpenGLGraphicsDevice device, IVertexBuffer buffer, VertexFormat format) {
_device = device;
// Generate the vertex array
_vaoId = GL.GenVertexArray();
GLHelpers.CheckErrors(GL);
if (_vaoId == 0) {
throw new Exception("Failed to generate vertex array.");
}
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO);
SetVertexBuffer(buffer, format);
}
public void SetVertexBuffer(IVertexBuffer buffer, VertexFormat format) {
GL.BindVertexArray(_vaoId);
GLHelpers.CheckErrors(GL);
buffer.Bind();
for (int i = 0; i < format.Attributes.Length; i++) {
var attr = format.Attributes[i];
GL.EnableVertexAttribArray((uint)i);
GLHelpers.CheckErrors(GL);
GL.VertexAttribPointer((uint)i, attr.Size, Convert(attr.Type), attr.Normalized, (uint)format.Stride, attr.Offset);
GLHelpers.CheckErrors(GL);
}
GL.BindVertexArray(0);
GLHelpers.CheckErrors(GL);
}
private GLEnum Convert(Chorizite.Core.Render.Enums.VertexAttribType type) => type switch {
Chorizite.Core.Render.Enums.VertexAttribType.Float => GLEnum.Float,
Chorizite.Core.Render.Enums.VertexAttribType.Int => GLEnum.Int,
Chorizite.Core.Render.Enums.VertexAttribType.UnsignedInt => GLEnum.UnsignedInt,
Chorizite.Core.Render.Enums.VertexAttribType.UnsignedByte => GLEnum.UnsignedByte,
Chorizite.Core.Render.Enums.VertexAttribType.Byte => GLEnum.Byte,
_ => throw new NotSupportedException()
};
public void Bind() {
GL.BindVertexArray(_vaoId);
GLHelpers.CheckErrors(GL);
}
public void Unbind() {
GL.BindVertexArray(0);
GLHelpers.CheckErrors(GL);
}
public void Dispose() {
_device.QueueGLAction(GL => {
if (_vaoId != 0) {
GL.DeleteVertexArray(_vaoId);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO);
_vaoId = 0;
}
GLHelpers.CheckErrors(GL);
});
}
}
}

View file

@ -0,0 +1,185 @@
using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
using System.Buffers;
using System.Runtime.InteropServices;
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// OpenGL vertex buffer
/// </summary>
public unsafe class ManagedGLVertexBuffer : IVertexBuffer {
private uint bufferId;
private readonly OpenGLGraphicsDevice _device;
private void* _mappedPtr;
private GL GL => _device.GL;
/// <inheritdoc />
public int Size { get; private set; }
/// <inheritdoc />
public BufferUsage Usage { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="ManagedGLVertexBuffer"/> class.
/// </summary>
/// <param name="usage">Buffer usage</param>
/// <param name="size">The size of the buffer, in bytes</param>
public unsafe ManagedGLVertexBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
_device = device;
Size = size;
Usage = usage;
// Generate the buffer
bufferId = GL.GenBuffer();
if (bufferId == 0) {
throw new Exception("Failed to generate vertex buffer.");
}
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
GLHelpers.CheckErrors(GL);
// Allocate the buffer with the specified size but no initial data
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
if (_device.HasBufferStorage) {
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
GL.BufferStorage(GLEnum.ArrayBuffer, (uint)Size, (void*)0, flags);
_mappedPtr = GL.MapBufferRange(GLEnum.ArrayBuffer, 0, (nuint)Size, MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
} else {
GL.BufferData(
GLEnum.ArrayBuffer,
(uint)Size,
(void*)0, // No initial data
Usage.ToGL());
}
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackAllocation(Size, GpuResourceType.Buffer);
}
/// <inheritdoc />
public unsafe void SetData<T>(T[] data) where T : IVertex {
SetData(data.AsSpan());
}
/// <inheritdoc />
public unsafe void SetData<T>(Span<T> data) where T : IVertex {
uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf<T>();
// Ensure the buffer size is sufficient
if (dataSize > Size) {
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
}
if (_mappedPtr != null) {
Span<T> mappedSpan = new Span<T>(_mappedPtr, data.Length);
data.CopyTo(mappedSpan);
} else {
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
// Map the buffer for writing
void* mappedPtr = GL.MapBufferRange(
GLEnum.ArrayBuffer,
0, // offset
dataSize,
MapBufferAccessMask.WriteBit | MapBufferAccessMask.InvalidateBufferBit // Overwrite entire buffer
);
if (mappedPtr == null) {
throw new Exception("Failed to map buffer for writing.");
}
try {
// Copy data directly to mapped memory
Span<T> mappedSpan = new Span<T>(mappedPtr, data.Length);
data.CopyTo(mappedSpan);
}
finally {
// Unmap the buffer
GL.UnmapBuffer(GLEnum.ArrayBuffer);
GLHelpers.CheckErrors(GL);
}
}
}
public unsafe void SetSubData<T>(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : IVertex {
SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
}
/// <inheritdoc />
public unsafe void SetSubData<T>(Span<T> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : IVertex {
if (Usage != BufferUsage.Dynamic) {
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
}
if (lengthElements <= 0) {
lengthElements = data.Length - sourceOffsetElements;
}
uint dataSizeBytes = (uint)lengthElements * (uint)Marshal.SizeOf<T>();
// Validate buffer bounds
if (destinationOffsetBytes + dataSizeBytes > Size) {
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
}
if (_mappedPtr != null) {
Span<T> mappedSpan = new Span<T>((byte*)_mappedPtr + destinationOffsetBytes, lengthElements);
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
} else {
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
// Map the specific range of the buffer
void* mappedPtr = GL.MapBufferRange(
GLEnum.ArrayBuffer,
destinationOffsetBytes,
dataSizeBytes,
MapBufferAccessMask.WriteBit // Write access for partial update
);
if (mappedPtr == null) {
throw new Exception("Failed to map buffer for writing.");
}
try {
// Copy the specified range of data to the mapped memory
Span<T> mappedSpan = new Span<T>(mappedPtr, lengthElements);
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
}
finally {
// Unmap the buffer
GL.UnmapBuffer(GLEnum.ArrayBuffer);
GLHelpers.CheckErrors(GL);
}
}
}
public void Bind() {
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
}
public void Unbind() {
GL.BindBuffer(GLEnum.ArrayBuffer, 0);
GLHelpers.CheckErrors(GL);
}
public unsafe void Dispose() {
_device.QueueGLAction(GL => {
if (bufferId != 0) {
GL.DeleteBuffer(bufferId);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
bufferId = 0;
_mappedPtr = null;
}
});
}
}
}

View file

@ -0,0 +1,633 @@
using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
// IUniformBuffer is in Chorizite.Core.dll but under the Chorizite.OpenGLSDLBackend namespace
using IUniformBuffer = Chorizite.OpenGLSDLBackend.IUniformBuffer;
using Silk.NET.OpenGL.Extensions.ARB;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
using PolygonMode = Silk.NET.OpenGL.PolygonMode;
using PrimitiveType = Silk.NET.OpenGL.PrimitiveType;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// OpenGL graphics device
/// </summary>
public unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice {
private readonly ILogger _log;
private readonly DebugRenderSettings _renderSettings;
public GL GL { get; }
public DebugRenderSettings RenderSettings => _renderSettings;
private readonly ConcurrentQueue<Action<GL>> _glThreadQueue = new();
public void QueueGLAction(Action<GL> action) {
_glThreadQueue.Enqueue(action);
}
public void ProcessGLQueue() {
while (_glThreadQueue.TryDequeue(out var action)) {
try {
action(GL);
} catch (Exception ex) {
_log.LogError(ex, "Error processing GL queue action");
}
}
}
public bool HasBindless { get; private set; }
public bool HasOpenGL43 { get; private set; }
public bool HasBufferStorage { get; private set; }
public bool HasTextureStorage { get; private set; }
public ArbBindlessTexture? BindlessExtension { get; private set; }
public uint InstanceVBO { get; private set; }
public void* InstanceVBOPtr { get; private set; }
public uint SharedQuadVBO { get; private set; }
public uint SharedDebugVAO { get; private set; }
public uint SharedDebugInstanceVBO { get; private set; }
public Chorizite.OpenGLSDLBackend.Lib.ParticleBatcher ParticleBatcher { get; private set; } = null!;
/// <summary>OpenGL sampler object with TextureWrapMode.Repeat (for meshes with wrapping UVs).</summary>
public uint WrapSampler { get; private set; }
/// <summary>OpenGL sampler object with TextureWrapMode.ClampToEdge (for meshes without wrapping UVs).</summary>
public uint ClampSampler { get; private set; }
private ManagedGLUniformBuffer? _sceneDataBuffer;
/// <summary>Shared SceneData UBO.</summary>
public ManagedGLUniformBuffer SceneDataBuffer => _sceneDataBuffer!;
private SceneData _currentSceneData;
public SceneData CurrentSceneData => _currentSceneData;
public void SetSceneData(ref SceneData data) {
_currentSceneData = data;
SceneDataBuffer.SetData(ref data);
}
private int _instanceBufferCapacity = 0;
private int _instanceBufferStride = 0;
/// <inheritdoc />
public override IntPtr NativeDevice { get; }
protected OpenGLGraphicsDevice() : base() {
_log = null!;
_renderSettings = null!;
GL = null!;
}
public OpenGLGraphicsDevice(GL gl, ILogger log, DebugRenderSettings renderSettings, bool allowBindless = true) : base() {
_log = log;
_renderSettings = renderSettings;
GL = gl;
GLHelpers.Init(this, log);
try {
GL.GetInteger(GLEnum.MajorVersion, out int major);
GL.GetInteger(GLEnum.MinorVersion, out int minor);
HasOpenGL43 = major > 4 || (major == 4 && minor >= 3);
HasTextureStorage = major > 4 || (major == 4 && minor >= 2) || GL.IsExtensionPresent("GL_ARB_texture_storage");
HasBufferStorage = major > 4 || (major == 4 && minor >= 4) || GL.IsExtensionPresent("GL_ARB_buffer_storage");
if (allowBindless && GL.TryGetExtension(out ArbBindlessTexture ext)) {
BindlessExtension = ext;
HasBindless = true;
} else {
HasBindless = false;
}
} catch {
HasOpenGL43 = false;
HasBindless = false;
}
GL.GenBuffers(1, out uint instanceVbo);
InstanceVBO = instanceVbo;
// Create sampler objects for wrap vs clamp
WrapSampler = GL.GenSampler();
GL.SamplerParameter(WrapSampler, SamplerParameterI.WrapS, (int)TextureWrapMode.Repeat);
GL.SamplerParameter(WrapSampler, SamplerParameterI.WrapT, (int)TextureWrapMode.Repeat);
GL.SamplerParameter(WrapSampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.LinearMipmapLinear);
GL.SamplerParameter(WrapSampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
if (renderSettings.EnableAnisotropicFiltering) {
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
if (maxAniso > 0) GL.SamplerParameter(WrapSampler, GLEnum.TextureMaxAnisotropy, maxAniso);
}
ClampSampler = GL.GenSampler();
GL.SamplerParameter(ClampSampler, SamplerParameterI.WrapS, (int)TextureWrapMode.ClampToEdge);
GL.SamplerParameter(ClampSampler, SamplerParameterI.WrapT, (int)TextureWrapMode.ClampToEdge);
GL.SamplerParameter(ClampSampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.LinearMipmapLinear);
GL.SamplerParameter(ClampSampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
if (renderSettings.EnableAnisotropicFiltering) {
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
if (maxAniso > 0) GL.SamplerParameter(ClampSampler, GLEnum.TextureMaxAnisotropy, maxAniso);
}
_sceneDataBuffer = new ManagedGLUniformBuffer(this, BufferUsage.Dynamic, Marshal.SizeOf<SceneData>());
InitializeSharedDebugResources();
// T3 interim: ParticleBatcher (Chorizite.OpenGLSDLBackend.Lib.ParticleBatcher) is a T4 type
// (Particle batcher + emitter extraction is in T4). It expects the WB-original
// OpenGLGraphicsDevice type; we cannot pass `this` until T4 extracts it alongside us.
// The property is set to null! here; T4 will restore the real construction.
ParticleBatcher = null!;
}
private void InitializeSharedDebugResources() {
// Unit quad vertices for two triangles (0 to 1 for length, -0.5 to 0.5 for thickness)
float[] quadVertices = {
0.0f, -0.5f,
1.0f, -0.5f,
1.0f, 0.5f,
0.0f, -0.5f,
1.0f, 0.5f,
0.0f, 0.5f
};
GL.GenBuffers(1, out uint quadVbo);
SharedQuadVBO = quadVbo;
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
fixed (float* pQuad = quadVertices) {
GL.BufferData(GLEnum.ArrayBuffer, (nuint)(quadVertices.Length * sizeof(float)), pQuad, GLEnum.StaticDraw);
}
GL.GenBuffers(1, out uint debugInstanceVbo);
SharedDebugInstanceVBO = debugInstanceVbo;
// Initial capacity for debug instances
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
GL.BufferData(GLEnum.ArrayBuffer, (nuint)(1024 * 44), (void*)0, GLEnum.StreamDraw); // 44 bytes is sizeof(LineInstance)
GL.GenVertexArrays(1, out uint debugVao);
SharedDebugVAO = debugVao;
GL.BindVertexArray(SharedDebugVAO);
// Quad Pos attribute (location 0)
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
GL.EnableVertexAttribArray(0);
GL.VertexAttribPointer(0, 2, GLEnum.Float, false, 2 * sizeof(float), (void*)0);
// Instance attributes
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
uint lineInstanceSize = 44; // Marshal.SizeOf<LineInstance>() - we'll hardcode or use a constant later
// aStart (location 1)
GL.EnableVertexAttribArray(1);
GL.VertexAttribPointer(1, 3, GLEnum.Float, false, lineInstanceSize, (void*)0);
GL.VertexAttribDivisor(1, 1);
// aEnd (location 2)
GL.EnableVertexAttribArray(2);
GL.VertexAttribPointer(2, 3, GLEnum.Float, false, lineInstanceSize, (void*)12); // OffsetOf End
GL.VertexAttribDivisor(2, 1);
// aColor (location 3)
GL.EnableVertexAttribArray(3);
GL.VertexAttribPointer(3, 4, GLEnum.Float, false, lineInstanceSize, (void*)24); // OffsetOf Color
GL.VertexAttribDivisor(3, 1);
// aThickness (location 4)
GL.EnableVertexAttribArray(4);
GL.VertexAttribPointer(4, 1, GLEnum.Float, false, lineInstanceSize, (void*)40); // OffsetOf Thickness
GL.VertexAttribDivisor(4, 1);
GL.BindVertexArray(0);
}
public void EnsureInstanceBufferCapacity(int count, int stride, bool forceOrphan = false) {
if (count <= _instanceBufferCapacity && !forceOrphan) return;
if (_instanceBufferCapacity > 0) {
GpuMemoryTracker.TrackDeallocation(_instanceBufferCapacity * _instanceBufferStride);
}
_instanceBufferCapacity = Math.Max(count, 256);
_instanceBufferStride = stride;
if (HasBufferStorage) {
if (InstanceVBO != 0) {
GL.DeleteBuffer(InstanceVBO);
}
GL.GenBuffers(1, out uint instanceVbo);
InstanceVBO = instanceVbo;
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
GL.BufferStorage(GLEnum.ArrayBuffer, (nuint)(_instanceBufferCapacity * _instanceBufferStride), (void*)0, flags);
InstanceVBOPtr = GL.MapBufferRange(GLEnum.ArrayBuffer, 0, (nuint)(_instanceBufferCapacity * _instanceBufferStride), MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
} else {
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
GL.BufferData(GLEnum.ArrayBuffer, (nuint)(_instanceBufferCapacity * _instanceBufferStride),
(void*)null, GLEnum.DynamicDraw);
InstanceVBOPtr = null;
}
GpuMemoryTracker.TrackAllocation(_instanceBufferCapacity * _instanceBufferStride);
}
public void UpdateInstanceBuffer<T>(List<T> data) where T : unmanaged {
EnsureInstanceBufferCapacity(data.Count, Marshal.SizeOf<T>(), true);
var span = CollectionsMarshal.AsSpan(data);
if (InstanceVBOPtr != null) {
var destSpan = new Span<T>(InstanceVBOPtr, data.Count);
span.CopyTo(destSpan);
} else {
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
fixed (T* ptr = span) {
GL.BufferSubData(GLEnum.ArrayBuffer, 0, (nuint)(data.Count * Marshal.SizeOf<T>()), ptr);
}
}
}
public void UpdateInstanceBuffer<T>(Span<T> data) where T : unmanaged {
EnsureInstanceBufferCapacity(data.Length, Marshal.SizeOf<T>(), true);
if (InstanceVBOPtr != null) {
var destSpan = new Span<T>(InstanceVBOPtr, data.Length);
data.CopyTo(destSpan);
} else {
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
fixed (T* ptr = data) {
GL.BufferSubData(GLEnum.ArrayBuffer, 0, (nuint)(data.Length * Marshal.SizeOf<T>()), ptr);
}
}
}
/// <inheritdoc />
public override void Clear(ColorVec color, ClearFlags flags, float depth, int stencil) {
GL.ClearColor(color.R, color.G, color.B, color.A);
GLHelpers.CheckErrors(GL);
GL.Clear((uint)Convert(flags));
GLHelpers.CheckErrors(GL);
}
/// <inheritdoc />
public override IIndexBuffer CreateIndexBuffer(int size,
Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
return new ManagedGLIndexBuffer(this, usage, size);
}
/// <inheritdoc />
public override IVertexBuffer CreateVertexBuffer(int size,
Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
return new ManagedGLVertexBuffer(this, usage, size);
}
/// <inheritdoc />
public override IVertexArray CreateArrayBuffer(IVertexBuffer vertexBuffer, VertexFormat format) {
return new ManagedGLVertexArray(this, vertexBuffer, format);
}
/// <inheritdoc />
public override void DrawElements(Chorizite.Core.Render.Enums.PrimitiveType type, int numElements, int indiceOffset = 0) {
GL.DrawElements(Convert(type), (uint)numElements, GLEnum.UnsignedInt, (void*)(indiceOffset * sizeof(uint)));
GLHelpers.CheckErrors(GL);
}
public override IShader CreateShader(string name, string vertexCode, string fragmentCode) {
var key = $"{GL.GetHashCode()}_{name}_{vertexCode.GetHashCode()}_{fragmentCode.GetHashCode()}";
while (true) {
if (_shaderCache.TryGetValue(key, out var existing)) {
if (existing is SharedShader shared && shared.TryIncrement()) {
return existing;
}
}
var inner = new GLSLShader(this, name, vertexCode, fragmentCode, _log);
var newShader = new SharedShader(inner, () => _shaderCache.TryRemove(key, out _));
if (_shaderCache.TryAdd(key, newShader)) {
return newShader;
}
// Someone else added it first, dispose ours and try again
newShader.DisposeInternal();
}
}
/// <inheritdoc />
public override IShader CreateShader(string name, string shaderDirectory) {
var key = $"{GL.GetHashCode()}_{name}";
while (true) {
if (_shaderCache.TryGetValue(key, out var existing)) {
if (existing is SharedShader shared && shared.TryIncrement()) {
return existing;
}
}
var inner = new GLSLShader(this, name, shaderDirectory, _log);
var newShader = new SharedShader(inner, () => _shaderCache.TryRemove(key, out _));
if (_shaderCache.TryAdd(key, newShader)) {
return newShader;
}
// Someone else added it first, dispose ours and try again
newShader.DisposeInternal();
}
}
private static readonly ConcurrentDictionary<string, IShader> _shaderCache = new();
private class SharedShader : IShader, IDisposable {
private readonly IShader _shader;
private readonly Action _onDispose;
private int _refCount = 1;
public string Name => _shader.Name;
public uint ProgramId => _shader.ProgramId;
public SharedShader(IShader shader, Action onDispose) {
_shader = shader;
_onDispose = onDispose;
}
public bool TryIncrement() {
while (true) {
int current = _refCount;
if (current <= 0) return false;
if (Interlocked.CompareExchange(ref _refCount, current + 1, current) == current) {
return true;
}
}
}
public void Bind() => _shader.Bind();
public void Unbind() => _shader.Unbind();
public void Load(string vertexSource, string fragmentSource) => _shader.Load(vertexSource, fragmentSource);
public void SetUniform(string name, int value) => _shader.SetUniform(name, value);
public void SetUniform(string name, float value) => _shader.SetUniform(name, value);
public void SetUniform(string name, Vector2 value) => _shader.SetUniform(name, value);
public void SetUniform(string name, Vector3 value) => _shader.SetUniform(name, value);
public void SetUniform(string name, Vector4 value) => _shader.SetUniform(name, value);
public void SetUniform(string name, Matrix4x4 value) => _shader.SetUniform(name, value);
public void SetUniform(string name, float[] values) => _shader.SetUniform(name, values);
public void DisposeInternal() {
_refCount = 0;
(_shader as IDisposable)?.Dispose();
}
public void Dispose() {
if (Interlocked.Decrement(ref _refCount) == 0) {
(_shader as IDisposable)?.Dispose();
_onDispose();
}
}
}
/// <inheritdoc />
public override ITexture
CreateTextureInternal(TextureFormat format, int width, int height, byte[]? data = null) {
if (format != TextureFormat.RGBA8) {
throw new NotImplementedException($"Texture format {format} is not supported.");
}
return new ManagedGLTexture(this, data, width, height);
}
/// <summary>
/// Creates a texture with custom texture parameters.
/// </summary>
public ITexture CreateTextureInternal(TextureFormat format, int width, int height, byte[]? data, TextureParameters texParams) {
if (format != TextureFormat.RGBA8) {
throw new NotImplementedException($"Texture format {format} is not supported.");
}
return new ManagedGLTexture(this, data, width, height, texParams);
}
/// <inheritdoc />
public override ITexture? CreateTextureInternal(TextureFormat format, string filename) {
if (format != TextureFormat.RGBA8) {
throw new NotImplementedException($"Texture format {format} is not supported.");
}
return new ManagedGLTexture(this, filename);
}
/// <inheritdoc />
public override ITextureArray
CreateTextureArrayInternal(TextureFormat format, int width, int height, int size) {
return new ManagedGLTextureArray(this, format, width, height, size, _log);
}
/// <summary>
/// Creates a texture array with custom texture parameters.
/// </summary>
public ITextureArray CreateTextureArrayInternal(TextureFormat format, int width, int height, int size, TextureParameters texParams) {
return new ManagedGLTextureArray(this, format, width, height, size, _log, texParams);
}
/// <inheritdoc />
public override void BeginFrame() {
GL.Viewport(Viewport.X, Viewport.Y, (uint)Viewport.Width, (uint)Viewport.Height);
GLHelpers.CheckErrors(GL);
GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
GLHelpers.CheckErrors(GL);
}
/// <inheritdoc />
public override void EndFrame() {
}
/// <inheritdoc />
protected override void SetRenderStateInternal(RenderState state, bool enabled) {
switch (state) {
case RenderState.AlphaBlend:
if (enabled) GL.Enable(EnableCap.Blend);
else GL.Disable(EnableCap.Blend);
GLHelpers.CheckErrors(GL);
break;
case RenderState.DepthTest:
if (enabled) GL.Enable(EnableCap.DepthTest);
else GL.Disable(EnableCap.DepthTest);
GLHelpers.CheckErrors(GL);
break;
case RenderState.ScissorTest:
if (enabled) GL.Enable(EnableCap.ScissorTest);
else GL.Disable(EnableCap.ScissorTest);
GLHelpers.CheckErrors(GL);
break;
case RenderState.DepthWrite:
if (enabled) GL.DepthMask(true);
else GL.DepthMask(false);
GLHelpers.CheckErrors(GL);
break;
case RenderState.Fog:
break;
case RenderState.Lighting:
break;
}
}
/// <inheritdoc />
protected override void SetBlendFactorInternal(BlendFactor srcBlendFactor, BlendFactor dstBlendFactor) {
GL.BlendFunc(Convert(srcBlendFactor), Convert(dstBlendFactor));
GLHelpers.CheckErrors(GL);
}
protected override void SetScissorRectInternal(Rectangle scissor) {
var gtop = (int)Viewport.Height - scissor.Y - scissor.Height;
GL.Scissor(scissor.X, gtop, (uint)scissor.Width, (uint)scissor.Height);
GLHelpers.CheckErrors(GL);
}
protected override void SetViewportInternal(Rectangle viewport) {
GL.Viewport(viewport.X, viewport.Y, (uint)viewport.Width, (uint)viewport.Height);
GLHelpers.CheckErrors(GL);
}
protected override void SetPolygonModeInternal(Chorizite.Core.Render.Enums.PolygonMode polygonMode) {
GL.PolygonMode(GLEnum.FrontAndBack, Convert(polygonMode));
GLHelpers.CheckErrors(GL);
}
protected override void SetCullModeInternal(CullMode cullMode) {
switch (cullMode) {
case CullMode.None:
GL.Disable(EnableCap.CullFace);
break;
case CullMode.Front:
GL.Enable(EnableCap.CullFace);
GL.CullFace(GLEnum.Front);
break;
case CullMode.Back:
GL.Enable(EnableCap.CullFace);
GL.CullFace(GLEnum.Back);
break;
}
}
private GLEnum Convert(Chorizite.Core.Render.Enums.PolygonMode mode) {
switch (mode) {
case Chorizite.Core.Render.Enums.PolygonMode.Fill:
return GLEnum.Fill;
case Chorizite.Core.Render.Enums.PolygonMode.Line:
return GLEnum.Line;
case Chorizite.Core.Render.Enums.PolygonMode.Point:
return GLEnum.Point;
default:
return GLEnum.Fill;
}
}
private GLEnum Convert(ClearFlags flags) {
GLEnum mask = 0;
if ((flags & ClearFlags.Color) == ClearFlags.Color) mask |= GLEnum.ColorBufferBit;
if ((flags & ClearFlags.Depth) == ClearFlags.Depth) mask |= GLEnum.DepthBufferBit;
if ((flags & ClearFlags.Stencil) == ClearFlags.Stencil) mask |= GLEnum.StencilBufferBit;
return mask;
}
private GLEnum Convert(BlendFactor factor) {
switch (factor) {
case BlendFactor.One:
return GLEnum.One;
case BlendFactor.SrcAlpha:
return GLEnum.SrcAlpha;
case BlendFactor.OneMinusSrcAlpha:
return GLEnum.OneMinusSrcAlpha;
case BlendFactor.DstAlpha:
return GLEnum.DstAlpha;
case BlendFactor.OneMinusDstAlpha:
return GLEnum.OneMinusDstAlpha;
default:
return GLEnum.One;
}
}
private PrimitiveType Convert(Chorizite.Core.Render.Enums.PrimitiveType type) {
switch (type) {
case Chorizite.Core.Render.Enums.PrimitiveType.PointList:
return PrimitiveType.Points;
case Chorizite.Core.Render.Enums.PrimitiveType.LineList:
return PrimitiveType.Lines;
case Chorizite.Core.Render.Enums.PrimitiveType.LineStrip:
return PrimitiveType.LineStrip;
case Chorizite.Core.Render.Enums.PrimitiveType.TriangleList:
return PrimitiveType.Triangles;
case Chorizite.Core.Render.Enums.PrimitiveType.TriangleStrip:
return PrimitiveType.TriangleStrip;
default:
throw new NotImplementedException($"Primitive type {type} is not supported.");
}
}
/// <inheritdoc />
public override IFramebuffer CreateFramebuffer(ITexture texture, int width, int height,
bool hasDepthStencil = true) {
if (texture == null) {
throw new ArgumentNullException(nameof(texture));
}
if (width <= 0 || height <= 0) {
throw new ArgumentException("Width and height must be positive.");
}
return new ManagedGLFramebuffer(this, texture, width, height, hasDepthStencil);
}
/// <inheritdoc />
public override void BindFramebuffer(IFramebuffer? framebuffer) {
uint fboId = framebuffer != null ? (uint)framebuffer.NativeHandle.ToInt32() : 0;
GL.BindFramebuffer(FramebufferTarget.Framebuffer, fboId);
}
/// <inheritdoc />
public override void Dispose() {
var instanceVBO = InstanceVBO;
var instanceBufferCapacity = _instanceBufferCapacity;
var instanceBufferStride = _instanceBufferStride;
var wrapSampler = WrapSampler;
var clampSampler = ClampSampler;
var sharedQuadVbo = SharedQuadVBO;
var sharedDebugInstanceVbo = SharedDebugInstanceVBO;
var sharedDebugVao = SharedDebugVAO;
QueueGLAction(gl => {
if (sharedQuadVbo != 0) gl.DeleteBuffer(sharedQuadVbo);
if (sharedDebugInstanceVbo != 0) gl.DeleteBuffer(sharedDebugInstanceVbo);
if (sharedDebugVao != 0) gl.DeleteVertexArray(sharedDebugVao);
if (instanceVBO != 0) {
gl.DeleteBuffer(instanceVBO);
if (instanceBufferCapacity > 0) {
GpuMemoryTracker.TrackDeallocation(instanceBufferCapacity * instanceBufferStride);
}
}
if (wrapSampler != 0) {
gl.DeleteSampler(wrapSampler);
}
if (clampSampler != 0) {
gl.DeleteSampler(clampSampler);
}
});
InstanceVBO = 0;
InstanceVBOPtr = null;
WrapSampler = 0;
ClampSampler = 0;
_sceneDataBuffer?.Dispose();
_sceneDataBuffer = null;
ParticleBatcher?.Dispose();
}
public override IUniformBuffer CreateUniformBuffer(BufferUsage usage, int size) {
return (IUniformBuffer)new ManagedGLUniformBuffer(this, usage, size);
}
}
}

View file

@ -0,0 +1,24 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// Global scene data for Uniform Buffer Object (UBO)
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 16)]
public struct SceneData {
public Matrix4x4 View; // 64 bytes
public Matrix4x4 Projection; // 64 bytes
public Matrix4x4 ViewProjection; // 64 bytes
public Vector3 CameraPosition; // 12 bytes
private float _padding1; // 4 bytes
public Vector3 LightDirection; // 12 bytes
private float _padding2; // 4 bytes
public Vector3 SunlightColor; // 12 bytes
private float _padding3; // 4 bytes
public Vector3 AmbientColor; // 12 bytes
public float SpecularPower; // 4 bytes
public Vector2 ViewportSize; // 8 bytes
private Vector2 _padding4; // 8 bytes
}
}

View file

@ -0,0 +1,55 @@
using Chorizite.Core.Render.Enums;
using Silk.NET.OpenGL;
using System;
namespace AcDream.App.Rendering.Wb {
public static class TextureFormatExtensions {
public static SizedInternalFormat ToGL(this Chorizite.Core.Render.Enums.TextureFormat format) {
return format switch {
TextureFormat.RGBA8 => SizedInternalFormat.Rgba8,
TextureFormat.RGB8 => SizedInternalFormat.Rgb8,
TextureFormat.A8 => SizedInternalFormat.R8,
TextureFormat.Rgba32f => SizedInternalFormat.Rgba32f,
TextureFormat.DXT1 => SizedInternalFormat.CompressedRgbaS3TCDxt1Ext,
TextureFormat.DXT3 => SizedInternalFormat.CompressedRgbaS3TCDxt3Ext,
TextureFormat.DXT5 => SizedInternalFormat.CompressedRgbaS3TCDxt5Ext,
_ => throw new NotSupportedException($"Texture format {format} is not supported."),
};
}
public static InternalFormat ToCompressedGL(this Chorizite.Core.Render.Enums.TextureFormat format) {
return format switch {
TextureFormat.DXT1 => InternalFormat.CompressedRgbaS3TCDxt1Ext,
TextureFormat.DXT3 => InternalFormat.CompressedRgbaS3TCDxt3Ext,
TextureFormat.DXT5 => InternalFormat.CompressedRgbaS3TCDxt5Ext,
_ => throw new NotSupportedException($"Texture format {format} does not support compression."),
};
}
public static PixelFormat ToPixelFormat(this Chorizite.Core.Render.Enums.TextureFormat format) {
return format switch {
Chorizite.Core.Render.Enums.TextureFormat.RGBA8 => PixelFormat.Rgba,
Chorizite.Core.Render.Enums.TextureFormat.RGB8 => PixelFormat.Rgb,
Chorizite.Core.Render.Enums.TextureFormat.A8 => PixelFormat.Red,
Chorizite.Core.Render.Enums.TextureFormat.Rgba32f => PixelFormat.Rgba,
_ => throw new NotSupportedException($"Texture format {format} is not supported."),
};
}
public static PixelType ToPixelType(this Chorizite.Core.Render.Enums.TextureFormat format) {
return format switch {
TextureFormat.RGBA8 => PixelType.UnsignedByte,
TextureFormat.RGB8 => PixelType.UnsignedByte,
TextureFormat.A8 => PixelType.UnsignedByte,
TextureFormat.Rgba32f => PixelType.Float,
_ => throw new NotSupportedException($"Texture format {format} is not supported."),
};
}
public static bool IsCompressed(this Chorizite.Core.Render.Enums.TextureFormat format) {
return format == Chorizite.Core.Render.Enums.TextureFormat.DXT1 ||
format == Chorizite.Core.Render.Enums.TextureFormat.DXT3 ||
format == Chorizite.Core.Render.Enums.TextureFormat.DXT5;
}
}
}

View file

@ -0,0 +1,35 @@
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// Configurable OpenGL texture parameters for wrap mode, filtering, mipmaps, and anisotropic filtering.
/// </summary>
public struct TextureParameters {
public TextureWrapMode WrapS;
public TextureWrapMode WrapT;
public TextureMinFilter MinFilter;
public TextureMagFilter MagFilter;
public bool EnableMipmaps;
public bool EnableAnisotropicFiltering;
/// <summary>Standard tiling textures — Repeat + trilinear + aniso.</summary>
public static readonly TextureParameters Default = new() {
WrapS = TextureWrapMode.Repeat,
WrapT = TextureWrapMode.Repeat,
MinFilter = TextureMinFilter.LinearMipmapLinear,
MagFilter = TextureMagFilter.Linear,
EnableMipmaps = true,
EnableAnisotropicFiltering = true,
};
/// <summary>Non-tiling textures (alpha maps, fonts, UI, object atlases) — ClampToEdge + trilinear + aniso.</summary>
public static readonly TextureParameters ClampToEdge = new() {
WrapS = TextureWrapMode.ClampToEdge,
WrapT = TextureWrapMode.ClampToEdge,
MinFilter = TextureMinFilter.LinearMipmapLinear,
MagFilter = TextureMagFilter.Linear,
EnableMipmaps = true,
EnableAnisotropicFiltering = true,
};
}
}

View file

@ -4,14 +4,12 @@ using System.Linq;
using System.Threading.Tasks;
using AcDream.Core.Meshing;
using AcDream.Core.Rendering;
using Chorizite.OpenGLSDLBackend;
using Chorizite.OpenGLSDLBackend.Lib;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Silk.NET.OpenGL;
using WorldBuilder.Shared.Models;
using WorldBuilder.Shared.Services;
namespace AcDream.App.Rendering.Wb;
@ -30,7 +28,9 @@ namespace AcDream.App.Rendering.Wb;
/// </summary>
public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
{
private readonly OpenGLGraphicsDevice? _graphicsDevice;
// T3 interim: ObjectMeshManager (T4-to-be-extracted) still expects the WB-original type.
// Will become AcDream.App.Rendering.Wb.OpenGLGraphicsDevice when T4 is done.
private readonly Chorizite.OpenGLSDLBackend.OpenGLGraphicsDevice? _graphicsDevice;
private readonly DefaultDatReaderWriter? _wbDats;
private readonly ObjectMeshManager? _meshManager;
private readonly DatCollection? _dats;
@ -75,7 +75,9 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
ArgumentNullException.ThrowIfNull(logger);
_dats = dats;
_graphicsDevice = new OpenGLGraphicsDevice(gl, logger, new DebugRenderSettings());
// T3 interim: construct the WB-original device for ObjectMeshManager compatibility.
// Will swap to AcDream.App.Rendering.Wb.OpenGLGraphicsDevice when T4 extracts ObjectMeshManager.
_graphicsDevice = new Chorizite.OpenGLSDLBackend.OpenGLGraphicsDevice(gl, logger, new WorldBuilder.Shared.Models.DebugRenderSettings());
_wbDats = new DefaultDatReaderWriter(datDir);
// Phase 2 diagnostic — replace NullLogger with a Console-backed
// logger so WB's internal catch block at ObjectMeshManager.cs:589

View file

@ -1,3 +1,4 @@
using Chorizite.Core.Render.Enums;
using DatReaderWriter.DBObjs;
namespace AcDream.Core.Rendering.Wb {
@ -157,5 +158,16 @@ namespace AcDream.Core.Rendering.Wb {
int b = color565 & 31;
return new byte[] { (byte)(r * 255 / 31), (byte)(g * 255 / 63), (byte)(b * 255 / 31), 255 };
}
/// <summary>
/// Gets the expected compressed data size for a texture layer.
/// Extracted from Chorizite.OpenGLSDLBackend.Lib.TextureHelpers (MIT).
/// </summary>
public static int GetCompressedLayerSize(int width, int height, TextureFormat format) {
int blocksWide = Math.Max(1, (width + 3) / 4);
int blocksHigh = Math.Max(1, (height + 3) / 4);
int blockSize = format == TextureFormat.DXT1 ? 8 : 16;
return blocksWide * blocksHigh * blockSize;
}
}
}