diff --git a/src/AcDream.App/AcDream.App.csproj b/src/AcDream.App/AcDream.App.csproj
index c5378e2..1f35dbb 100644
--- a/src/AcDream.App/AcDream.App.csproj
+++ b/src/AcDream.App/AcDream.App.csproj
@@ -14,6 +14,10 @@
+
+
diff --git a/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs b/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs
new file mode 100644
index 0000000..1978881
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs
@@ -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 {
+ ///
+ /// Converts a BufferUsage to a GL BufferUsageARB
+ ///
+ ///
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs b/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs
new file mode 100644
index 0000000..66bdd06
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs
@@ -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);
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/EmbeddedResourceReader.cs b/src/AcDream.App/Rendering/Wb/EmbeddedResourceReader.cs
new file mode 100644
index 0000000..ebf55b4
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/EmbeddedResourceReader.cs
@@ -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();
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/GLHelpers.cs b/src/AcDream.App/Rendering/Wb/GLHelpers.cs
new file mode 100644
index 0000000..564d5ce
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/GLHelpers.cs
@@ -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
+ ///
+ /// Checks for OpenGL errors and provides context-specific information
+ ///
+ 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
+
+
+ ///
+ /// Gets detailed information about the current texture state for debugging
+ ///
+ 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();
+ }
+
+ ///
+ /// Validates texture completeness for mipmapping
+ ///
+ 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;
+ }
+
+ ///
+ /// Logs current OpenGL state for debugging
+ ///
+ 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());
+ }
+
+ ///
+ /// 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.
+ ///
+ 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);
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/GLSLShader.cs b/src/AcDream.App/Rendering/Wb/GLSLShader.cs
new file mode 100644
index 0000000..85f569b
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/GLSLShader.cs
@@ -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 _uniformLocations = [];
+ private Dictionary _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);
+ });
+ }
+ }
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/GLStateScope.cs b/src/AcDream.App/Rendering/Wb/GLStateScope.cs
new file mode 100644
index 0000000..49abea1
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/GLStateScope.cs
@@ -0,0 +1,230 @@
+using Silk.NET.OpenGL;
+using System;
+
+namespace AcDream.App.Rendering.Wb {
+ ///
+ /// A RAII scope for saving and restoring OpenGL state.
+ ///
+ 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;
+
+ ///
+ /// Captures the current OpenGL state.
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Restores only the scissor state from the scope.
+ ///
+ 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]);
+ }
+
+ ///
+ /// Restores the captured OpenGL state.
+ ///
+ 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;
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs b/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs
new file mode 100644
index 0000000..2b4862c
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs
@@ -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 {
+ ///
+ /// Resource types for GPU memory tracking.
+ ///
+ public enum GpuResourceType {
+ Texture,
+ Buffer,
+ Shader,
+ VAO,
+ FBO,
+ RBO,
+ Other
+ }
+
+ ///
+ /// Details about a GPU resource type.
+ ///
+ public record GpuResourceDetails(GpuResourceType Type, int Count, long Bytes);
+
+ ///
+ /// Details about a specific named buffer.
+ ///
+ public record NamedBufferDetails(string Name, long CapacityBytes, long UsedBytes);
+
+ ///
+ /// Tracks manual VRAM allocations for buffers and textures.
+ ///
+ public static class GpuMemoryTracker {
+ private static long _allocatedBytes;
+ private static readonly long[] _allocatedBytesByType = new long[Enum.GetValues().Length];
+ private static readonly int[] _resourceCountsByType = new int[Enum.GetValues().Length];
+ private static readonly ConcurrentDictionary _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 GetNamedBufferDetails() => _namedBuffers.Values.OrderBy(b => b.Name);
+
+ public static IEnumerable GetDetails() {
+ var types = Enum.GetValues();
+ foreach (var type in types) {
+ yield return new GpuResourceDetails(
+ type,
+ _resourceCountsByType[(int)type],
+ Interlocked.Read(ref _allocatedBytesByType[(int)type])
+ );
+ }
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs
new file mode 100644
index 0000000..7a3b190
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs
@@ -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 {
+ ///
+ /// Implementation of a framebuffer for OpenGL ES 3.0 using Silk.NET.
+ ///
+ 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);
+ }
+ });
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs
new file mode 100644
index 0000000..4889a0f
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs
@@ -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 {
+ ///
+ /// OpenGL index buffer
+ ///
+ public unsafe class ManagedGLIndexBuffer : IIndexBuffer {
+ private uint bufferId;
+ private readonly OpenGLGraphicsDevice _device;
+ private void* _mappedPtr;
+ private GL GL => _device.GL;
+
+ ///
+ public int Size { get; private set; }
+
+ ///
+ public BufferUsage Usage { get; private set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Buffer usage
+ /// The size of the buffer, in bytes
+ 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);
+ }
+
+ ///
+ public void SetData(uint[] data) {
+ SetData(data.AsSpan());
+ }
+
+ ///
+ public unsafe void SetData(Span 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 mappedSpan = new Span(_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);
+ }
+ }
+
+
+ ///
+ public unsafe void SetSubData(Span 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 mappedSpan = new Span((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);
+ }
+ }
+ }
+
+
+ ///
+ 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);
+ }
+ }
+
+ ///
+ public void Bind() {
+ BaseObjectRenderManager.CurrentIBO = 0;
+ GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
+ GLHelpers.CheckErrors(GL);
+ }
+
+ ///
+ 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;
+ }
+ });
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs
new file mode 100644
index 0000000..27d83cd
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs
@@ -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;
+
+ ///
+ public IntPtr NativePtr => (IntPtr)_texture;
+
+ ///
+ public int Width { get; private set; }
+
+ ///
+ 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; }
+
+ ///
+ 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;
+ }
+
+ ///
+ 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();
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
new file mode 100644
index 0000000..11e20d9
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
@@ -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 _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 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;
+ }
+ });
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs
new file mode 100644
index 0000000..da93db1
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs
@@ -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 {
+ ///
+ /// OpenGL uniform buffer
+ ///
+ public unsafe class ManagedGLUniformBuffer : IUniformBuffer {
+ private uint bufferId;
+ private readonly OpenGLGraphicsDevice _device;
+ private GL GL => _device.GL;
+
+ ///
+ public int Size { get; private set; }
+
+ ///
+ public BufferUsage Usage { get; private set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Graphics device
+ /// Buffer usage
+ /// The size of the buffer, in bytes
+ 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);
+ }
+
+ ///
+ public unsafe void SetData(T[] data) where T : unmanaged {
+ SetData(data.AsSpan());
+ }
+
+ ///
+ public unsafe void SetData(Span data) where T : unmanaged {
+ uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf();
+
+ // 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);
+ }
+ }
+
+ ///
+ public unsafe void SetSubData(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : unmanaged {
+ SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
+ }
+
+ ///
+ public unsafe void SetSubData(Span 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();
+
+ // 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);
+ }
+ }
+
+ ///
+ /// Sets a single piece of data in the buffer.
+ ///
+ public unsafe void SetData(ref T data) where T : unmanaged {
+ fixed (T* pData = &data) {
+ SetData(new Span(pData, 1));
+ }
+ }
+
+ ///
+ /// Binds the buffer to the specified binding point.
+ ///
+ /// The binding point to bind to
+ public void Bind(uint bindingPoint) {
+ GL.BindBufferBase(GLEnum.UniformBuffer, bindingPoint, bufferId);
+ GLHelpers.CheckErrors(GL);
+ }
+
+ ///
+ public void Bind() {
+ GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
+ GLHelpers.CheckErrors(GL);
+ }
+
+ ///
+ 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;
+ }
+ });
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs
new file mode 100644
index 0000000..583e82d
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs
@@ -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);
+ });
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs
new file mode 100644
index 0000000..d9a3752
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs
@@ -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 {
+ ///
+ /// OpenGL vertex buffer
+ ///
+ public unsafe class ManagedGLVertexBuffer : IVertexBuffer {
+ private uint bufferId;
+ private readonly OpenGLGraphicsDevice _device;
+ private void* _mappedPtr;
+ private GL GL => _device.GL;
+
+ ///
+ public int Size { get; private set; }
+
+ ///
+ public BufferUsage Usage { get; private set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Buffer usage
+ /// The size of the buffer, in bytes
+ 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);
+ }
+
+ ///
+ public unsafe void SetData(T[] data) where T : IVertex {
+ SetData(data.AsSpan());
+ }
+
+ ///
+ public unsafe void SetData(Span data) where T : IVertex {
+ uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf();
+
+ // 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 mappedSpan = new Span(_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 mappedSpan = new Span(mappedPtr, data.Length);
+ data.CopyTo(mappedSpan);
+ }
+ finally {
+ // Unmap the buffer
+ GL.UnmapBuffer(GLEnum.ArrayBuffer);
+ GLHelpers.CheckErrors(GL);
+ }
+ }
+ }
+
+ public unsafe void SetSubData(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : IVertex {
+ SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
+ }
+
+ ///
+ public unsafe void SetSubData(Span 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();
+
+ // 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 mappedSpan = new Span((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 mappedSpan = new Span(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;
+ }
+ });
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs b/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs
new file mode 100644
index 0000000..3e42bd9
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs
@@ -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 {
+ ///
+ /// OpenGL graphics device
+ ///
+ public unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice {
+ private readonly ILogger _log;
+ private readonly DebugRenderSettings _renderSettings;
+
+ public GL GL { get; }
+ public DebugRenderSettings RenderSettings => _renderSettings;
+
+ private readonly ConcurrentQueue> _glThreadQueue = new();
+
+ public void QueueGLAction(Action 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!;
+
+ /// OpenGL sampler object with TextureWrapMode.Repeat (for meshes with wrapping UVs).
+ public uint WrapSampler { get; private set; }
+ /// OpenGL sampler object with TextureWrapMode.ClampToEdge (for meshes without wrapping UVs).
+ public uint ClampSampler { get; private set; }
+
+ private ManagedGLUniformBuffer? _sceneDataBuffer;
+ /// Shared SceneData UBO.
+ 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;
+
+ ///
+ 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());
+
+ 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() - 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(List data) where T : unmanaged {
+ EnsureInstanceBufferCapacity(data.Count, Marshal.SizeOf(), true);
+ var span = CollectionsMarshal.AsSpan(data);
+ if (InstanceVBOPtr != null) {
+ var destSpan = new Span(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()), ptr);
+ }
+ }
+ }
+
+ public void UpdateInstanceBuffer(Span data) where T : unmanaged {
+ EnsureInstanceBufferCapacity(data.Length, Marshal.SizeOf(), true);
+ if (InstanceVBOPtr != null) {
+ var destSpan = new Span(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()), ptr);
+ }
+ }
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ public override IIndexBuffer CreateIndexBuffer(int size,
+ Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
+ return new ManagedGLIndexBuffer(this, usage, size);
+ }
+
+ ///
+ public override IVertexBuffer CreateVertexBuffer(int size,
+ Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
+ return new ManagedGLVertexBuffer(this, usage, size);
+ }
+
+ ///
+ public override IVertexArray CreateArrayBuffer(IVertexBuffer vertexBuffer, VertexFormat format) {
+ return new ManagedGLVertexArray(this, vertexBuffer, format);
+ }
+
+ ///
+ 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();
+ }
+ }
+
+ ///
+ 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 _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();
+ }
+ }
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ /// Creates a texture with custom texture parameters.
+ ///
+ 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);
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ public override ITextureArray
+ CreateTextureArrayInternal(TextureFormat format, int width, int height, int size) {
+ return new ManagedGLTextureArray(this, format, width, height, size, _log);
+ }
+
+ ///
+ /// Creates a texture array with custom texture parameters.
+ ///
+ public ITextureArray CreateTextureArrayInternal(TextureFormat format, int width, int height, int size, TextureParameters texParams) {
+ return new ManagedGLTextureArray(this, format, width, height, size, _log, texParams);
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ public override void EndFrame() {
+ }
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ 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.");
+ }
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ public override void BindFramebuffer(IFramebuffer? framebuffer) {
+ uint fboId = framebuffer != null ? (uint)framebuffer.NativeHandle.ToInt32() : 0;
+ GL.BindFramebuffer(FramebufferTarget.Framebuffer, fboId);
+ }
+
+ ///
+ 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);
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/SceneData.cs b/src/AcDream.App/Rendering/Wb/SceneData.cs
new file mode 100644
index 0000000..2923404
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/SceneData.cs
@@ -0,0 +1,24 @@
+using System.Numerics;
+using System.Runtime.InteropServices;
+
+namespace AcDream.App.Rendering.Wb {
+ ///
+ /// Global scene data for Uniform Buffer Object (UBO)
+ ///
+ [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
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs b/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs
new file mode 100644
index 0000000..d3b00bb
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/TextureParameters.cs b/src/AcDream.App/Rendering/Wb/TextureParameters.cs
new file mode 100644
index 0000000..30a7be1
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/TextureParameters.cs
@@ -0,0 +1,35 @@
+using Silk.NET.OpenGL;
+
+namespace AcDream.App.Rendering.Wb {
+ ///
+ /// Configurable OpenGL texture parameters for wrap mode, filtering, mipmaps, and anisotropic filtering.
+ ///
+ public struct TextureParameters {
+ public TextureWrapMode WrapS;
+ public TextureWrapMode WrapT;
+ public TextureMinFilter MinFilter;
+ public TextureMagFilter MagFilter;
+ public bool EnableMipmaps;
+ public bool EnableAnisotropicFiltering;
+
+ /// Standard tiling textures — Repeat + trilinear + aniso.
+ public static readonly TextureParameters Default = new() {
+ WrapS = TextureWrapMode.Repeat,
+ WrapT = TextureWrapMode.Repeat,
+ MinFilter = TextureMinFilter.LinearMipmapLinear,
+ MagFilter = TextureMagFilter.Linear,
+ EnableMipmaps = true,
+ EnableAnisotropicFiltering = true,
+ };
+
+ /// Non-tiling textures (alpha maps, fonts, UI, object atlases) — ClampToEdge + trilinear + aniso.
+ public static readonly TextureParameters ClampToEdge = new() {
+ WrapS = TextureWrapMode.ClampToEdge,
+ WrapT = TextureWrapMode.ClampToEdge,
+ MinFilter = TextureMinFilter.LinearMipmapLinear,
+ MagFilter = TextureMagFilter.Linear,
+ EnableMipmaps = true,
+ EnableAnisotropicFiltering = true,
+ };
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
index 2729747..b79354d 100644
--- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
+++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
@@ -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;
///
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
diff --git a/src/AcDream.Core/Rendering/Wb/TextureHelpers.cs b/src/AcDream.Core/Rendering/Wb/TextureHelpers.cs
index 38a347f..54b757c 100644
--- a/src/AcDream.Core/Rendering/Wb/TextureHelpers.cs
+++ b/src/AcDream.Core/Rendering/Wb/TextureHelpers.cs
@@ -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 };
}
+
+ ///
+ /// Gets the expected compressed data size for a texture layer.
+ /// Extracted from Chorizite.OpenGLSDLBackend.Lib.TextureHelpers (MIT).
+ ///
+ 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;
+ }
}
}