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; + } } }