using AcDream.App.Rendering.Wb; using Silk.NET.OpenGL; namespace AcDream.App.Rendering.Gpu.Gl; /// /// Records one pass's draw work. Binding calls translate almost mechanically /// to GL (a storage/uniform binding is glBindBufferRange, an indexed /// draw is glDrawElementsInstancedBaseVertexBaseInstance, and so on); /// the two pieces of real logic are the render-state diff applied on /// / the dynamic setters, and the "flush dirty /// ring + texture-table bytes immediately before every draw" discipline /// described on . /// internal sealed class GlGpuPassEncoder : IGpuPassEncoder { private readonly GlGpuDevice _device; private readonly GlGpuFrame _frame; private readonly GL _gl; private readonly IGlAmbientStateApi _ambientApi; private readonly GlAmbientCapabilityState _ambientOnEntry; private bool _closed; private GlGpuPipeline? _currentPipeline; private GpuIndexType _currentIndexType = GpuIndexType.UInt16; private uint _currentIndexBufferBaseOffset; private GpuPushConstants? _currentPushConstants; internal GlGpuPassEncoder(GlGpuDevice device, GlGpuFrame frame, GpuPassDescription pass) { _device = device; _frame = frame; _gl = device.Gl; Pass = pass; // Campaign V slice V4a (2026-07-27 revert postmortem, plan §7.1 rule 1): // capture every ambient capability a bound pipeline can change, so // Dispose can put it back. Every acdream renderer is still raw GL // until V4c/V4d, so each one assumes whatever capability state the // PREVIOUS renderer left behind is still there — GL_MULTISAMPLE and // GL_SAMPLE_ALPHA_TO_COVERAGE in particular are set once per frame by // quality settings and never re-asserted per draw. The first V4a // attempt bound a pipeline that changed this state and never restored // it, so the world drew without multisampling from the first UI frame // on. Capturing here and restoring on Dispose keeps the GL backend's // behaviour-preserving property true at this seam. Deleted at V4h // once nothing raw-GL remains. _ambientApi = new SilkGlAmbientStateApi(_gl); _ambientOnEntry = GlAmbientCapabilityState.Capture(_ambientApi); // Campaign V slice V6d. GL_MULTISAMPLE is the one piece of pass state // with no representation in GpuPipelineDescription, and the pass's own // SampleCount is the contract's answer for it: a single-sampled pass // does not multisample. Until now the retained UI asserted that with a // raw glDisable of its own — exactly the kind of state a // backend-neutral renderer cannot own. Quality settings enable // GL_MULTISAMPLE once per frame for the world, and if it leaks into the // UI pass every glyph's soft alpha edge becomes dithered coverage // instead of a clean alpha blend (the "fuzzy text" artifact). The // ambient capture above puts it back on Dispose, so the raw-GL world // renderers that follow are unaffected. _ambientApi.SetCapability(EnableCap.Multisample, pass.SampleCount > 1); } public GpuPassDescription Pass { get; } public void BindPipeline(IGpuPipeline pipeline) { ArgumentNullException.ThrowIfNull(pipeline); ThrowIfClosed(); var p = (GlGpuPipeline)pipeline; _currentPipeline = p; GpuPipelineDescription description = p.Description; var desired = new GlRenderStateSnapshot( p.GlProgram, description.Blend, description.Depth.Test, description.Depth.Write, description.Depth.Compare, description.Cull, description.FrontFace, description.AlphaToCoverage, description.ColorWrite, description.StencilTest, description.Stencil); _device.ApplyRenderState(desired); _gl.BindVertexArray(p.GlVertexArray); GLHelpers.ThrowOnResourceError(_gl, $"bind pipeline '{description.Name}' VAO"); // Campaign V slice V6d: the device's texture table is bound with the // pipeline, the GL analogue of the Vulkan backend binding descriptor // set 2 on every draw. It has to happen here rather than once per frame // because every raw-GL world renderer binds its OWN private handle // table at this same binding before its own draws, with its own slot // numbering; an RHI shader that read that instead would sample a // plausible but entirely unrelated texture. Removed at V4h with the // per-renderer tables. _gl.BindBufferBase( GLEnum.ShaderStorageBuffer, GpuBindingModel.StorageTextureTable, _device.TextureTableGlName); GLHelpers.ThrowOnResourceError(_gl, $"bind pipeline '{description.Name}' texture table"); // Push constants "survive pipeline changes within a pass" per the // IGpuPassEncoder contract. GL uniforms are per-program state, so the // GL backend must explicitly re-apply the last value to the newly // bound program to honour that — Vulkan gets this for free from a // shared pipeline layout. if (_currentPushConstants is { } constants) _device.PushConstants.Apply(p.GlProgram, in constants); } public void BindStorageBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes) { ThrowIfClosed(); var b = RequireGlBuffer(buffer); _gl.BindBufferRange(GLEnum.ShaderStorageBuffer, binding, b.GlName, (nint)offsetBytes, sizeBytes); GLHelpers.ThrowOnResourceError(_gl, $"bind storage buffer '{buffer.Name}' at binding {binding}"); } public void BindUniformBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes) { ThrowIfClosed(); var b = RequireGlBuffer(buffer); _gl.BindBufferRange(GLEnum.UniformBuffer, binding, b.GlName, (nint)offsetBytes, sizeBytes); GLHelpers.ThrowOnResourceError(_gl, $"bind uniform buffer '{buffer.Name}' at binding {binding}"); } public unsafe void BindVertexBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes) { ThrowIfClosed(); if (_currentPipeline is not { } pipeline) throw new InvalidOperationException("BindPipeline must be called before BindVertexBuffer."); var b = RequireGlBuffer(buffer); _gl.BindBuffer(GLEnum.ArrayBuffer, b.GlName); GpuVertexLayout layout = pipeline.Description.VertexLayout; // Slice V6l: only the attributes this binding actually supplies. GL has // no binding indirection of its own — glVertexAttribPointer records the // currently bound ARRAY_BUFFER per attribute — so the binding index is // resolved here, by filtering, rather than by the driver. uint stride = layout.StrideOf(binding); foreach (GpuVertexAttribute attribute in layout.Attributes) { if (attribute.Binding != binding) continue; GlVertexAttributeShape shape = GlEnumMapping.VertexShapeOf(attribute.Format); nint attributeOffset = (nint)(offsetBytes + attribute.OffsetBytes); if (shape.Integer) { // An integer shader input (uvec4) must come through the I-form. // Supplying it via glVertexAttribPointer leaves the value undefined. _gl.VertexAttribIPointer( attribute.Location, shape.ComponentCount, (VertexAttribIType)shape.Type, stride, (void*)attributeOffset); } else { _gl.VertexAttribPointer( attribute.Location, shape.ComponentCount, shape.Type, shape.Normalized, stride, (void*)attributeOffset); } } GLHelpers.ThrowOnResourceError(_gl, $"bind vertex buffer '{buffer.Name}'"); _gl.BindBuffer(GLEnum.ArrayBuffer, 0); } public void BindIndexBuffer(IGpuBuffer buffer, uint offsetBytes, GpuIndexType indexType) { ThrowIfClosed(); var b = RequireGlBuffer(buffer); _currentIndexType = indexType; _currentIndexBufferBaseOffset = offsetBytes; _gl.BindBuffer(GLEnum.ElementArrayBuffer, b.GlName); GLHelpers.ThrowOnResourceError(_gl, $"bind index buffer '{buffer.Name}'"); } public void SetPushConstants(in GpuPushConstants constants) { ThrowIfClosed(); _currentPushConstants = constants; if (_currentPipeline is { } pipeline) _device.PushConstants.Apply(pipeline.GlProgram, in constants); } public void SetViewport(int x, int y, int width, int height) { ThrowIfClosed(); _gl.Viewport(x, y, (uint)width, (uint)height); } public void SetScissor(int x, int y, int width, int height) { ThrowIfClosed(); _gl.Enable(EnableCap.ScissorTest); _gl.Scissor(x, y, (uint)width, (uint)height); } public void SetCullMode(GpuCullMode cullMode) { ThrowIfClosed(); _device.ApplyRenderState(_device.CurrentRenderState with { Cull = cullMode }); } public void SetFrontFace(GpuFrontFace frontFace) { ThrowIfClosed(); _device.ApplyRenderState(_device.CurrentRenderState with { FrontFace = frontFace }); } public void SetDepthWrite(bool enabled) { ThrowIfClosed(); _device.ApplyRenderState(_device.CurrentRenderState with { DepthWrite = enabled }); } public void SetStencil(in GpuStencilState stencil) { ThrowIfClosed(); _device.ApplyRenderState(_device.CurrentRenderState with { Stencil = stencil }); } public unsafe void DrawIndexed(uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance) { ThrowIfClosed(); _device.FlushBeforeDraw(_frame.SlotIndex); int indexSize = GlEnumMapping.IndexSizeBytesOf(_currentIndexType); nint indexOffset = (nint)(_currentIndexBufferBaseOffset + firstIndex * (uint)indexSize); _gl.DrawElementsInstancedBaseVertexBaseInstance( GlEnumMapping.PrimitiveTypeOf(RequirePipeline().Description.Topology), indexCount, GlEnumMapping.DrawElementsTypeOf(_currentIndexType), (void*)indexOffset, instanceCount, vertexOffset, firstInstance); GLHelpers.ThrowOnResourceError(_gl, "DrawIndexed"); } public void Draw(uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance) { ThrowIfClosed(); _device.FlushBeforeDraw(_frame.SlotIndex); _gl.DrawArraysInstancedBaseInstance( (GLEnum)GlEnumMapping.PrimitiveTypeOf(RequirePipeline().Description.Topology), (int)firstVertex, vertexCount, instanceCount, firstInstance); GLHelpers.ThrowOnResourceError(_gl, "Draw"); } public unsafe void MultiDrawIndexedIndirect(IGpuBuffer commands, uint offsetBytes, uint drawCount, uint strideBytes) { ThrowIfClosed(); var indirect = RequireGlBuffer(commands); _device.FlushBeforeDraw(_frame.SlotIndex); _gl.BindBuffer(GLEnum.DrawIndirectBuffer, indirect.GlName); _gl.MultiDrawElementsIndirect( GlEnumMapping.PrimitiveTypeOf(RequirePipeline().Description.Topology), GlEnumMapping.DrawElementsTypeOf(_currentIndexType), (void*)(nint)offsetBytes, drawCount, strideBytes); GLHelpers.ThrowOnResourceError(_gl, "MultiDrawIndexedIndirect"); _gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0); } public IDisposable BeginTimerScope(string scopeName) => _device.TimerPool.BeginScope(scopeName); public void Dispose() { if (_closed) return; _closed = true; // GL has no store-op work to do here: GpuStoreOp.Resolve was already // rejected at BeginPass (V1 targets are single-sampled), and // Store/DontCare need no explicit action — the framebuffer's contents // simply persist until the next pass rebinds a target. // // Restore whatever capability state was ambient before this pass // opened (see the constructor's comment) so a still-raw-GL renderer // running immediately after this pass sees exactly what it would have // seen had this pass never bound a pipeline. _ambientOnEntry.Restore(_ambientApi); _frame.ClosePass(this); } private GlGpuPipeline RequirePipeline() => _currentPipeline ?? throw new InvalidOperationException("BindPipeline must be called before drawing."); private static GlGpuBuffer RequireGlBuffer(IGpuBuffer buffer) { ArgumentNullException.ThrowIfNull(buffer); if (buffer is not GlGpuBuffer glBuffer) throw new ArgumentException("The GL backend can only bind GL buffers.", nameof(buffer)); return glBuffer; } private void ThrowIfClosed() { if (_closed) throw new ObjectDisposedException(nameof(GlGpuPassEncoder)); } }