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 ParticleBatcher ParticleBatcher { get; internal 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(); // ParticleBatcher is constructed post-ctor by WbMeshAdapter (WbMeshAdapter.cs:78) // after the adapter has wired up all dependencies. The null! here is overridden // immediately after construction; it is not observable as null at runtime. 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); } } }