feat(render): Vulkan campaign V11 step 2 — delete the OpenGL backend
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1 already removed ImGui/Studio/DevTools. This step deletes the GL rendering backend itself: every Gpu/Gl/** implementation, the Wb ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/ BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache, RenderBootstrap, and RenderFrameGlStateController. GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/ OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone — there is nothing left to select between. The five world-draw dual-arm renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer, ParticleRenderer, SkyRenderer) and the composition roots (WorldRenderComposition, HostInputCameraComposition, LivePresentationComposition, FrameRootComposition) collapse to their RHI-only arm. GL-only diagnostic properties with a live external reader (DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op rather than disappearing, since the reader is out of this commit's scope. A few GL-flavored mechanisms turned out to be backend-neutral once isolated: GlConstructionCleanupLedger is renamed ResourceConstructionCleanupLedger (exception-chain walking has nothing to do with GL), and GlfwNativePlatformProbe moved out of the otherwise GL-only GraphicalCapabilityRecord.cs into GraphicalWindowBackendSelection.cs before the rest of that file was deleted. Test files with no surviving subject are deleted outright (GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests, PortalDepthShaderParityTests, TextureCacheBindlessTests, TextRendererFailureSafetyTests, ClipFrameUploadTests, every Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests); others get their dead GL-only members trimmed while their live assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now reads GpuBindingModel.StorageClipRegions, the same binding index under its new backend-neutral name; GpuResourceRetirementTransactionTests drops its OpenGLGraphicsDevice-subclassing test double and the two GL queue tests it existed for). EnvCellRendererTests' construction helper now builds a real ObjectMeshManager via VulkanMeshPipelineDevice instead of passing null through a null-forgiving operator, since the RHI constructor never tolerated a null mesh manager and the old GL constructor (which did) is gone. Deferred to the next two steps, deliberately not touched here: the Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl (WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale csproj comment (the package itself is still load-bearing — TextureFormat and friends are used well beyond the deleted ManagedGLUniformBuffer), and the CI/gate scripts. Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors. Tests: full-solution `dotnet test` green across every project (App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all others 100%); the 2 App.Tests names that flake under full-suite parallel execution (#250-family, documented pre-existing) pass in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b70b9832ff
commit
8a7a0837e1
121 changed files with 1243 additions and 19840 deletions
|
|
@ -1,313 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// The narrow slice of GL that <see cref="GlAmbientCapabilityState"/> reads and
|
||||
/// writes. It exists so the save/restore transaction can be exercised without a
|
||||
/// GL context: the property that matters is "every value this pass can change
|
||||
/// is put back, including when a draw throws", and that is a property of the
|
||||
/// bookkeeping, not of the driver.
|
||||
///
|
||||
/// Slice V6d generalized this from <c>ITextRenderGlStateApi</c>, which
|
||||
/// <c>TextRenderer</c> owned privately and which restored a strict subset of the
|
||||
/// same values. That renderer no longer touches GL at all, and the guarantee now
|
||||
/// lives in one place for every RHI pass.
|
||||
/// </summary>
|
||||
internal interface IGlAmbientStateApi
|
||||
{
|
||||
bool IsEnabled(EnableCap capability);
|
||||
|
||||
int GetInteger(GetPName parameter);
|
||||
|
||||
bool GetBoolean(GetPName parameter);
|
||||
|
||||
/// <summary>The four colour-mask channels, in RGBA order.</summary>
|
||||
bool[] GetColorMask();
|
||||
|
||||
void SetCapability(EnableCap capability, bool enabled);
|
||||
|
||||
void DepthMask(bool enabled);
|
||||
|
||||
void DepthFunc(DepthFunction function);
|
||||
|
||||
void BlendFuncSeparate(
|
||||
BlendingFactor sourceRgb,
|
||||
BlendingFactor destinationRgb,
|
||||
BlendingFactor sourceAlpha,
|
||||
BlendingFactor destinationAlpha);
|
||||
|
||||
void CullFace(TriangleFace face);
|
||||
|
||||
void FrontFace(FrontFaceDirection direction);
|
||||
|
||||
void StencilFunc(StencilFunction function, int reference, uint mask);
|
||||
|
||||
void StencilOp(StencilOp fail, StencilOp depthFail, StencilOp pass);
|
||||
|
||||
void StencilMask(uint mask);
|
||||
|
||||
void ColorMask(bool red, bool green, bool blue, bool alpha);
|
||||
|
||||
void UseProgram(uint program);
|
||||
|
||||
void BindVertexArray(uint vertexArray);
|
||||
|
||||
void BindBuffer(BufferTargetARB target, uint buffer);
|
||||
|
||||
void ActiveTexture(TextureUnit unit);
|
||||
|
||||
void BindTexture(TextureTarget target, uint texture);
|
||||
}
|
||||
|
||||
internal sealed class SilkGlAmbientStateApi : IGlAmbientStateApi
|
||||
{
|
||||
private readonly GL _gl;
|
||||
|
||||
public SilkGlAmbientStateApi(GL gl) => _gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
|
||||
public bool IsEnabled(EnableCap capability) => _gl.IsEnabled(capability);
|
||||
|
||||
public int GetInteger(GetPName parameter)
|
||||
{
|
||||
_gl.GetInteger(parameter, out int value);
|
||||
return value;
|
||||
}
|
||||
|
||||
public bool GetBoolean(GetPName parameter) => _gl.GetBoolean(parameter);
|
||||
|
||||
public unsafe bool[] GetColorMask()
|
||||
{
|
||||
var values = new bool[4];
|
||||
fixed (bool* first = values)
|
||||
_gl.GetBoolean(GetPName.ColorWritemask, first);
|
||||
return values;
|
||||
}
|
||||
|
||||
public void SetCapability(EnableCap capability, bool enabled)
|
||||
{
|
||||
if (enabled)
|
||||
_gl.Enable(capability);
|
||||
else
|
||||
_gl.Disable(capability);
|
||||
}
|
||||
|
||||
public void DepthMask(bool enabled) => _gl.DepthMask(enabled);
|
||||
|
||||
public void DepthFunc(DepthFunction function) => _gl.DepthFunc(function);
|
||||
|
||||
public void BlendFuncSeparate(
|
||||
BlendingFactor sourceRgb,
|
||||
BlendingFactor destinationRgb,
|
||||
BlendingFactor sourceAlpha,
|
||||
BlendingFactor destinationAlpha) =>
|
||||
_gl.BlendFuncSeparate(sourceRgb, destinationRgb, sourceAlpha, destinationAlpha);
|
||||
|
||||
public void CullFace(TriangleFace face) => _gl.CullFace(face);
|
||||
|
||||
public void FrontFace(FrontFaceDirection direction) => _gl.FrontFace(direction);
|
||||
|
||||
public void StencilFunc(StencilFunction function, int reference, uint mask) =>
|
||||
_gl.StencilFunc(function, reference, mask);
|
||||
|
||||
public void StencilOp(StencilOp fail, StencilOp depthFail, StencilOp pass) =>
|
||||
_gl.StencilOp(fail, depthFail, pass);
|
||||
|
||||
public void StencilMask(uint mask) => _gl.StencilMask(mask);
|
||||
|
||||
public void ColorMask(bool red, bool green, bool blue, bool alpha) =>
|
||||
_gl.ColorMask(red, green, blue, alpha);
|
||||
|
||||
public void UseProgram(uint program) => _gl.UseProgram(program);
|
||||
|
||||
public void BindVertexArray(uint vertexArray) => _gl.BindVertexArray(vertexArray);
|
||||
|
||||
public void BindBuffer(BufferTargetARB target, uint buffer) => _gl.BindBuffer(target, buffer);
|
||||
|
||||
public void ActiveTexture(TextureUnit unit) => _gl.ActiveTexture(unit);
|
||||
|
||||
public void BindTexture(TextureTarget target, uint texture) => _gl.BindTexture(target, texture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every ambient GL capability/binding a <see cref="GlGpuPipeline"/> bind (or a
|
||||
/// dynamic setter, or the pass's own sample count) can change, captured by raw
|
||||
/// query and restored by raw call.
|
||||
///
|
||||
/// Transitional for as long as raw-GL renderers coexist with RHI-ported ones:
|
||||
/// each raw-GL renderer assumes whatever state the previous one left behind is
|
||||
/// still there, so a pass that changes state and does not put it back is
|
||||
/// invisible until the world silhouette changes. That is precisely how the first
|
||||
/// V4a attempt lost multisampling (plan §7.1 rule 1). Deleted at V4h.
|
||||
/// </summary>
|
||||
internal readonly struct GlAmbientCapabilityState
|
||||
{
|
||||
private readonly int _program;
|
||||
private readonly int _vertexArray;
|
||||
private readonly int _arrayBuffer;
|
||||
private readonly int _activeTexture;
|
||||
private readonly int _texture0Binding2D;
|
||||
private readonly bool _depthTest;
|
||||
private readonly bool _depthWrite;
|
||||
private readonly int _depthFunc;
|
||||
private readonly bool _blend;
|
||||
private readonly int _blendSourceRgb;
|
||||
private readonly int _blendDestinationRgb;
|
||||
private readonly int _blendSourceAlpha;
|
||||
private readonly int _blendDestinationAlpha;
|
||||
private readonly bool _cullFace;
|
||||
private readonly int _cullFaceMode;
|
||||
private readonly int _frontFace;
|
||||
private readonly bool _alphaToCoverage;
|
||||
private readonly bool _multisample;
|
||||
// Slice V6l: the stencil dimension. #117's portal punch is the only pipeline
|
||||
// that enables it, and it draws in the middle of a frame whose other
|
||||
// renderers are still raw GL and assume the test is off.
|
||||
private readonly bool _stencilTest;
|
||||
private readonly int _stencilFunc;
|
||||
private readonly int _stencilReference;
|
||||
private readonly int _stencilValueMask;
|
||||
private readonly int _stencilWriteMask;
|
||||
private readonly int _stencilFail;
|
||||
private readonly int _stencilDepthFail;
|
||||
private readonly int _stencilPass;
|
||||
// Slice V6l: GpuPipelineDescription.ColorWrite has had no consumer until the
|
||||
// portal depth mask, which is colour-invisible by construction. A pass that
|
||||
// left the mask off would black out every raw-GL renderer that followed it,
|
||||
// which is §7.1 rule 1's exact failure mode wearing a different name.
|
||||
private readonly bool _colorMaskRed;
|
||||
private readonly bool _colorMaskGreen;
|
||||
private readonly bool _colorMaskBlue;
|
||||
private readonly bool _colorMaskAlpha;
|
||||
|
||||
private GlAmbientCapabilityState(
|
||||
int program, int vertexArray, int arrayBuffer, int activeTexture, int texture0Binding2D,
|
||||
bool depthTest, bool depthWrite, int depthFunc,
|
||||
bool blend, int blendSourceRgb, int blendDestinationRgb, int blendSourceAlpha, int blendDestinationAlpha,
|
||||
bool cullFace, int cullFaceMode, int frontFace,
|
||||
bool alphaToCoverage, bool multisample,
|
||||
bool stencilTest, int stencilFunc, int stencilReference, int stencilValueMask,
|
||||
int stencilWriteMask, int stencilFail, int stencilDepthFail, int stencilPass,
|
||||
bool colorMaskRed, bool colorMaskGreen, bool colorMaskBlue, bool colorMaskAlpha)
|
||||
{
|
||||
_program = program;
|
||||
_vertexArray = vertexArray;
|
||||
_arrayBuffer = arrayBuffer;
|
||||
_activeTexture = activeTexture;
|
||||
_texture0Binding2D = texture0Binding2D;
|
||||
_depthTest = depthTest;
|
||||
_depthWrite = depthWrite;
|
||||
_depthFunc = depthFunc;
|
||||
_blend = blend;
|
||||
_blendSourceRgb = blendSourceRgb;
|
||||
_blendDestinationRgb = blendDestinationRgb;
|
||||
_blendSourceAlpha = blendSourceAlpha;
|
||||
_blendDestinationAlpha = blendDestinationAlpha;
|
||||
_cullFace = cullFace;
|
||||
_cullFaceMode = cullFaceMode;
|
||||
_frontFace = frontFace;
|
||||
_alphaToCoverage = alphaToCoverage;
|
||||
_multisample = multisample;
|
||||
_stencilTest = stencilTest;
|
||||
_stencilFunc = stencilFunc;
|
||||
_stencilReference = stencilReference;
|
||||
_stencilValueMask = stencilValueMask;
|
||||
_stencilWriteMask = stencilWriteMask;
|
||||
_stencilFail = stencilFail;
|
||||
_stencilDepthFail = stencilDepthFail;
|
||||
_stencilPass = stencilPass;
|
||||
_colorMaskRed = colorMaskRed;
|
||||
_colorMaskGreen = colorMaskGreen;
|
||||
_colorMaskBlue = colorMaskBlue;
|
||||
_colorMaskAlpha = colorMaskAlpha;
|
||||
}
|
||||
|
||||
internal static GlAmbientCapabilityState Capture(IGlAmbientStateApi gl)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
int program = gl.GetInteger(GetPName.CurrentProgram);
|
||||
int vertexArray = gl.GetInteger(GetPName.VertexArrayBinding);
|
||||
int arrayBuffer = gl.GetInteger(GetPName.ArrayBufferBinding);
|
||||
int activeTexture = gl.GetInteger(GetPName.ActiveTexture);
|
||||
|
||||
int texture0Binding2D;
|
||||
try
|
||||
{
|
||||
gl.ActiveTexture(TextureUnit.Texture0);
|
||||
texture0Binding2D = gl.GetInteger(GetPName.TextureBinding2D);
|
||||
}
|
||||
finally
|
||||
{
|
||||
gl.ActiveTexture((TextureUnit)activeTexture);
|
||||
}
|
||||
|
||||
bool[] colorMask = gl.GetColorMask();
|
||||
return new GlAmbientCapabilityState(
|
||||
program, vertexArray, arrayBuffer, activeTexture, texture0Binding2D,
|
||||
gl.IsEnabled(EnableCap.DepthTest),
|
||||
gl.GetBoolean(GetPName.DepthWritemask),
|
||||
gl.GetInteger(GetPName.DepthFunc),
|
||||
gl.IsEnabled(EnableCap.Blend),
|
||||
gl.GetInteger(GetPName.BlendSrcRgb),
|
||||
gl.GetInteger(GetPName.BlendDstRgb),
|
||||
gl.GetInteger(GetPName.BlendSrcAlpha),
|
||||
gl.GetInteger(GetPName.BlendDstAlpha),
|
||||
gl.IsEnabled(EnableCap.CullFace),
|
||||
gl.GetInteger(GetPName.CullFaceMode),
|
||||
gl.GetInteger(GetPName.FrontFace),
|
||||
gl.IsEnabled(EnableCap.SampleAlphaToCoverage),
|
||||
gl.IsEnabled(EnableCap.Multisample),
|
||||
gl.IsEnabled(EnableCap.StencilTest),
|
||||
gl.GetInteger(GetPName.StencilFunc),
|
||||
gl.GetInteger(GetPName.StencilRef),
|
||||
gl.GetInteger(GetPName.StencilValueMask),
|
||||
gl.GetInteger(GetPName.StencilWritemask),
|
||||
gl.GetInteger(GetPName.StencilFail),
|
||||
gl.GetInteger(GetPName.StencilPassDepthFail),
|
||||
gl.GetInteger(GetPName.StencilPassDepthPass),
|
||||
colorMask[0], colorMask[1], colorMask[2], colorMask[3]);
|
||||
}
|
||||
|
||||
internal void Restore(IGlAmbientStateApi gl)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
gl.UseProgram((uint)_program);
|
||||
gl.BindVertexArray((uint)_vertexArray);
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBuffer);
|
||||
|
||||
gl.ActiveTexture(TextureUnit.Texture0);
|
||||
gl.BindTexture(TextureTarget.Texture2D, (uint)_texture0Binding2D);
|
||||
gl.ActiveTexture((TextureUnit)_activeTexture);
|
||||
|
||||
gl.SetCapability(EnableCap.DepthTest, _depthTest);
|
||||
gl.DepthMask(_depthWrite);
|
||||
gl.DepthFunc((DepthFunction)_depthFunc);
|
||||
|
||||
gl.SetCapability(EnableCap.Blend, _blend);
|
||||
gl.BlendFuncSeparate(
|
||||
(BlendingFactor)_blendSourceRgb,
|
||||
(BlendingFactor)_blendDestinationRgb,
|
||||
(BlendingFactor)_blendSourceAlpha,
|
||||
(BlendingFactor)_blendDestinationAlpha);
|
||||
|
||||
gl.SetCapability(EnableCap.CullFace, _cullFace);
|
||||
gl.CullFace((TriangleFace)_cullFaceMode);
|
||||
gl.FrontFace((FrontFaceDirection)_frontFace);
|
||||
|
||||
gl.SetCapability(EnableCap.SampleAlphaToCoverage, _alphaToCoverage);
|
||||
gl.SetCapability(EnableCap.Multisample, _multisample);
|
||||
|
||||
gl.SetCapability(EnableCap.StencilTest, _stencilTest);
|
||||
gl.StencilFunc(
|
||||
(StencilFunction)_stencilFunc,
|
||||
_stencilReference,
|
||||
(uint)_stencilValueMask);
|
||||
gl.StencilOp(
|
||||
(StencilOp)_stencilFail,
|
||||
(StencilOp)_stencilDepthFail,
|
||||
(StencilOp)_stencilPass);
|
||||
gl.StencilMask((uint)_stencilWriteMask);
|
||||
|
||||
gl.ColorMask(_colorMaskRed, _colorMaskGreen, _colorMaskBlue, _colorMaskAlpha);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks which texture-table slots changed since the last flush and gives them
|
||||
/// back as maximal runs of <i>consecutive</i> dirty slots.
|
||||
///
|
||||
/// <para><b>Why runs, and not one merged range.</b> The ring can flush a single
|
||||
/// span covering everything written since the last draw, because every byte in
|
||||
/// that span was allocated this frame and nothing in flight reads it. The
|
||||
/// texture table cannot: it is a long-lived array of bindless handles, and two
|
||||
/// registrations in one frame can land on slots 5 and 50 with forty-four live
|
||||
/// slots in between. Those live slots are read by draws already submitted this
|
||||
/// frame, so a single mapped write over [5, 51) — mapped with
|
||||
/// <c>GL_MAP_INVALIDATE_RANGE_BIT</c>, which lets the driver discard the whole
|
||||
/// range's contents while it is mapped, and <c>GL_MAP_UNSYNCHRONIZED_BIT</c>,
|
||||
/// which stops it from waiting first — would expose those draws to torn
|
||||
/// handles. Splitting on the gaps keeps every mapped range made only of slots
|
||||
/// that no submitted draw can be reading.</para>
|
||||
///
|
||||
/// <para><b>Why a dirty slot is safe to write unsynchronized.</b> Two producers
|
||||
/// touch a slot: <c>GlGpuDevice.RegisterTexture</c>, which writes a slot fresh
|
||||
/// from <see cref="GlTextureSlotAllocator"/> and therefore one no batch has ever
|
||||
/// indexed; and <c>ReleaseTextureSlot</c>'s zeroing write, which runs inside a
|
||||
/// retirement callback, i.e. after the fence covering every frame that could
|
||||
/// still have referenced it. Both are already past the point where the GPU can
|
||||
/// read the old value — which is exactly the assertion
|
||||
/// <c>GL_MAP_UNSYNCHRONIZED_BIT</c> makes.</para>
|
||||
///
|
||||
/// <para>GL-free by design, like <see cref="GlRingBufferState"/> and
|
||||
/// <see cref="GlTextureSlotAllocator"/>, so the bookkeeping is unit tested
|
||||
/// without a context. Enumeration is allocation-free: the tracker keeps the
|
||||
/// min/max window that has been dirtied so a clean table costs one comparison,
|
||||
/// and a dirty one scans only that window.</para>
|
||||
/// </summary>
|
||||
internal sealed class GlDirtySlotRuns
|
||||
{
|
||||
private readonly bool[] _dirty;
|
||||
private int _windowStart = -1;
|
||||
private int _windowEnd = -1;
|
||||
|
||||
public GlDirtySlotRuns(uint capacity)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfZero(capacity);
|
||||
_dirty = new bool[capacity];
|
||||
}
|
||||
|
||||
public uint Capacity => (uint)_dirty.Length;
|
||||
|
||||
public bool HasDirtySlots => _windowStart >= 0;
|
||||
|
||||
public void Mark(uint slot)
|
||||
{
|
||||
if (slot >= (uint)_dirty.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(slot),
|
||||
slot,
|
||||
$"The tracked table holds {_dirty.Length} slots.");
|
||||
}
|
||||
|
||||
_dirty[slot] = true;
|
||||
_windowStart = _windowStart < 0 ? (int)slot : Math.Min(_windowStart, (int)slot);
|
||||
_windowEnd = Math.Max(_windowEnd, (int)slot + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes the lowest remaining run of consecutive dirty slots, clearing it,
|
||||
/// and reports its first slot and length. Returns <c>false</c> once none
|
||||
/// remain, so a caller drains with a <c>while</c> loop.
|
||||
/// </summary>
|
||||
public bool TryTakeNextRun(out uint firstSlot, out uint slotCount)
|
||||
{
|
||||
firstSlot = 0;
|
||||
slotCount = 0;
|
||||
if (_windowStart < 0)
|
||||
return false;
|
||||
|
||||
int index = _windowStart;
|
||||
while (index < _windowEnd && !_dirty[index])
|
||||
index++;
|
||||
if (index >= _windowEnd)
|
||||
{
|
||||
CloseWindow();
|
||||
return false;
|
||||
}
|
||||
|
||||
int start = index;
|
||||
while (index < _windowEnd && _dirty[index])
|
||||
{
|
||||
_dirty[index] = false;
|
||||
index++;
|
||||
}
|
||||
|
||||
firstSlot = (uint)start;
|
||||
slotCount = (uint)(index - start);
|
||||
if (index >= _windowEnd)
|
||||
CloseWindow();
|
||||
else
|
||||
_windowStart = index;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CloseWindow()
|
||||
{
|
||||
_windowStart = -1;
|
||||
_windowEnd = -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>One vertex attribute's GL shape: component count, element type, and whether integer values normalize to [0,1]/[-1,1].</summary>
|
||||
/// <summary>
|
||||
/// How one vertex attribute reaches GL. <paramref name="Integer"/> selects
|
||||
/// <c>glVertexAttribIPointer</c> over <c>glVertexAttribPointer</c>: GL requires
|
||||
/// the integer entry point for an integer shader input (<c>uvec4</c> and friends)
|
||||
/// and leaves the value undefined otherwise.
|
||||
/// </summary>
|
||||
internal readonly record struct GlVertexAttributeShape(
|
||||
int ComponentCount,
|
||||
VertexAttribPointerType Type,
|
||||
bool Normalized,
|
||||
bool Integer = false);
|
||||
|
||||
/// <summary>
|
||||
/// Pure, GL-context-free mappings from the RHI's backend-neutral enums to
|
||||
/// Silk.NET's OpenGL enum values. Kept as small switch expressions so each
|
||||
/// mapping is independently unit-testable and — per the campaign spec's
|
||||
/// "when in doubt, set the state" rule — every backend-neutral value has an
|
||||
/// explicit case rather than a fallthrough default.
|
||||
/// </summary>
|
||||
internal static class GlEnumMapping
|
||||
{
|
||||
public static GlVertexAttributeShape VertexShapeOf(GpuVertexFormat format) => format switch
|
||||
{
|
||||
GpuVertexFormat.Float1 => new GlVertexAttributeShape(1, VertexAttribPointerType.Float, false),
|
||||
GpuVertexFormat.Float2 => new GlVertexAttributeShape(2, VertexAttribPointerType.Float, false),
|
||||
GpuVertexFormat.Float3 => new GlVertexAttributeShape(3, VertexAttribPointerType.Float, false),
|
||||
GpuVertexFormat.Float4 => new GlVertexAttributeShape(4, VertexAttribPointerType.Float, false),
|
||||
GpuVertexFormat.UByte4Normalized => new GlVertexAttributeShape(4, VertexAttribPointerType.UnsignedByte, true),
|
||||
// Integer attributes carry Integer = true; the encoder must route them
|
||||
// through glVertexAttribIPointer, not the normalized float path.
|
||||
GpuVertexFormat.UByte4UInt => new GlVertexAttributeShape(4, VertexAttribPointerType.UnsignedByte, false, Integer: true),
|
||||
// Slice V6l: particle.vert's per-instance `in uint aTextureIndex`.
|
||||
GpuVertexFormat.UInt1 => new GlVertexAttributeShape(1, VertexAttribPointerType.UnsignedInt, false, Integer: true),
|
||||
_ => throw new NotSupportedException($"No GL vertex attribute shape for {format}."),
|
||||
};
|
||||
|
||||
/// <summary>Slice V6l: the stencil comparison, which shares GL's depth-function enum values.</summary>
|
||||
public static StencilFunction StencilFunctionOf(GpuCompareOp compare) => compare switch
|
||||
{
|
||||
GpuCompareOp.Never => StencilFunction.Never,
|
||||
GpuCompareOp.Less => StencilFunction.Less,
|
||||
GpuCompareOp.LessOrEqual => StencilFunction.Lequal,
|
||||
GpuCompareOp.Equal => StencilFunction.Equal,
|
||||
GpuCompareOp.Greater => StencilFunction.Greater,
|
||||
GpuCompareOp.GreaterOrEqual => StencilFunction.Gequal,
|
||||
GpuCompareOp.Always => StencilFunction.Always,
|
||||
_ => throw new NotSupportedException($"No GL stencil function for {compare}."),
|
||||
};
|
||||
|
||||
/// <summary>Slice V6l: what a stencil outcome does to the stored value.</summary>
|
||||
public static StencilOp StencilOpOf(GpuStencilOp op) => op switch
|
||||
{
|
||||
GpuStencilOp.Keep => StencilOp.Keep,
|
||||
GpuStencilOp.Zero => StencilOp.Zero,
|
||||
GpuStencilOp.Replace => StencilOp.Replace,
|
||||
_ => throw new NotSupportedException($"No GL stencil operation for {op}."),
|
||||
};
|
||||
|
||||
public static PrimitiveType PrimitiveTypeOf(GpuPrimitiveTopology topology) => topology switch
|
||||
{
|
||||
GpuPrimitiveTopology.TriangleList => PrimitiveType.Triangles,
|
||||
GpuPrimitiveTopology.LineList => PrimitiveType.Lines,
|
||||
_ => throw new NotSupportedException($"No GL primitive type for {topology}."),
|
||||
};
|
||||
|
||||
public static DrawElementsType DrawElementsTypeOf(GpuIndexType indexType) => indexType switch
|
||||
{
|
||||
GpuIndexType.UInt16 => DrawElementsType.UnsignedShort,
|
||||
GpuIndexType.UInt32 => DrawElementsType.UnsignedInt,
|
||||
_ => throw new NotSupportedException($"No GL index type for {indexType}."),
|
||||
};
|
||||
|
||||
public static int IndexSizeBytesOf(GpuIndexType indexType) => indexType switch
|
||||
{
|
||||
GpuIndexType.UInt16 => sizeof(ushort),
|
||||
GpuIndexType.UInt32 => sizeof(uint),
|
||||
_ => throw new NotSupportedException($"No index byte size for {indexType}."),
|
||||
};
|
||||
|
||||
public static DepthFunction DepthFunctionOf(GpuCompareOp compareOp) => compareOp switch
|
||||
{
|
||||
GpuCompareOp.Never => DepthFunction.Never,
|
||||
GpuCompareOp.Less => DepthFunction.Less,
|
||||
GpuCompareOp.LessOrEqual => DepthFunction.Lequal,
|
||||
GpuCompareOp.Equal => DepthFunction.Equal,
|
||||
GpuCompareOp.Greater => DepthFunction.Greater,
|
||||
GpuCompareOp.GreaterOrEqual => DepthFunction.Gequal,
|
||||
GpuCompareOp.Always => DepthFunction.Always,
|
||||
_ => throw new NotSupportedException($"No GL depth function for {compareOp}."),
|
||||
};
|
||||
|
||||
public static TriangleFace CullFaceModeOf(GpuCullMode cullMode) => cullMode switch
|
||||
{
|
||||
GpuCullMode.Back => TriangleFace.Back,
|
||||
GpuCullMode.Front => TriangleFace.Front,
|
||||
// GpuCullMode.None never reaches glCullFace — culling is disabled instead.
|
||||
_ => throw new NotSupportedException($"No GL cull face mode for {cullMode}."),
|
||||
};
|
||||
|
||||
public static FrontFaceDirection FrontFaceDirectionOf(GpuFrontFace frontFace) => frontFace switch
|
||||
{
|
||||
GpuFrontFace.CounterClockwise => FrontFaceDirection.Ccw,
|
||||
GpuFrontFace.Clockwise => FrontFaceDirection.CW,
|
||||
_ => throw new NotSupportedException($"No GL front-face direction for {frontFace}."),
|
||||
};
|
||||
|
||||
public static (BlendingFactor Source, BlendingFactor Destination) BlendFactorsOf(GpuBlendMode blend) => blend switch
|
||||
{
|
||||
GpuBlendMode.StraightAlpha => (BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha),
|
||||
GpuBlendMode.Additive => (BlendingFactor.SrcAlpha, BlendingFactor.One),
|
||||
GpuBlendMode.InverseAlpha => (BlendingFactor.OneMinusSrcAlpha, BlendingFactor.SrcAlpha),
|
||||
// GpuBlendMode.None never reaches glBlendFunc — blending is disabled instead.
|
||||
_ => throw new NotSupportedException($"No GL blend factors for {blend}."),
|
||||
};
|
||||
|
||||
public static TextureMinFilter MinFilterOf(GpuFilter filter, GpuMipFilter mipFilter) => (filter, mipFilter) switch
|
||||
{
|
||||
(GpuFilter.Nearest, GpuMipFilter.None) => TextureMinFilter.Nearest,
|
||||
(GpuFilter.Linear, GpuMipFilter.None) => TextureMinFilter.Linear,
|
||||
(GpuFilter.Nearest, GpuMipFilter.Nearest) => TextureMinFilter.NearestMipmapNearest,
|
||||
(GpuFilter.Linear, GpuMipFilter.Nearest) => TextureMinFilter.LinearMipmapNearest,
|
||||
(GpuFilter.Nearest, GpuMipFilter.Linear) => TextureMinFilter.NearestMipmapLinear,
|
||||
(GpuFilter.Linear, GpuMipFilter.Linear) => TextureMinFilter.LinearMipmapLinear,
|
||||
_ => throw new NotSupportedException($"No GL min filter for {filter}/{mipFilter}."),
|
||||
};
|
||||
|
||||
public static TextureMagFilter MagFilterOf(GpuFilter filter) => filter switch
|
||||
{
|
||||
GpuFilter.Nearest => TextureMagFilter.Nearest,
|
||||
GpuFilter.Linear => TextureMagFilter.Linear,
|
||||
_ => throw new NotSupportedException($"No GL mag filter for {filter}."),
|
||||
};
|
||||
|
||||
public static TextureWrapMode WrapModeOf(GpuAddressMode addressMode) => addressMode switch
|
||||
{
|
||||
GpuAddressMode.Repeat => TextureWrapMode.Repeat,
|
||||
GpuAddressMode.ClampToEdge => TextureWrapMode.ClampToEdge,
|
||||
_ => throw new NotSupportedException($"No GL wrap mode for {addressMode}."),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// A plain GL buffer object. Deliberately not one of the existing
|
||||
/// <c>ManagedGL*</c> wrappers: those implement Chorizite's <c>IVertexBuffer</c>/
|
||||
/// <c>IIndexBuffer</c> (generic-over-<c>IVertex</c>, single-usage) and are
|
||||
/// constructed against <see cref="OpenGLGraphicsDevice"/>, which the campaign
|
||||
/// explicitly sheds — <see cref="GlGpuDevice"/> is a fresh root. One GL buffer
|
||||
/// object already serves every <see cref="GpuBufferUsage"/> combination (the
|
||||
/// usage only matters at bind time), so a single small class covers the whole
|
||||
/// <see cref="IGpuBuffer"/> surface without forking per usage.
|
||||
///
|
||||
/// Allocated once at the description's size via <c>glBufferData</c> with null
|
||||
/// data. There are two write paths after that, and which one applies is a
|
||||
/// property of the buffer's role rather than of the buffer object:
|
||||
/// <see cref="Upload"/> is an ordinary synchronized <c>glBufferSubData</c>, used
|
||||
/// for one-off and streaming writes the driver must order for us (the mesh
|
||||
/// arena, texture staging); <see cref="WriteRangeUnsynchronized"/> maps the
|
||||
/// range and is used only by the per-frame upload ring, whose non-overlap
|
||||
/// invariant the caller can state. Neither path holds a persistent mapping.
|
||||
///
|
||||
/// The <c>glBufferData</c> usage hint follows
|
||||
/// <see cref="GpuBufferDescription.Residency"/>:
|
||||
/// <see cref="GpuMemoryResidency.DeviceLocal"/> is written rarely and read by
|
||||
/// many draws, so it takes <c>StaticDraw</c>; the host-writable rings and
|
||||
/// tables are rewritten every frame and take <c>DynamicDraw</c>. The hint is
|
||||
/// advisory to the driver, but keeping it per-residency is what let the mesh
|
||||
/// arena (Campaign V slice V4b) move onto this class without changing the
|
||||
/// allocation it has always requested.
|
||||
/// </summary>
|
||||
internal sealed class GlGpuBuffer : IGpuBuffer
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly IGpuResourceRetirementQueue _retirement;
|
||||
private uint _name;
|
||||
|
||||
public GlGpuBuffer(GL gl, IGpuResourceRetirementQueue retirement, GpuBufferDescription description)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
|
||||
Name = description.Name;
|
||||
SizeBytes = description.SizeBytes;
|
||||
Usage = description.Usage;
|
||||
Residency = description.Residency;
|
||||
|
||||
_name = GlResourceCommand.CreateName(_gl, $"buffer '{Name}'", _gl.GenBuffer, _gl.DeleteBuffer);
|
||||
try
|
||||
{
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, _name);
|
||||
unsafe
|
||||
{
|
||||
_gl.BufferData(GLEnum.CopyWriteBuffer, (nuint)SizeBytes, null, UsageHintFor(Residency));
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"allocate buffer '{Name}' ({SizeBytes} bytes)");
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A rejected data store (GL_OUT_OF_MEMORY is a real outcome for the
|
||||
// mesh arena's 384 MiB growth destination) must not strand the name
|
||||
// that was already created for it. The caller sees the original
|
||||
// failure and owns nothing.
|
||||
GlResourceCommand.DeleteBuffer(
|
||||
_gl,
|
||||
_name,
|
||||
$"rollback buffer '{Name}' after a failed allocation");
|
||||
_name = 0;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public long SizeBytes { get; }
|
||||
public GpuBufferUsage Usage { get; }
|
||||
public GpuMemoryResidency Residency { get; }
|
||||
|
||||
/// <summary>The physical GL buffer name. For bind calls issued by <see cref="GlGpuPassEncoder"/>.</summary>
|
||||
internal uint GlName => _name;
|
||||
|
||||
private static BufferUsageARB UsageHintFor(GpuMemoryResidency residency) =>
|
||||
residency == GpuMemoryResidency.DeviceLocal
|
||||
? BufferUsageARB.StaticDraw
|
||||
: BufferUsageARB.DynamicDraw;
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the physical buffer on the calling thread instead of deferring it
|
||||
/// through the device's retirement queue the way <see cref="Dispose"/> does.
|
||||
///
|
||||
/// Campaign V slice V4b: the mesh arena (<c>GlobalMeshBuffer</c>) already gates
|
||||
/// every arena delete behind its own <c>GpuRetirementLedger</c> and decrements
|
||||
/// its <c>MaximumPhysicalArenaBytes</c> accounting in the same retirement stage.
|
||||
/// Routing the physical free through the queue a second time would delay it by
|
||||
/// a further flight generation, so the arena's physical-capacity accounting
|
||||
/// would run ahead of real GPU residency and could admit a migration that
|
||||
/// breaches the 896 MiB dual-generation ceiling. A caller of this method must
|
||||
/// therefore already have proved no submitted frame can still reference the
|
||||
/// buffer.
|
||||
///
|
||||
/// Retryable by construction: the managed name is cleared only after the driver
|
||||
/// reports success, so a failed delete is re-issued by the next attempt, and any
|
||||
/// later call (including <see cref="Dispose"/>) is a no-op.
|
||||
/// </summary>
|
||||
internal void DeleteRetired(string context)
|
||||
{
|
||||
uint name = _name;
|
||||
if (name == 0)
|
||||
return;
|
||||
|
||||
_gl.DeleteBuffer(name);
|
||||
// Per the GL error contract a command which generates an error does not
|
||||
// change object state, so validation stays in the same stage as the
|
||||
// mutation and the name is only surrendered once deletion committed.
|
||||
GLHelpers.ThrowOnResourceError(_gl, context);
|
||||
_name = 0;
|
||||
}
|
||||
|
||||
public void Upload(long offsetBytes, ReadOnlySpan<byte> data)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (offsetBytes < 0 || offsetBytes + data.Length > SizeBytes)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(offsetBytes),
|
||||
$"Upload of {data.Length} bytes at offset {offsetBytes} exceeds buffer '{Name}' ({SizeBytes} bytes).");
|
||||
}
|
||||
if (data.IsEmpty)
|
||||
return;
|
||||
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, _name);
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* pointer = data)
|
||||
_gl.BufferSubData(GLEnum.CopyWriteBuffer, (nint)offsetBytes, (nuint)data.Length, pointer);
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"upload {data.Length} bytes to buffer '{Name}' at offset {offsetBytes}");
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes <paramref name="data"/> at <paramref name="offsetBytes"/> through
|
||||
/// <c>glMapBufferRange</c> with
|
||||
/// <c>GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_RANGE_BIT</c>
|
||||
/// — the canonical GL upload-ring idiom, and the only write path the
|
||||
/// per-frame ring uses.
|
||||
///
|
||||
/// <para><b>Why not <see cref="Upload"/>.</b> A partial <c>glBufferSubData</c>
|
||||
/// into a buffer object that already-submitted draws are still reading leaves
|
||||
/// the driver to guess how to keep both truths alive: it can stall, it can
|
||||
/// rename the whole data store and copy the untouched remainder forward, or
|
||||
/// it can route the write through an internal staging copy. Which one it
|
||||
/// picks is a heuristic, and the heuristic is fed by the update pattern —
|
||||
/// Campaign V's V4c revert (plan §5.5) saw a world that rendered blank
|
||||
/// roughly one launch in three, with no GL error at any point, on the
|
||||
/// connected path that issues 10–40 such partial updates per frame, and zero
|
||||
/// times on the offline path that issues 2–4. Mapping the range instead
|
||||
/// removes the guess: the three bits together say "I am writing this range,
|
||||
/// I am overwriting all of it, and nothing in flight reads it," which is
|
||||
/// exactly the ring's actual invariant.</para>
|
||||
///
|
||||
/// <para><b>The precondition is the caller's to prove.</b>
|
||||
/// <c>GL_MAP_UNSYNCHRONIZED_BIT</c> means the driver inserts no wait, so the
|
||||
/// caller asserts that no GL command already submitted and not yet complete
|
||||
/// reads any byte of <c>[offsetBytes, offsetBytes + data.Length)</c>. The ring
|
||||
/// has that invariant on both axes: within a frame the allocation cursor only
|
||||
/// moves forward and <see cref="GlRingBufferState"/> refuses a write below the
|
||||
/// flushed high-water mark, and across frames a slot is only rewritten after
|
||||
/// <c>GpuFrameFlightController.BeginFrame</c> has waited on the fence for the
|
||||
/// last frame that used it. <c>GL_MAP_INVALIDATE_RANGE_BIT</c> is likewise
|
||||
/// only sound because every byte of the mapped range is then written.</para>
|
||||
///
|
||||
/// <para>A <c>false</c> return from <c>glUnmapBuffer</c> means the data store
|
||||
/// was lost while mapped (a GPU reset or display-mode change), so the write
|
||||
/// did not land. That throws rather than being retried: the frame's uploads
|
||||
/// are already partly gone, and silently continuing would draw from a
|
||||
/// half-written ring.</para>
|
||||
/// </summary>
|
||||
internal unsafe void WriteRangeUnsynchronized(long offsetBytes, ReadOnlySpan<byte> data, string context)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (offsetBytes < 0 || offsetBytes + data.Length > SizeBytes)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(offsetBytes),
|
||||
$"Mapped write of {data.Length} bytes at offset {offsetBytes} exceeds buffer '{Name}' ({SizeBytes} bytes).");
|
||||
}
|
||||
if (data.IsEmpty)
|
||||
return;
|
||||
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, _name);
|
||||
void* mapped = _gl.MapBufferRange(
|
||||
GLEnum.CopyWriteBuffer,
|
||||
(nint)offsetBytes,
|
||||
(nuint)data.Length,
|
||||
MapBufferAccessMask.WriteBit
|
||||
| MapBufferAccessMask.UnsynchronizedBit
|
||||
| MapBufferAccessMask.InvalidateRangeBit);
|
||||
if (mapped is null)
|
||||
{
|
||||
// A null return always sets a GL error, so surface that first — it
|
||||
// names the actual rejection instead of the symptom.
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
$"map {data.Length} bytes of buffer '{Name}' at offset {offsetBytes} ({context})");
|
||||
throw new InvalidOperationException(
|
||||
$"glMapBufferRange returned no pointer for {data.Length} bytes of buffer '{Name}' " +
|
||||
$"at offset {offsetBytes} ({context}).");
|
||||
}
|
||||
|
||||
data.CopyTo(new Span<byte>(mapped, data.Length));
|
||||
|
||||
bool unmapped = _gl.UnmapBuffer(GLEnum.CopyWriteBuffer);
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"unmap buffer '{Name}' after writing {context}");
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, 0);
|
||||
if (!unmapped)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Buffer '{Name}' lost its data store while {data.Length} bytes at offset " +
|
||||
$"{offsetBytes} ({context}) were mapped; the write did not land.");
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyTo(IGpuBuffer destination, long sourceOffsetBytes, long destinationOffsetBytes, long byteCount)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ArgumentNullException.ThrowIfNull(destination);
|
||||
if (destination is not GlGpuBuffer target)
|
||||
throw new ArgumentException("A GL device can only copy into a GL buffer.", nameof(destination));
|
||||
if (byteCount == 0)
|
||||
return;
|
||||
|
||||
_gl.BindBuffer(GLEnum.CopyReadBuffer, _name);
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, target._name);
|
||||
_gl.CopyBufferSubData(
|
||||
GLEnum.CopyReadBuffer,
|
||||
GLEnum.CopyWriteBuffer,
|
||||
(nint)sourceOffsetBytes,
|
||||
(nint)destinationOffsetBytes,
|
||||
(nuint)byteCount);
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
$"copy {byteCount} bytes from buffer '{Name}' to '{target.Name}'");
|
||||
_gl.BindBuffer(GLEnum.CopyReadBuffer, 0);
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, 0);
|
||||
}
|
||||
|
||||
public void Read(long offsetBytes, Span<byte> destination)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (destination.IsEmpty)
|
||||
return;
|
||||
|
||||
_gl.BindBuffer(GLEnum.CopyReadBuffer, _name);
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* pointer = destination)
|
||||
_gl.GetBufferSubData(GLEnum.CopyReadBuffer, (nint)offsetBytes, (nuint)destination.Length, pointer);
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"read {destination.Length} bytes from buffer '{Name}' at offset {offsetBytes}");
|
||||
_gl.BindBuffer(GLEnum.CopyReadBuffer, 0);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
uint name = _name;
|
||||
if (name == 0)
|
||||
return;
|
||||
_name = 0;
|
||||
|
||||
GL gl = _gl;
|
||||
string label = Name;
|
||||
_retirement.Retire(() =>
|
||||
{
|
||||
gl.DeleteBuffer(name);
|
||||
GLHelpers.ThrowOnResourceError(gl, $"delete buffer '{label}' ({name})");
|
||||
});
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_name == 0)
|
||||
throw new ObjectDisposedException(Name);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,667 +0,0 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Diagnostics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// OpenGL 4.3 implementation of <see cref="IGpuDevice"/> — Campaign V slice V1.
|
||||
/// See <c>docs/plans/2026-07-27-vulkan-campaign.md</c> §3 for the pinned
|
||||
/// contract this backend fills and §5 for what this slice covers.
|
||||
///
|
||||
/// Deliberately NOT derived from Chorizite's <c>BaseGraphicsDevice</c>/
|
||||
/// <see cref="OpenGLGraphicsDevice"/> — this is a fresh root, which is one of
|
||||
/// the things Campaign V sheds. It owns its own <see cref="BindlessSupport"/>
|
||||
/// instance (created in the constructor) rather than sharing the legacy WB
|
||||
/// render path's; both simply wrap the same stateless
|
||||
/// <c>GL_ARB_bindless_texture</c> extension, so two instances coexist safely,
|
||||
/// and it lets this device be constructed the moment a GL context and a
|
||||
/// <see cref="GpuFrameFlightController"/> exist — no dependency on when the
|
||||
/// legacy path happens to detect bindless support during composition.
|
||||
///
|
||||
/// <para><b>Ring capacity.</b> Each flight slot gets one managed staging
|
||||
/// <c>byte[]</c> and one same-sized GL buffer, both fixed at construction
|
||||
/// (default 16 MiB — see <see cref="DefaultRingCapacityBytesPerSlot"/>). V1
|
||||
/// chose "throw with a message naming the needed size" over "grow by create-
|
||||
/// copy-retire" for an over-capacity request: nothing consumes this device
|
||||
/// yet, so there is no real per-frame data volume to size against, and a
|
||||
/// silent/implicit grow would hide a future renderer's actual working set
|
||||
/// from the person porting it. <see cref="GlRingBufferState"/> is the pure
|
||||
/// piece that enforces this.</para>
|
||||
///
|
||||
/// <para><b>Flush discipline.</b> Ring bytes and the texture-handle table are
|
||||
/// both flushed immediately before every
|
||||
/// <c>Draw</c>/<c>DrawIndexed</c>/<c>MultiDrawIndexedIndirect</c> — see
|
||||
/// <see cref="FlushBeforeDraw"/> — never at bind time, so a renderer that
|
||||
/// writes after binding still uploads correctly. Both flushes go through
|
||||
/// <see cref="GlGpuBuffer.WriteRangeUnsynchronized"/>, i.e.
|
||||
/// <c>glMapBufferRange</c> with
|
||||
/// <c>GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_RANGE_BIT</c>,
|
||||
/// rather than <c>glBufferSubData</c>: these are the only two buffers this
|
||||
/// backend rewrites while the frame's own draws are still in flight, and that
|
||||
/// is precisely the case where a partial <c>glBufferSubData</c> leaves the
|
||||
/// result to a driver heuristic. See that method's remarks and campaign plan
|
||||
/// §5.5 for the failure it produced. The two flushes differ in shape because
|
||||
/// their non-overlap proofs differ: the ring writes one span (its cursor only
|
||||
/// moves forward within a frame), the table writes one span per run of
|
||||
/// consecutive dirty slots (see <see cref="GlDirtySlotRuns"/>).</para>
|
||||
/// </summary>
|
||||
internal sealed class GlGpuDevice : IGpuDevice
|
||||
{
|
||||
internal const int DefaultRingCapacityBytesPerSlot = 16 * 1024 * 1024;
|
||||
|
||||
// GL tokens not exposed as named Silk.NET GetPName members (mirrors the
|
||||
// same raw-hex pattern GraphicalCapabilityRecord.cs already uses for
|
||||
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS).
|
||||
private const int GlMaxShaderStorageBufferBindings = 0x90DD;
|
||||
private const int GlMaxClipDistances = 0x0D32;
|
||||
private const int GlMaxSamples = 0x8D57;
|
||||
private const int GlShaderStorageBufferOffsetAlignment = 0x90DF;
|
||||
|
||||
private readonly GL _gl;
|
||||
private readonly GpuFrameFlightController _frameFlights;
|
||||
private readonly BindlessSupport _bindless;
|
||||
private readonly string _shadersDirectory;
|
||||
private readonly int _ringCapacityBytesPerSlot;
|
||||
|
||||
private readonly GlRingBufferState[] _ringStates;
|
||||
private readonly byte[][] _ringStaging;
|
||||
private readonly GlGpuBuffer[] _ringBuffers;
|
||||
|
||||
private readonly GlTextureSlotAllocator _textureSlotAllocator =
|
||||
new(GpuBindingModel.TextureTableCapacity);
|
||||
private readonly ulong[] _textureHandleTable = new ulong[GpuBindingModel.TextureTableCapacity];
|
||||
private readonly GlGpuBuffer _textureTableBuffer;
|
||||
private readonly GlDirtySlotRuns _textureTableDirtySlots =
|
||||
new(GpuBindingModel.TextureTableCapacity);
|
||||
|
||||
private readonly GlRenderStateCache _renderState = new();
|
||||
private readonly GlGpuPushConstantBinder _pushConstants;
|
||||
private readonly GlGpuTimerPool _timerPool;
|
||||
private readonly Dictionary<GpuSamplerDescription, GlGpuSampler> _samplers = [];
|
||||
private readonly List<Action> _queuedActions = [];
|
||||
private readonly GlGpuTexture _defaultTexture;
|
||||
|
||||
private long _nextSerial;
|
||||
private bool _disposed;
|
||||
|
||||
public GlGpuDevice(
|
||||
GL gl,
|
||||
GpuFrameFlightController frameFlights,
|
||||
string shadersDirectory,
|
||||
int ringCapacityBytesPerSlot = DefaultRingCapacityBytesPerSlot)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_frameFlights = frameFlights ?? throw new ArgumentNullException(nameof(frameFlights));
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(shadersDirectory);
|
||||
_shadersDirectory = shadersDirectory;
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(ringCapacityBytesPerSlot);
|
||||
_ringCapacityBytesPerSlot = ringCapacityBytesPerSlot;
|
||||
|
||||
if (!BindlessSupport.TryCreate(_gl, out BindlessSupport? bindless) || bindless is null)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"GlGpuDevice requires GL_ARB_bindless_texture. The startup capability gate " +
|
||||
"(GraphicalCapabilityGuard) should already have rejected a driver without it, " +
|
||||
"so reaching this means the device was constructed before that gate ran.");
|
||||
}
|
||||
_bindless = bindless;
|
||||
|
||||
Capabilities = CaptureCapabilities();
|
||||
|
||||
int slotCount = _frameFlights.SlotCount;
|
||||
_ringStates = new GlRingBufferState[slotCount];
|
||||
_ringStaging = new byte[slotCount][];
|
||||
_ringBuffers = new GlGpuBuffer[slotCount];
|
||||
for (int slot = 0; slot < slotCount; slot++)
|
||||
{
|
||||
_ringStates[slot] = new GlRingBufferState(_ringCapacityBytesPerSlot);
|
||||
_ringStaging[slot] = new byte[_ringCapacityBytesPerSlot];
|
||||
_ringBuffers[slot] = new GlGpuBuffer(
|
||||
_gl,
|
||||
Retirement,
|
||||
new GpuBufferDescription(
|
||||
$"gpu-ring-slot-{slot}",
|
||||
_ringCapacityBytesPerSlot,
|
||||
GpuBufferUsage.Storage | GpuBufferUsage.Uniform | GpuBufferUsage.Indirect,
|
||||
GpuMemoryResidency.HostWritable));
|
||||
}
|
||||
|
||||
_textureTableBuffer = new GlGpuBuffer(
|
||||
_gl,
|
||||
Retirement,
|
||||
new GpuBufferDescription(
|
||||
"gpu-texture-table",
|
||||
GpuBindingModel.TextureTableCapacity * sizeof(ulong),
|
||||
GpuBufferUsage.Storage,
|
||||
GpuMemoryResidency.HostWritable));
|
||||
|
||||
_pushConstants = new GlGpuPushConstantBinder(_gl);
|
||||
_timerPool = new GlGpuTimerPool(new SilkGlTimerQueryApi(_gl), Capabilities.SupportsTimestampQueries);
|
||||
|
||||
_defaultTexture = new GlGpuTexture(
|
||||
_gl,
|
||||
Retirement,
|
||||
new GpuTextureDescription(
|
||||
"default-white",
|
||||
GpuTextureKind.Texture2D,
|
||||
GpuTextureFormat.Rgba8Unorm,
|
||||
Width: 1,
|
||||
Height: 1,
|
||||
LayerCount: 1,
|
||||
MipLevelCount: 1));
|
||||
_defaultTexture.Upload(0, 0, [255, 255, 255, 255]);
|
||||
IGpuSampler defaultSampler = CreateSampler(GpuSamplerDescription.UiNearest);
|
||||
DefaultTextureSlot = RegisterTexture(_defaultTexture, defaultSampler);
|
||||
}
|
||||
|
||||
public GpuBackendKind Backend => GpuBackendKind.OpenGl;
|
||||
public GpuCapabilityRecord Capabilities { get; }
|
||||
public IGpuResourceRetirementQueue Retirement => _frameFlights;
|
||||
public IGpuTimerPool Timers => _timerPool;
|
||||
public GpuTextureSlot DefaultTextureSlot { get; }
|
||||
|
||||
internal GL Gl => _gl;
|
||||
internal GlGpuPushConstantBinder PushConstants => _pushConstants;
|
||||
internal GlGpuTimerPool TimerPool => _timerPool;
|
||||
|
||||
/// <summary>
|
||||
/// GL name of the buffer emulating the global texture table
|
||||
/// (<see cref="GpuBindingModel.StorageTextureTable"/>). Slice V6d:
|
||||
/// <see cref="GlGpuPassEncoder.BindPipeline"/> binds it on every pipeline
|
||||
/// bind, which is the GL analogue of the Vulkan backend binding descriptor
|
||||
/// set 2 on every draw. Without it an RHI shader that samples the table
|
||||
/// would read whichever raw-GL renderer's private handle table was left at
|
||||
/// binding 9 — a different slot numbering entirely, which is the loudest
|
||||
/// possible way to sample the wrong texture.
|
||||
/// </summary>
|
||||
internal uint TextureTableGlName => _textureTableBuffer.GlName;
|
||||
|
||||
internal GlRenderStateSnapshot CurrentRenderState { get; private set; }
|
||||
|
||||
public IGpuBuffer CreateBuffer(in GpuBufferDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return new GlGpuBuffer(_gl, Retirement, description);
|
||||
}
|
||||
|
||||
public IGpuTexture CreateTexture(in GpuTextureDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return new GlGpuTexture(_gl, Retirement, description);
|
||||
}
|
||||
|
||||
public IGpuSampler CreateSampler(in GpuSamplerDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (_samplers.TryGetValue(description, out GlGpuSampler? existing))
|
||||
return existing;
|
||||
|
||||
var created = new GlGpuSampler(_gl, Retirement, description);
|
||||
_samplers.Add(description, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
public IGpuPipeline CreatePipeline(GpuPipelineDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ArgumentNullException.ThrowIfNull(description);
|
||||
string vertexPath = Path.Combine(_shadersDirectory, $"{description.Shaders.Name}.vert");
|
||||
string fragmentPath = Path.Combine(_shadersDirectory, $"{description.Shaders.Name}.frag");
|
||||
// Campaign V slice V6d: every RHI pipeline gets the shared preamble,
|
||||
// unconditionally. The Vulkan backend injects its own preamble into
|
||||
// every shader it compiles, so making the GL side selective would mean
|
||||
// one source file compiling against two different sets of definitions
|
||||
// depending on which pipeline happened to ask for it. A shader that
|
||||
// reads nothing from the preamble simply carries an unused declaration.
|
||||
string common = File.ReadAllText(Path.Combine(_shadersDirectory, "common.glsl"));
|
||||
string vertexSource = Shader.InjectPreamble(File.ReadAllText(vertexPath), common);
|
||||
string fragmentSource = Shader.InjectPreamble(File.ReadAllText(fragmentPath), common);
|
||||
return new GlGpuPipeline(_gl, Retirement, description, vertexSource, fragmentSource);
|
||||
}
|
||||
|
||||
public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return new GlGpuRenderTarget(_gl, Retirement, description);
|
||||
}
|
||||
|
||||
public GpuTextureSlot RegisterTexture(IGpuTexture texture, IGpuSampler sampler)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ArgumentNullException.ThrowIfNull(texture);
|
||||
ArgumentNullException.ThrowIfNull(sampler);
|
||||
if (texture is not GlGpuTexture glTexture)
|
||||
throw new ArgumentException("The GL backend can only register a GL texture.", nameof(texture));
|
||||
if (sampler is not GlGpuSampler glSampler)
|
||||
throw new ArgumentException("The GL backend can only register a GL sampler.", nameof(sampler));
|
||||
|
||||
uint slot = _textureSlotAllocator.Allocate();
|
||||
ulong handle = _bindless.GetResidentHandle(glTexture.GlName, glSampler.GlName);
|
||||
WriteHandle(slot, handle);
|
||||
return new GpuTextureSlot(slot);
|
||||
}
|
||||
|
||||
public void ReleaseTextureSlot(GpuTextureSlot slot)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (!slot.IsAssigned)
|
||||
throw new ArgumentException("Cannot release an unassigned texture slot.", nameof(slot));
|
||||
|
||||
uint index = slot.Index;
|
||||
// Deferred through the retirement queue: a submitted-but-not-yet-
|
||||
// retired frame may still read this slot's handle, so the free list
|
||||
// (and thus reuse) must wait for that frame to retire.
|
||||
Retirement.Retire(() =>
|
||||
{
|
||||
WriteHandle(index, 0);
|
||||
_textureSlotAllocator.Release(index);
|
||||
});
|
||||
}
|
||||
|
||||
public IGpuFrame BeginFrame()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
_frameFlights.BeginFrame();
|
||||
long serial = ++_nextSerial;
|
||||
int slot = _frameFlights.CurrentSlot;
|
||||
_ringStates[slot].Reset();
|
||||
return new GlGpuFrame(this, slot, serial);
|
||||
}
|
||||
|
||||
internal void EndFrame() => _frameFlights.EndFrame();
|
||||
|
||||
internal GpuRingAllocation AllocateRing(int slotIndex, int byteCount, GpuRingUsage usage)
|
||||
{
|
||||
uint alignment = usage switch
|
||||
{
|
||||
GpuRingUsage.Storage => Capabilities.MinStorageBufferOffsetAlignment,
|
||||
GpuRingUsage.Uniform => Capabilities.MinUniformBufferOffsetAlignment,
|
||||
_ => 4u,
|
||||
};
|
||||
uint offset = _ringStates[slotIndex].Allocate(byteCount, alignment);
|
||||
Span<byte> data = byteCount == 0
|
||||
? Span<byte>.Empty
|
||||
: _ringStaging[slotIndex].AsSpan((int)offset, byteCount);
|
||||
return new GpuRingAllocation(_ringBuffers[slotIndex], offset, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes this slot's dirty ring bytes and every dirty run of the texture
|
||||
/// table into their GL buffers, then clears both watermarks. Called
|
||||
/// immediately before every draw — never at bind time, because a renderer
|
||||
/// may still write into a ring allocation after binding it.
|
||||
///
|
||||
/// Both writes are mapped and unsynchronized (see the class remarks and
|
||||
/// <see cref="GlGpuBuffer.WriteRangeUnsynchronized"/>). The ring's dirty
|
||||
/// span may include alignment padding between allocations; those bytes are
|
||||
/// below the cursor, were allocated this frame, and are read by nothing, so
|
||||
/// covering them in one map is sound and saves a call. The table has no
|
||||
/// equivalent licence, which is why it drains run by run.
|
||||
/// </summary>
|
||||
internal void FlushBeforeDraw(int slotIndex)
|
||||
{
|
||||
(int start, int length) = _ringStates[slotIndex].TakeDirtyRange();
|
||||
if (length > 0)
|
||||
{
|
||||
_ringBuffers[slotIndex].WriteRangeUnsynchronized(
|
||||
start,
|
||||
_ringStaging[slotIndex].AsSpan(start, length),
|
||||
$"ring slot {slotIndex}");
|
||||
}
|
||||
|
||||
FlushTextureTable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drains every dirty run of the texture table into its GL buffer. Called by
|
||||
/// <see cref="FlushBeforeDraw"/> for RHI draws, and directly by the still-raw-GL
|
||||
/// world renderers before their own draws — see the V4t remarks on
|
||||
/// <see cref="RegisterWorldTextureHandle"/>.
|
||||
/// </summary>
|
||||
internal void FlushTextureTable()
|
||||
{
|
||||
while (_textureTableDirtySlots.TryTakeNextRun(out uint firstSlot, out uint slotCount))
|
||||
{
|
||||
ReadOnlySpan<byte> bytes = MemoryMarshal.AsBytes(
|
||||
_textureHandleTable.AsSpan((int)firstSlot, (int)slotCount));
|
||||
_textureTableBuffer.WriteRangeUnsynchronized(
|
||||
(long)firstSlot * sizeof(ulong),
|
||||
bytes,
|
||||
$"texture table slots {firstSlot}..{firstSlot + slotCount - 1}");
|
||||
}
|
||||
}
|
||||
|
||||
internal void BeginPass(GpuPassDescription description)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ArgumentNullException.ThrowIfNull(description);
|
||||
if (description.Color.Store == GpuStoreOp.Resolve
|
||||
|| description.Depth is { Store: GpuStoreOp.Resolve })
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"GpuStoreOp.Resolve is a Vulkan-only path; Campaign V slice V1's GL backend " +
|
||||
"only ever renders single-sampled targets.");
|
||||
}
|
||||
|
||||
uint framebuffer = 0;
|
||||
if (description.Color.Target is { } target)
|
||||
{
|
||||
if (target is not GlGpuRenderTarget glTarget)
|
||||
throw new ArgumentException("The GL backend can only render into a GL render target.");
|
||||
framebuffer = glTarget.GlFramebufferName;
|
||||
}
|
||||
_gl.BindFramebuffer(GLEnum.Framebuffer, framebuffer);
|
||||
|
||||
bool clearsColor = description.Color.Load == GpuLoadOp.Clear;
|
||||
bool clearsDepth = description.Depth is { Load: GpuLoadOp.Clear };
|
||||
if (clearsColor || clearsDepth)
|
||||
{
|
||||
// Force the write masks on before clearing, regardless of what
|
||||
// the previous pass's last draw left them at (e.g. depth-write
|
||||
// disabled mid-translucent-pass) — glClear silently no-ops for a
|
||||
// buffer whose mask is off.
|
||||
ClearBufferMask mask = 0;
|
||||
if (clearsColor)
|
||||
{
|
||||
_gl.ColorMask(true, true, true, true);
|
||||
Vector4 clearColor = description.Color.ClearColor;
|
||||
_gl.ClearColor(clearColor.X, clearColor.Y, clearColor.Z, clearColor.W);
|
||||
mask |= ClearBufferMask.ColorBufferBit;
|
||||
}
|
||||
if (description.Depth is { Load: GpuLoadOp.Clear } depth)
|
||||
{
|
||||
_gl.DepthMask(true);
|
||||
_gl.ClearDepth(depth.ClearDepth);
|
||||
_gl.ClearStencil((int)depth.ClearStencil);
|
||||
mask |= ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit;
|
||||
}
|
||||
_gl.Clear(mask);
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"clear pass '{description.Name}'");
|
||||
}
|
||||
|
||||
// Campaign V slice V4a (2026-07-27 revert postmortem, plan §7.1 rule 2):
|
||||
// reset unconditionally on EVERY pass, not only a clearing one. While
|
||||
// raw-GL renderers coexist with RHI-ported ones (through V4h), a
|
||||
// raw-GL renderer can run between two RHI passes within the same frame
|
||||
// and change GL program/blend/depth/cull state the cache never
|
||||
// observes. A reset gated on "this pass cleared" left the cache
|
||||
// trusting a stale belief in that case, so a later BindPipeline
|
||||
// skipped re-issuing glUseProgram and the following push-constant
|
||||
// upload threw GL_INVALID_OPERATION against whatever program was
|
||||
// actually bound — exactly the failure the first V4a attempt hit.
|
||||
// Resetting on every BeginPass costs one redundant state application
|
||||
// on the pass's first bind and is removed at V4h once nothing raw-GL
|
||||
// remains.
|
||||
_renderState.Reset();
|
||||
}
|
||||
|
||||
// Campaign V slice V6k deleted the V4a pre-approved transitional seam
|
||||
// (RegisterExternalColorTexture / TryResolveExternalColorTexture, campaign
|
||||
// doc §7.1's final paragraph). It existed so the retained UI could blit the
|
||||
// paperdoll and creature-appraisal viewport textures while their renderer
|
||||
// still owned a hand-rolled FBO the RHI knew nothing about. That renderer now
|
||||
// creates an IGpuRenderTarget and registers its colour attachment through
|
||||
// RegisterTexture like anything else, so the escape hatch has no caller —
|
||||
// exactly the end §7.1 wrote for it.
|
||||
|
||||
// ── V4t transitional seam: the world texture stack's entry to this table ──
|
||||
//
|
||||
// Campaign V slice V4t moves the world's CPU data model off the raw 64-bit
|
||||
// ARB_bindless_texture handle and onto GpuTextureSlot, but §5.5.6 closed the
|
||||
// GL re-land of V4c/V4d, so WbDrawDispatcher, EnvCellRenderer,
|
||||
// TerrainModernRenderer and ParticleRenderer still submit through raw GL and
|
||||
// cannot reach FlushBeforeDraw. They therefore call FlushTextureTable and
|
||||
// bind TextureTableGlName at GpuBindingModel.StorageTextureTable themselves,
|
||||
// immediately before their own draws — the same shape their retired private
|
||||
// GlBindlessHandleTable had, against the one table that is now the device's.
|
||||
//
|
||||
// Residency ownership does NOT move. Each world texture is created, made
|
||||
// resident and destroyed by its own cache (TerrainAtlas,
|
||||
// CompositeTextureArrayCache, StandaloneBindlessTextureCache,
|
||||
// ManagedGLTextureArray); this device only owns the table entry, keyed 1:1 by
|
||||
// the caller's already-resident handle. That is what separates this from
|
||||
// RegisterTexture, which owns the residency it creates.
|
||||
//
|
||||
// Deleted with the raw-GL world path when the Vulkan world arm lands, at
|
||||
// which point every one of those caches registers through RegisterTexture.
|
||||
private readonly Dictionary<ulong, GpuTextureSlot> _worldTextureSlotsByHandle = new();
|
||||
|
||||
/// <summary>
|
||||
/// Interns an already-resident world texture handle into the device's table
|
||||
/// and returns its slot. Idempotent: the same handle always resolves to the
|
||||
/// same slot until <see cref="ReleaseWorldTextureHandle"/> retires it, which
|
||||
/// is what lets a cache call this per draw rather than tracking the slot.
|
||||
/// </summary>
|
||||
internal GpuTextureSlot RegisterWorldTextureHandle(ulong residentHandle)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (residentHandle == 0)
|
||||
return GpuTextureSlot.Unassigned;
|
||||
if (_worldTextureSlotsByHandle.TryGetValue(residentHandle, out GpuTextureSlot existing))
|
||||
return existing;
|
||||
|
||||
uint slotIndex = _textureSlotAllocator.Allocate();
|
||||
WriteHandle(slotIndex, residentHandle);
|
||||
var slot = new GpuTextureSlot(slotIndex);
|
||||
_worldTextureSlotsByHandle.Add(residentHandle, slot);
|
||||
return slot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retires the table entry for a world handle the caller is about to make
|
||||
/// non-resident. The slot itself returns to the free list only once the
|
||||
/// retirement queue confirms no submitted frame can still read it, exactly
|
||||
/// as for <see cref="ReleaseTextureSlot"/>. A handle that was never
|
||||
/// registered is a no-op, so a cache may call this unconditionally on its
|
||||
/// teardown path.
|
||||
/// </summary>
|
||||
internal void ReleaseWorldTextureHandle(ulong residentHandle)
|
||||
{
|
||||
if (residentHandle == 0)
|
||||
return;
|
||||
if (!_worldTextureSlotsByHandle.Remove(residentHandle, out GpuTextureSlot slot))
|
||||
return;
|
||||
ReleaseTextureSlot(slot);
|
||||
}
|
||||
|
||||
/// <summary>Live world-handle registrations. Diagnostics and tests only.</summary>
|
||||
internal int WorldTextureSlotCount => _worldTextureSlotsByHandle.Count;
|
||||
|
||||
internal void ApplyRenderState(GlRenderStateSnapshot desired)
|
||||
{
|
||||
GlRenderStateChanges changes = _renderState.Apply(desired);
|
||||
CurrentRenderState = desired;
|
||||
if (!changes.AnyChange)
|
||||
return;
|
||||
|
||||
if (changes.Program)
|
||||
_gl.UseProgram(desired.Program);
|
||||
if (changes.Blend)
|
||||
{
|
||||
if (desired.Blend == GpuBlendMode.None)
|
||||
{
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gl.Enable(EnableCap.Blend);
|
||||
(BlendingFactor source, BlendingFactor destination) = GlEnumMapping.BlendFactorsOf(desired.Blend);
|
||||
_gl.BlendFunc(source, destination);
|
||||
}
|
||||
}
|
||||
if (changes.DepthTest)
|
||||
{
|
||||
if (desired.DepthTest)
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
else
|
||||
_gl.Disable(EnableCap.DepthTest);
|
||||
}
|
||||
if (changes.DepthWrite)
|
||||
_gl.DepthMask(desired.DepthWrite);
|
||||
if (changes.DepthCompare)
|
||||
_gl.DepthFunc(GlEnumMapping.DepthFunctionOf(desired.DepthCompare));
|
||||
if (changes.Cull)
|
||||
{
|
||||
if (desired.Cull == GpuCullMode.None)
|
||||
{
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gl.Enable(EnableCap.CullFace);
|
||||
_gl.CullFace(GlEnumMapping.CullFaceModeOf(desired.Cull));
|
||||
}
|
||||
}
|
||||
if (changes.FrontFace)
|
||||
_gl.FrontFace(GlEnumMapping.FrontFaceDirectionOf(desired.FrontFace));
|
||||
if (changes.AlphaToCoverage)
|
||||
{
|
||||
if (desired.AlphaToCoverage)
|
||||
_gl.Enable(EnableCap.SampleAlphaToCoverage);
|
||||
else
|
||||
_gl.Disable(EnableCap.SampleAlphaToCoverage);
|
||||
}
|
||||
if (changes.ColorWrite)
|
||||
{
|
||||
_gl.ColorMask(desired.ColorWrite, desired.ColorWrite, desired.ColorWrite, desired.ColorWrite);
|
||||
}
|
||||
// Slice V6l. The stencil VALUES are issued whenever the test is on, even
|
||||
// if only the enable changed: GL keeps func/op/mask as global state that a
|
||||
// raw-GL renderer may have moved since this cache last saw it, and the
|
||||
// portal punch's correctness depends on the exact triple it asked for.
|
||||
if (changes.StencilTest)
|
||||
{
|
||||
if (desired.StencilTest)
|
||||
_gl.Enable(EnableCap.StencilTest);
|
||||
else
|
||||
_gl.Disable(EnableCap.StencilTest);
|
||||
}
|
||||
if (desired.StencilTest && (changes.StencilTest || changes.Stencil))
|
||||
{
|
||||
GpuStencilState stencil = desired.Stencil;
|
||||
_gl.StencilFunc(
|
||||
GlEnumMapping.StencilFunctionOf(stencil.Compare),
|
||||
(int)stencil.Reference,
|
||||
stencil.CompareMask);
|
||||
_gl.StencilOp(
|
||||
GlEnumMapping.StencilOpOf(stencil.Fail),
|
||||
GlEnumMapping.StencilOpOf(stencil.DepthFail),
|
||||
GlEnumMapping.StencilOpOf(stencil.Pass));
|
||||
_gl.StencilMask(stencil.WriteMask);
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(_gl, "apply GL render state");
|
||||
}
|
||||
|
||||
public void QueueDeviceAction(Action action)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
_queuedActions.Add(action);
|
||||
}
|
||||
|
||||
public void ProcessDeviceActions()
|
||||
{
|
||||
Action[] pending = [.. _queuedActions];
|
||||
_queuedActions.Clear();
|
||||
foreach (Action action in pending)
|
||||
action();
|
||||
}
|
||||
|
||||
public byte[] CaptureBackbuffer(int width, int height)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height);
|
||||
|
||||
// FrameScreenshotController owns the one backbuffer read in the process:
|
||||
// it names framebuffer 0 rather than inheriting a binding, and it
|
||||
// resolves the multisampled default framebuffer before reading, because
|
||||
// glReadPixels against a multisampled read framebuffer is undefined.
|
||||
// A second implementation here would be a second instrument to keep
|
||||
// sound. FlipRows is the same top-left-origin flip the screenshot gates
|
||||
// rely on, so this seam stays byte-for-byte compatible with them.
|
||||
byte[] pixels = FrameScreenshotController.ReadDefaultFramebuffer(_gl, width, height);
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"capture backbuffer {width}x{height}");
|
||||
return FrameScreenshotController.FlipRows(pixels, width, height);
|
||||
}
|
||||
|
||||
public void WaitIdle()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
_frameFlights.WaitForSubmittedWork();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
|
||||
// Only resources THIS device created are disposed here.
|
||||
// GpuFrameFlightController is constructor-injected and owned by
|
||||
// whoever composed this device — disposing it would be a double-
|
||||
// dispose from that owner's perspective, and every disposal below
|
||||
// routes through it as the retirement queue, so it must still be
|
||||
// alive when this method returns.
|
||||
foreach (GlGpuSampler sampler in _samplers.Values)
|
||||
sampler.Dispose();
|
||||
_samplers.Clear();
|
||||
|
||||
_defaultTexture.Dispose();
|
||||
|
||||
foreach (GlGpuBuffer ringBuffer in _ringBuffers)
|
||||
ringBuffer.Dispose();
|
||||
|
||||
_textureTableBuffer.Dispose();
|
||||
_timerPool.DisposeQueries();
|
||||
}
|
||||
|
||||
private void WriteHandle(uint slot, ulong handle)
|
||||
{
|
||||
_textureHandleTable[slot] = handle;
|
||||
_textureTableDirtySlots.Mark(slot);
|
||||
}
|
||||
|
||||
private GpuCapabilityRecord CaptureCapabilities()
|
||||
{
|
||||
int major = _gl.GetInteger(GetPName.MajorVersion);
|
||||
int minor = _gl.GetInteger(GetPName.MinorVersion);
|
||||
bool openGl43 = major > 4 || (major == 4 && minor >= 3);
|
||||
|
||||
_gl.GetInteger((GetPName)GlMaxShaderStorageBufferBindings, out int maxStorageBindings);
|
||||
_gl.GetInteger((GetPName)GlMaxClipDistances, out int maxClipDistances);
|
||||
_gl.GetInteger((GetPName)GlMaxSamples, out int maxSamples);
|
||||
_gl.GetInteger(GetPName.UniformBufferOffsetAlignment, out int uniformAlignment);
|
||||
_gl.GetInteger((GetPName)GlShaderStorageBufferOffsetAlignment, out int storageAlignment);
|
||||
|
||||
bool timerQuery = major > 3 || (major == 3 && minor >= 3) || _gl.IsExtensionPresent("GL_ARB_timer_query");
|
||||
|
||||
return new GpuCapabilityRecord
|
||||
{
|
||||
Backend = GpuBackendKind.OpenGl,
|
||||
DeviceName = _gl.GetStringS(GLEnum.Renderer),
|
||||
DriverInfo = _gl.GetStringS(GLEnum.Vendor),
|
||||
ApiVersion = $"OpenGL {_gl.GetStringS(GLEnum.Version)}",
|
||||
MaxTextureTableSlots = GpuBindingModel.TextureTableCapacity,
|
||||
MaxStorageBufferBindings = (uint)maxStorageBindings,
|
||||
MaxPushConstantBytes = (uint)GpuBindingModel.MaxPushConstantBytes,
|
||||
MinStorageBufferOffsetAlignment = (uint)storageAlignment,
|
||||
MinUniformBufferOffsetAlignment = (uint)uniformAlignment,
|
||||
MaxClipDistances = (uint)maxClipDistances,
|
||||
MaxSampleCount = (uint)maxSamples,
|
||||
SupportsMultiDrawIndirect = openGl43 || _gl.IsExtensionPresent("GL_ARB_multi_draw_indirect"),
|
||||
SupportsDrawParameters = _gl.IsExtensionPresent("GL_ARB_shader_draw_parameters"),
|
||||
SupportsTextureCompressionBc = _gl.IsExtensionPresent("GL_EXT_texture_compression_s3tc"),
|
||||
SupportsTimestampQueries = timerQuery,
|
||||
// The ring maps and unmaps per flush; it never holds a persistent
|
||||
// mapping a renderer could write into between frames, which is what
|
||||
// this capability advertises. Only Vulkan reports true.
|
||||
SupportsPersistentlyMappedRings = false,
|
||||
};
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// One frame's recording context on the GL backend. Ring allocations are
|
||||
/// forwarded to the device's per-slot <see cref="GlRingBufferState"/>;
|
||||
/// passes are single-level (no nesting, matching every acdream renderer
|
||||
/// today) and enforced here.
|
||||
/// </summary>
|
||||
internal sealed class GlGpuFrame : IGpuFrame
|
||||
{
|
||||
private readonly GlGpuDevice _device;
|
||||
private bool _ended;
|
||||
private GlGpuPassEncoder? _openPass;
|
||||
|
||||
internal GlGpuFrame(GlGpuDevice device, int slotIndex, long serial)
|
||||
{
|
||||
_device = device;
|
||||
SlotIndex = slotIndex;
|
||||
Serial = serial;
|
||||
}
|
||||
|
||||
public int SlotIndex { get; }
|
||||
public long Serial { get; }
|
||||
|
||||
public GpuRingAllocation AllocateRing(int byteCount, GpuRingUsage usage) =>
|
||||
_device.AllocateRing(SlotIndex, byteCount, usage);
|
||||
|
||||
public IGpuPassEncoder BeginPass(GpuPassDescription description)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(description);
|
||||
if (_openPass is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A pass is already open on this frame; dispose it before beginning another.");
|
||||
}
|
||||
|
||||
_device.BeginPass(description);
|
||||
var encoder = new GlGpuPassEncoder(_device, this, description);
|
||||
_openPass = encoder;
|
||||
return encoder;
|
||||
}
|
||||
|
||||
internal void ClosePass(GlGpuPassEncoder encoder)
|
||||
{
|
||||
if (ReferenceEquals(_openPass, encoder))
|
||||
_openPass = null;
|
||||
}
|
||||
|
||||
public void End()
|
||||
{
|
||||
if (_ended)
|
||||
return;
|
||||
_ended = true;
|
||||
_device.EndFrame();
|
||||
}
|
||||
|
||||
public void Dispose() => End();
|
||||
}
|
||||
|
|
@ -1,316 +0,0 @@
|
|||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Records one pass's draw work. Binding calls translate almost mechanically
|
||||
/// to GL (a storage/uniform binding is <c>glBindBufferRange</c>, an indexed
|
||||
/// draw is <c>glDrawElementsInstancedBaseVertexBaseInstance</c>, and so on);
|
||||
/// the two pieces of real logic are the render-state diff applied on
|
||||
/// <see cref="BindPipeline"/> / the dynamic setters, and the "flush dirty
|
||||
/// ring + texture-table bytes immediately before every draw" discipline
|
||||
/// described on <see cref="GlGpuDevice"/>.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Compiles <see cref="GpuPipelineDescription.Shaders"/> through the existing
|
||||
/// <see cref="ShaderProgramConstruction"/> (the same compiler every other GL
|
||||
/// shader in the codebase uses — this is not a second compiler) and owns one
|
||||
/// VAO shaped by <see cref="GpuPipelineDescription.VertexLayout"/>.
|
||||
///
|
||||
/// The VAO only records which attribute locations are enabled and their
|
||||
/// component shape; it does NOT bind a vertex buffer at creation time. Vertex
|
||||
/// attribute pointers are re-issued by <see cref="GlGpuPassEncoder.BindVertexBuffer"/>
|
||||
/// every time a buffer/offset is bound, because a ring allocation's offset
|
||||
/// changes every frame — GL has no notion of "rebase this VAO's buffer at a
|
||||
/// new offset" independent of re-issuing <c>glVertexAttribPointer</c>.
|
||||
/// </summary>
|
||||
internal sealed class GlGpuPipeline : IGpuPipeline
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly IGpuResourceRetirementQueue _retirement;
|
||||
private uint _program;
|
||||
private uint _vertexArray;
|
||||
|
||||
public GlGpuPipeline(GL gl, IGpuResourceRetirementQueue retirement, GpuPipelineDescription description, string vertexSource, string fragmentSource)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
|
||||
Description = description ?? throw new ArgumentNullException(nameof(description));
|
||||
|
||||
_program = ShaderProgramConstruction.Build(new GlShaderProgramBuildApi(_gl), vertexSource, fragmentSource);
|
||||
try
|
||||
{
|
||||
_vertexArray = GlResourceCommand.CreateName(
|
||||
_gl,
|
||||
$"pipeline '{description.Name}' VAO",
|
||||
_gl.GenVertexArray,
|
||||
_gl.DeleteVertexArray);
|
||||
_gl.BindVertexArray(_vertexArray);
|
||||
GpuVertexLayout layout = description.VertexLayout;
|
||||
foreach (GpuVertexAttribute attribute in layout.Attributes)
|
||||
{
|
||||
_gl.EnableVertexAttribArray(attribute.Location);
|
||||
// Slice V6l: the divisor is VAO state and survives every later
|
||||
// glVertexAttribPointer, so it belongs here with the enables
|
||||
// rather than in the per-frame rebind. Zero is the GL default and
|
||||
// is restated explicitly — a VAO name can be recycled by the
|
||||
// driver, and inheriting a stale divisor draws one instance's
|
||||
// data across every vertex.
|
||||
_gl.VertexAttribDivisor(
|
||||
attribute.Location,
|
||||
layout.InputRateOf(attribute.Binding) == GpuVertexInputRate.Instance ? 1u : 0u);
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"configure pipeline '{description.Name}' VAO");
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_gl.DeleteProgram(_program);
|
||||
_program = 0;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public GpuPipelineDescription Description { get; }
|
||||
|
||||
internal uint GlProgram => _program;
|
||||
internal uint GlVertexArray => _vertexArray;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
uint program = _program;
|
||||
uint vertexArray = _vertexArray;
|
||||
if (program == 0 && vertexArray == 0)
|
||||
return;
|
||||
_program = 0;
|
||||
_vertexArray = 0;
|
||||
|
||||
GL gl = _gl;
|
||||
string label = Description.Name;
|
||||
_retirement.Retire(() =>
|
||||
{
|
||||
if (vertexArray != 0)
|
||||
{
|
||||
gl.DeleteVertexArray(vertexArray);
|
||||
GLHelpers.ThrowOnResourceError(gl, $"delete pipeline '{label}' VAO");
|
||||
}
|
||||
if (program != 0)
|
||||
{
|
||||
gl.DeleteProgram(program);
|
||||
GLHelpers.ThrowOnResourceError(gl, $"delete pipeline '{label}' program");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Applies a <see cref="GpuPushConstants"/> value to the currently bound GL
|
||||
/// program by uniform name (<see cref="GlPushConstantUniformNames"/>),
|
||||
/// caching each program's resolved locations so the name lookup only happens
|
||||
/// once per program. A location of -1 (the program does not declare that
|
||||
/// uniform) is silently skipped, exactly as the contract's XML docs specify.
|
||||
/// </summary>
|
||||
internal sealed class GlGpuPushConstantBinder
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly Dictionary<uint, ProgramLocations> _locationsByProgram = new();
|
||||
|
||||
public GlGpuPushConstantBinder(GL gl) => _gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
|
||||
public unsafe void Apply(uint program, in GpuPushConstants constants)
|
||||
{
|
||||
ProgramLocations locations = GetOrResolveLocations(program);
|
||||
|
||||
if (locations.ViewProjection >= 0)
|
||||
{
|
||||
Matrix4x4 m = constants.ViewProjection;
|
||||
_gl.UniformMatrix4(locations.ViewProjection, 1, false, (float*)&m);
|
||||
}
|
||||
if (locations.DrawIdOffset >= 0)
|
||||
_gl.Uniform1(locations.DrawIdOffset, constants.DrawIdOffset);
|
||||
if (locations.LightingMode >= 0)
|
||||
_gl.Uniform1(locations.LightingMode, constants.LightingMode);
|
||||
if (locations.RenderPass >= 0)
|
||||
_gl.Uniform1(locations.RenderPass, constants.RenderPass);
|
||||
if (locations.LightDebug >= 0)
|
||||
_gl.Uniform1(locations.LightDebug, constants.LightDebug);
|
||||
if (locations.TextureIndexA >= 0)
|
||||
_gl.Uniform1(locations.TextureIndexA, constants.TextureIndexA);
|
||||
if (locations.TextureIndexB >= 0)
|
||||
_gl.Uniform1(locations.TextureIndexB, constants.TextureIndexB);
|
||||
if (locations.ParamA >= 0)
|
||||
_gl.Uniform1(locations.ParamA, constants.ParamA);
|
||||
if (locations.ParamB >= 0)
|
||||
_gl.Uniform1(locations.ParamB, constants.ParamB);
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"apply push constants to program {program}");
|
||||
}
|
||||
|
||||
/// <summary>Drops cached locations for a deleted program. Call before its GL name is reused.</summary>
|
||||
public void Forget(uint program) => _locationsByProgram.Remove(program);
|
||||
|
||||
private ProgramLocations GetOrResolveLocations(uint program)
|
||||
{
|
||||
if (_locationsByProgram.TryGetValue(program, out ProgramLocations existing))
|
||||
return existing;
|
||||
|
||||
ProgramLocations locations = new(
|
||||
_gl.GetUniformLocation(program, GlPushConstantUniformNames.ViewProjection),
|
||||
_gl.GetUniformLocation(program, GlPushConstantUniformNames.DrawIdOffset),
|
||||
_gl.GetUniformLocation(program, GlPushConstantUniformNames.LightingMode),
|
||||
_gl.GetUniformLocation(program, GlPushConstantUniformNames.RenderPass),
|
||||
_gl.GetUniformLocation(program, GlPushConstantUniformNames.LightDebug),
|
||||
_gl.GetUniformLocation(program, GlPushConstantUniformNames.TextureIndexA),
|
||||
_gl.GetUniformLocation(program, GlPushConstantUniformNames.TextureIndexB),
|
||||
_gl.GetUniformLocation(program, GlPushConstantUniformNames.ParamA),
|
||||
_gl.GetUniformLocation(program, GlPushConstantUniformNames.ParamB));
|
||||
_locationsByProgram[program] = locations;
|
||||
return locations;
|
||||
}
|
||||
|
||||
private readonly record struct ProgramLocations(
|
||||
int ViewProjection,
|
||||
int DrawIdOffset,
|
||||
int LightingMode,
|
||||
int RenderPass,
|
||||
int LightDebug,
|
||||
int TextureIndexA,
|
||||
int TextureIndexB,
|
||||
int ParamA,
|
||||
int ParamB);
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// An offscreen colour(+depth) FBO. The colour attachment is a
|
||||
/// <see cref="GlGpuTexture"/> so it can be registered into the texture table
|
||||
/// after the pass; the depth/stencil attachment (when requested) is a plain
|
||||
/// renderbuffer, mirroring <c>ManagedGLFramebuffer</c>'s approach — nothing
|
||||
/// ever samples depth for the targets this slice's contract describes
|
||||
/// (paperdoll, creature appraisal, portal masking).
|
||||
/// </summary>
|
||||
internal sealed class GlGpuRenderTarget : IGpuRenderTarget
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly IGpuResourceRetirementQueue _retirement;
|
||||
private uint _framebuffer;
|
||||
private uint _depthStencilRenderbuffer;
|
||||
private GlGpuTexture? _color;
|
||||
|
||||
public GlGpuRenderTarget(GL gl, IGpuResourceRetirementQueue retirement, GpuRenderTargetDescription description)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
|
||||
Description = description;
|
||||
if (description.SampleCount != 1)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"GL render targets are single-sampled in Campaign V slice V1; " +
|
||||
"MSAA offscreen targets are not part of this contract.");
|
||||
}
|
||||
|
||||
_color = new GlGpuTexture(
|
||||
_gl,
|
||||
_retirement,
|
||||
new GpuTextureDescription(
|
||||
$"{description.Name}-color",
|
||||
GpuTextureKind.Texture2D,
|
||||
description.ColorFormat,
|
||||
description.Width,
|
||||
description.Height,
|
||||
LayerCount: 1,
|
||||
MipLevelCount: 1));
|
||||
|
||||
_framebuffer = GlResourceCommand.CreateName(_gl, $"framebuffer '{description.Name}'", _gl.GenFramebuffer, _gl.DeleteFramebuffer);
|
||||
_gl.BindFramebuffer(GLEnum.Framebuffer, _framebuffer);
|
||||
_gl.FramebufferTexture2D(
|
||||
GLEnum.Framebuffer,
|
||||
GLEnum.ColorAttachment0,
|
||||
GLEnum.Texture2D,
|
||||
_color.GlName,
|
||||
0);
|
||||
|
||||
if (description.DepthFormat is not null)
|
||||
{
|
||||
_depthStencilRenderbuffer = GlResourceCommand.CreateName(
|
||||
_gl,
|
||||
$"depth renderbuffer '{description.Name}'",
|
||||
_gl.GenRenderbuffer,
|
||||
_gl.DeleteRenderbuffer);
|
||||
_gl.BindRenderbuffer(GLEnum.Renderbuffer, _depthStencilRenderbuffer);
|
||||
_gl.RenderbufferStorage(GLEnum.Renderbuffer, GLEnum.Depth24Stencil8, (uint)description.Width, (uint)description.Height);
|
||||
_gl.FramebufferRenderbuffer(
|
||||
GLEnum.Framebuffer,
|
||||
GLEnum.DepthStencilAttachment,
|
||||
GLEnum.Renderbuffer,
|
||||
_depthStencilRenderbuffer);
|
||||
}
|
||||
|
||||
GLEnum status = _gl.CheckFramebufferStatus(GLEnum.Framebuffer);
|
||||
_gl.BindFramebuffer(GLEnum.Framebuffer, 0);
|
||||
if (status != GLEnum.FramebufferComplete)
|
||||
{
|
||||
// Roll back the two names this constructor already owns before
|
||||
// surfacing the failure — nothing has been published yet.
|
||||
_gl.DeleteFramebuffer(_framebuffer);
|
||||
if (_depthStencilRenderbuffer != 0)
|
||||
_gl.DeleteRenderbuffer(_depthStencilRenderbuffer);
|
||||
_color.Dispose();
|
||||
throw new InvalidOperationException(
|
||||
$"Render target '{description.Name}' framebuffer is incomplete: {status}.");
|
||||
}
|
||||
}
|
||||
|
||||
public GpuRenderTargetDescription Description { get; }
|
||||
|
||||
public IGpuTexture ColorTexture => _color ?? throw new ObjectDisposedException(Description.Name);
|
||||
|
||||
internal uint GlFramebufferName => _framebuffer;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
uint framebuffer = _framebuffer;
|
||||
uint renderbuffer = _depthStencilRenderbuffer;
|
||||
GlGpuTexture? color = _color;
|
||||
if (framebuffer == 0 && color is null)
|
||||
return;
|
||||
_framebuffer = 0;
|
||||
_depthStencilRenderbuffer = 0;
|
||||
_color = null;
|
||||
|
||||
GL gl = _gl;
|
||||
string label = Description.Name;
|
||||
_retirement.Retire(() =>
|
||||
{
|
||||
if (framebuffer != 0)
|
||||
{
|
||||
gl.DeleteFramebuffer(framebuffer);
|
||||
GLHelpers.ThrowOnResourceError(gl, $"delete framebuffer '{label}'");
|
||||
}
|
||||
if (renderbuffer != 0)
|
||||
{
|
||||
gl.DeleteRenderbuffer(renderbuffer);
|
||||
GLHelpers.ThrowOnResourceError(gl, $"delete depth renderbuffer '{label}'");
|
||||
}
|
||||
});
|
||||
color?.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// One GL sampler object. <see cref="GlGpuDevice.CreateSampler"/>
|
||||
/// de-duplicates by <see cref="GpuSamplerDescription"/> value, so a given
|
||||
/// wrap/filter combination is only ever backed by one physical sampler name —
|
||||
/// the same pattern <c>SamplerCache</c> already uses for its two fixed
|
||||
/// samplers, generalized to the full description space the RHI exposes.
|
||||
/// </summary>
|
||||
internal sealed class GlGpuSampler : IGpuSampler
|
||||
{
|
||||
private const uint GlTextureMaxAnisotropy = 0x84FE;
|
||||
|
||||
private readonly GL _gl;
|
||||
private readonly IGpuResourceRetirementQueue _retirement;
|
||||
private uint _name;
|
||||
|
||||
public GlGpuSampler(GL gl, IGpuResourceRetirementQueue retirement, GpuSamplerDescription description)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
|
||||
Description = description;
|
||||
|
||||
_name = GlResourceCommand.CreateName(_gl, "sampler", _gl.GenSampler, _gl.DeleteSampler);
|
||||
TextureMinFilter minFilter = GlEnumMapping.MinFilterOf(description.MinFilter, description.MipFilter);
|
||||
TextureMagFilter magFilter = GlEnumMapping.MagFilterOf(description.MagFilter);
|
||||
TextureWrapMode wrapU = GlEnumMapping.WrapModeOf(description.AddressU);
|
||||
TextureWrapMode wrapV = GlEnumMapping.WrapModeOf(description.AddressV);
|
||||
|
||||
_gl.SamplerParameter(_name, SamplerParameterI.MinFilter, (int)minFilter);
|
||||
_gl.SamplerParameter(_name, SamplerParameterI.MagFilter, (int)magFilter);
|
||||
_gl.SamplerParameter(_name, SamplerParameterI.WrapS, (int)wrapU);
|
||||
_gl.SamplerParameter(_name, SamplerParameterI.WrapT, (int)wrapV);
|
||||
if (description.MaxAnisotropy > 1f)
|
||||
_gl.SamplerParameter(_name, (SamplerParameterF)GlTextureMaxAnisotropy, description.MaxAnisotropy);
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"configure sampler {_name}");
|
||||
}
|
||||
|
||||
public GpuSamplerDescription Description { get; }
|
||||
|
||||
internal uint GlName => _name;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
uint name = _name;
|
||||
if (name == 0)
|
||||
return;
|
||||
_name = 0;
|
||||
|
||||
GL gl = _gl;
|
||||
_retirement.Retire(() =>
|
||||
{
|
||||
gl.DeleteSampler(name);
|
||||
GLHelpers.ThrowOnResourceError(gl, $"delete sampler {name}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// A GL texture (2D or 2D array) allocated with immutable storage
|
||||
/// (<c>glTexStorage2D/3D</c>) so every mip level is defined the moment the
|
||||
/// object is created, regardless of upload order — mirroring
|
||||
/// <c>ManagedGLTextureArray</c>'s allocation strategy without inheriting its
|
||||
/// Chorizite/<see cref="OpenGLGraphicsDevice"/> coupling.
|
||||
///
|
||||
/// The texture's own min/mag filter is set at creation (not left at GL
|
||||
/// defaults) purely so the texture is "complete" for sampling the instant it
|
||||
/// exists — completeness is judged from the texture object's own filter
|
||||
/// state, independent of whatever <see cref="IGpuSampler"/> a draw later
|
||||
/// binds. The bound sampler object overrides actual filtering at draw time
|
||||
/// (same override rule <c>SamplerCache</c> already documents), so this
|
||||
/// default never affects the rendered image.
|
||||
/// </summary>
|
||||
internal sealed class GlGpuTexture : IGpuTexture
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly IGpuResourceRetirementQueue _retirement;
|
||||
private readonly GlTextureFormatInfo _formatInfo;
|
||||
private readonly GLEnum _target;
|
||||
private uint _name;
|
||||
|
||||
public GlGpuTexture(GL gl, IGpuResourceRetirementQueue retirement, GpuTextureDescription description)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
|
||||
Name = description.Name;
|
||||
Kind = description.Kind;
|
||||
Format = description.Format;
|
||||
Width = description.Width;
|
||||
Height = description.Height;
|
||||
LayerCount = description.LayerCount;
|
||||
MipLevelCount = description.MipLevelCount;
|
||||
_formatInfo = GlGpuTextureFormatMapping.Resolve(Format);
|
||||
_target = Kind == GpuTextureKind.Texture2D ? GLEnum.Texture2D : GLEnum.Texture2DArray;
|
||||
|
||||
_name = GlResourceCommand.CreateName(_gl, $"texture '{Name}'", _gl.GenTexture, _gl.DeleteTexture);
|
||||
_gl.BindTexture(_target, _name);
|
||||
if (Kind == GpuTextureKind.Texture2D)
|
||||
{
|
||||
_gl.TexStorage2D(_target, (uint)MipLevelCount, _formatInfo.SizedInternalFormat, (uint)Width, (uint)Height);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gl.TexStorage3D(
|
||||
_target,
|
||||
(uint)MipLevelCount,
|
||||
_formatInfo.SizedInternalFormat,
|
||||
(uint)Width,
|
||||
(uint)Height,
|
||||
(uint)LayerCount);
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
$"allocate texture '{Name}' {Format} {Width}x{Height}x{LayerCount} ({MipLevelCount} mips)");
|
||||
|
||||
TextureMinFilter minFilter = MipLevelCount > 1 ? TextureMinFilter.LinearMipmapLinear : TextureMinFilter.Linear;
|
||||
_gl.TexParameter(_target, TextureParameterName.TextureMinFilter, (int)minFilter);
|
||||
_gl.TexParameter(_target, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"set default filter for texture '{Name}'");
|
||||
_gl.BindTexture(_target, 0);
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public GpuTextureKind Kind { get; }
|
||||
public GpuTextureFormat Format { get; }
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
public int LayerCount { get; }
|
||||
public int MipLevelCount { get; }
|
||||
|
||||
/// <summary>The physical GL texture name. Used by <see cref="GlGpuDevice.RegisterTexture"/> to obtain a bindless handle.</summary>
|
||||
internal uint GlName => _name;
|
||||
|
||||
internal GLEnum Target => _target;
|
||||
|
||||
public void Upload(int mipLevel, int layer, ReadOnlySpan<byte> data)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (Format == GpuTextureFormat.Depth24Stencil8)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Depth24Stencil8 textures are attachment-only; they are never uploaded from the CPU.");
|
||||
}
|
||||
if ((uint)mipLevel >= (uint)MipLevelCount)
|
||||
throw new ArgumentOutOfRangeException(nameof(mipLevel));
|
||||
if (Kind == GpuTextureKind.Texture2D && layer != 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(layer), "A 2D texture only has layer 0.");
|
||||
if (Kind == GpuTextureKind.Texture2DArray && (uint)layer >= (uint)LayerCount)
|
||||
throw new ArgumentOutOfRangeException(nameof(layer));
|
||||
|
||||
int mipWidth = Math.Max(1, Width >> mipLevel);
|
||||
int mipHeight = Math.Max(1, Height >> mipLevel);
|
||||
long expected = _formatInfo.LayerByteCount(mipWidth, mipHeight);
|
||||
if (data.Length != expected)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Upload for texture '{Name}' mip {mipLevel} has {data.Length} bytes; expected {expected} " +
|
||||
$"for {Format} {mipWidth}x{mipHeight}.",
|
||||
nameof(data));
|
||||
}
|
||||
|
||||
_gl.BindTexture(_target, _name);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* pointer = data)
|
||||
{
|
||||
if (Kind == GpuTextureKind.Texture2D)
|
||||
UploadTexture2D(mipLevel, mipWidth, mipHeight, data.Length, pointer);
|
||||
else
|
||||
UploadTexture2DArray(mipLevel, layer, mipWidth, mipHeight, data.Length, pointer);
|
||||
}
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"upload texture '{Name}' mip {mipLevel} layer {layer}");
|
||||
_gl.BindTexture(_target, 0);
|
||||
}
|
||||
|
||||
private unsafe void UploadTexture2D(int mipLevel, int mipWidth, int mipHeight, int byteCount, byte* pointer)
|
||||
{
|
||||
if (_formatInfo.IsCompressed)
|
||||
{
|
||||
_gl.CompressedTexSubImage2D(
|
||||
_target, mipLevel, 0, 0, (uint)mipWidth, (uint)mipHeight,
|
||||
(InternalFormat)_formatInfo.SizedInternalFormat, (uint)byteCount, pointer);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gl.TexSubImage2D(
|
||||
_target, mipLevel, 0, 0, (uint)mipWidth, (uint)mipHeight,
|
||||
_formatInfo.UploadPixelFormat, _formatInfo.UploadPixelType, pointer);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void UploadTexture2DArray(int mipLevel, int layer, int mipWidth, int mipHeight, int byteCount, byte* pointer)
|
||||
{
|
||||
if (_formatInfo.IsCompressed)
|
||||
{
|
||||
_gl.CompressedTexSubImage3D(
|
||||
_target, mipLevel, 0, 0, layer, (uint)mipWidth, (uint)mipHeight, 1,
|
||||
(InternalFormat)_formatInfo.SizedInternalFormat, (uint)byteCount, pointer);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gl.TexSubImage3D(
|
||||
_target, mipLevel, 0, 0, layer, (uint)mipWidth, (uint)mipHeight, 1,
|
||||
_formatInfo.UploadPixelFormat, _formatInfo.UploadPixelType, pointer);
|
||||
}
|
||||
}
|
||||
|
||||
public void GenerateMipChain()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (MipLevelCount <= 1)
|
||||
return;
|
||||
|
||||
// Matches ManagedGLTextureArray.ProcessDirtyUpdatesInternal: GL's
|
||||
// driver-defined compressed-mip regeneration is the one behaviour this
|
||||
// migration deliberately does not carry into the RHI contract (the
|
||||
// Vulkan backend cannot blit BC images at all and requires a CPU-built
|
||||
// chain instead). Callers that need BC mips must supply every level
|
||||
// through Upload directly; this is a documented no-op for them on GL,
|
||||
// not a silent gap, since the GL renderer path today already skips
|
||||
// glGenerateMipmap for compressed arrays.
|
||||
if (_formatInfo.IsCompressed)
|
||||
return;
|
||||
|
||||
_gl.BindTexture(_target, _name);
|
||||
_gl.GenerateMipmap(_target);
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"generate mip chain for texture '{Name}'");
|
||||
_gl.BindTexture(_target, 0);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
uint name = _name;
|
||||
if (name == 0)
|
||||
return;
|
||||
_name = 0;
|
||||
|
||||
GL gl = _gl;
|
||||
string label = Name;
|
||||
_retirement.Retire(() =>
|
||||
{
|
||||
gl.DeleteTexture(name);
|
||||
GLHelpers.ThrowOnResourceError(gl, $"delete texture '{label}' ({name})");
|
||||
});
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_name == 0)
|
||||
throw new ObjectDisposedException(Name);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// GL format triple for a <see cref="GpuTextureFormat"/>: the sized internal
|
||||
/// format used at allocation (<c>glTexStorage2D/3D</c>), the upload
|
||||
/// format/type for uncompressed uploads, and whether uploads go through
|
||||
/// <c>glCompressedTexSubImage2D/3D</c> instead.
|
||||
///
|
||||
/// <see cref="GpuTextureFormat"/> is acdream's own enum, not the Chorizite
|
||||
/// <c>TextureFormat</c> the existing <c>TextureFormatExtensions</c> targets,
|
||||
/// so this is the "otherwise add a private mapper" fallback the campaign spec
|
||||
/// calls for rather than an extension of that existing type. Pure data —
|
||||
/// safe to unit test without a live GL context, since Silk's GL enums are
|
||||
/// plain value types.
|
||||
/// </summary>
|
||||
internal readonly record struct GlTextureFormatInfo(
|
||||
SizedInternalFormat SizedInternalFormat,
|
||||
PixelFormat UploadPixelFormat,
|
||||
PixelType UploadPixelType,
|
||||
bool IsCompressed,
|
||||
int BlockOrTexelBytes,
|
||||
int BlockDimension)
|
||||
{
|
||||
/// <summary>Bytes required for one full, uncompressed mip layer at the given dimensions, or one compressed layer rounded up to whole blocks.</summary>
|
||||
public long LayerByteCount(int width, int height)
|
||||
{
|
||||
if (!IsCompressed)
|
||||
return (long)width * height * BlockOrTexelBytes;
|
||||
|
||||
int blocksWide = (width + BlockDimension - 1) / BlockDimension;
|
||||
int blocksHigh = (height + BlockDimension - 1) / BlockDimension;
|
||||
return (long)blocksWide * blocksHigh * BlockOrTexelBytes;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class GlGpuTextureFormatMapping
|
||||
{
|
||||
public static GlTextureFormatInfo Resolve(GpuTextureFormat format) => format switch
|
||||
{
|
||||
GpuTextureFormat.Rgba8Unorm or GpuTextureFormat.Rgba8UnormRenderTarget =>
|
||||
new GlTextureFormatInfo(SizedInternalFormat.Rgba8, PixelFormat.Rgba, PixelType.UnsignedByte, false, 4, 1),
|
||||
GpuTextureFormat.R8Unorm =>
|
||||
new GlTextureFormatInfo(SizedInternalFormat.R8, PixelFormat.Red, PixelType.UnsignedByte, false, 1, 1),
|
||||
GpuTextureFormat.Bc1Unorm =>
|
||||
new GlTextureFormatInfo(SizedInternalFormat.CompressedRgbaS3TCDxt1Ext, PixelFormat.Rgba, PixelType.UnsignedByte, true, 8, 4),
|
||||
GpuTextureFormat.Bc2Unorm =>
|
||||
new GlTextureFormatInfo(SizedInternalFormat.CompressedRgbaS3TCDxt3Ext, PixelFormat.Rgba, PixelType.UnsignedByte, true, 16, 4),
|
||||
GpuTextureFormat.Bc3Unorm =>
|
||||
new GlTextureFormatInfo(SizedInternalFormat.CompressedRgbaS3TCDxt5Ext, PixelFormat.Rgba, PixelType.UnsignedByte, true, 16, 4),
|
||||
GpuTextureFormat.Depth24Stencil8 =>
|
||||
new GlTextureFormatInfo(SizedInternalFormat.Depth24Stencil8, PixelFormat.DepthStencil, PixelType.UnsignedInt248, false, 4, 1),
|
||||
_ => throw new NotSupportedException($"No GL format mapping for {format}."),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Seam over the four GL calls a <c>TimeElapsed</c> timer scope needs, mirroring
|
||||
/// <c>IGpuFenceApi</c>'s role for <see cref="GpuFrameFlightController"/>: it lets
|
||||
/// <see cref="GlGpuTimerPool"/>'s double-buffering and result-promotion logic run
|
||||
/// under a unit test with no live GL context.
|
||||
/// </summary>
|
||||
internal interface IGlTimerQueryApi
|
||||
{
|
||||
uint CreateQuery();
|
||||
void DeleteQuery(uint query);
|
||||
void Begin(uint query);
|
||||
void End();
|
||||
|
||||
/// <summary>Non-blocking: false when the result for <paramref name="query"/> is not yet available (or the query was never begun).</summary>
|
||||
bool TryGetResult(uint query, out double milliseconds);
|
||||
}
|
||||
|
||||
internal sealed class SilkGlTimerQueryApi(GL gl) : IGlTimerQueryApi
|
||||
{
|
||||
private readonly GL _gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
|
||||
public uint CreateQuery() => _gl.GenQuery();
|
||||
|
||||
public void DeleteQuery(uint query) => _gl.DeleteQuery(query);
|
||||
|
||||
public void Begin(uint query) => _gl.BeginQuery(QueryTarget.TimeElapsed, query);
|
||||
|
||||
public void End() => _gl.EndQuery(QueryTarget.TimeElapsed);
|
||||
|
||||
public bool TryGetResult(uint query, out double milliseconds)
|
||||
{
|
||||
_gl.GetQueryObject(query, QueryObjectParameterName.ResultAvailable, out int available);
|
||||
if (available == 0)
|
||||
{
|
||||
milliseconds = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
_gl.GetQueryObject(query, QueryObjectParameterName.Result, out ulong elapsedNanoseconds);
|
||||
milliseconds = elapsedNanoseconds / 1_000_000d;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IGpuTimerPool"/> backed by <c>TimeElapsed</c> queries. Core GL
|
||||
/// only allows one <c>GL_TIME_ELAPSED</c> query active at a time (the
|
||||
/// restriction is per target, not per query object), so scopes must not
|
||||
/// nest — <see cref="BeginScope"/> throws if a previous scope in the same
|
||||
/// pass hasn't been disposed yet, exactly as <c>WbDrawDispatcher</c> already
|
||||
/// requires for its own opaque/transparent query pair.
|
||||
///
|
||||
/// Each distinct scope name gets its own double-buffered pair of query
|
||||
/// objects (per the campaign spec: "double-buffered so results are read from
|
||||
/// a retired frame and never block"). A scope's second call reads back the
|
||||
/// FIRST call's result non-blockingly — by the time a scope name repeats,
|
||||
/// at least one full frame has usually retired, so the result is normally
|
||||
/// ready; if it is not, the previous value is kept until it is.
|
||||
/// </summary>
|
||||
internal sealed class GlGpuTimerPool : IGpuTimerPool
|
||||
{
|
||||
private const int BufferDepth = 2;
|
||||
|
||||
private sealed class ScopeState
|
||||
{
|
||||
public readonly uint[] Queries = new uint[BufferDepth];
|
||||
public readonly bool[] Began = new bool[BufferDepth];
|
||||
public int NextSlot;
|
||||
public double LastResolvedMilliseconds;
|
||||
public bool HasResolvedValue;
|
||||
}
|
||||
|
||||
private readonly IGlTimerQueryApi _api;
|
||||
private readonly Dictionary<string, ScopeState> _scopes = new(StringComparer.Ordinal);
|
||||
private string? _activeScopeName;
|
||||
|
||||
public GlGpuTimerPool(IGlTimerQueryApi api, bool isSupported)
|
||||
{
|
||||
_api = api ?? throw new ArgumentNullException(nameof(api));
|
||||
IsSupported = isSupported;
|
||||
}
|
||||
|
||||
public bool IsSupported { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Begins (or continues) the named scope. Returns a disposable that ends
|
||||
/// the query; dispose it before beginning another scope in the same pass.
|
||||
/// </summary>
|
||||
public IDisposable BeginScope(string scopeName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(scopeName);
|
||||
if (!IsSupported)
|
||||
return NullTimerScope.Instance;
|
||||
|
||||
if (_activeScopeName is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"GPU timer scope '{_activeScopeName}' is still active; TimeElapsed queries " +
|
||||
"cannot nest. Dispose the previous scope before beginning another.");
|
||||
}
|
||||
|
||||
if (!_scopes.TryGetValue(scopeName, out ScopeState? state))
|
||||
{
|
||||
state = new ScopeState();
|
||||
for (int i = 0; i < BufferDepth; i++)
|
||||
state.Queries[i] = _api.CreateQuery();
|
||||
_scopes.Add(scopeName, state);
|
||||
}
|
||||
|
||||
int slot = state.NextSlot;
|
||||
state.NextSlot = (slot + 1) % BufferDepth;
|
||||
|
||||
// Poll the OTHER slot's query — the one begun on the previous call to
|
||||
// this scope name — before beginning this one. That is what makes a
|
||||
// scope's Nth call read back the (N-1)th call's result: with only two
|
||||
// slots, "the previous slot" and "the other slot" are the same index,
|
||||
// and by now it has usually had at least one frame to retire.
|
||||
int previousSlot = (slot + BufferDepth - 1) % BufferDepth;
|
||||
if (state.Began[previousSlot] && _api.TryGetResult(state.Queries[previousSlot], out double milliseconds))
|
||||
{
|
||||
state.LastResolvedMilliseconds = milliseconds;
|
||||
state.HasResolvedValue = true;
|
||||
}
|
||||
|
||||
_api.Begin(state.Queries[slot]);
|
||||
state.Began[slot] = true;
|
||||
_activeScopeName = scopeName;
|
||||
return new ActiveTimerScope(this, scopeName);
|
||||
}
|
||||
|
||||
public bool TryResolve(string scopeName, out double milliseconds)
|
||||
{
|
||||
if (_scopes.TryGetValue(scopeName, out ScopeState? state) && state.HasResolvedValue)
|
||||
{
|
||||
milliseconds = state.LastResolvedMilliseconds;
|
||||
return true;
|
||||
}
|
||||
|
||||
milliseconds = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void EndScope(string scopeName)
|
||||
{
|
||||
if (_activeScopeName != scopeName)
|
||||
return;
|
||||
|
||||
_api.End();
|
||||
_activeScopeName = null;
|
||||
}
|
||||
|
||||
internal void DisposeQueries()
|
||||
{
|
||||
foreach (ScopeState state in _scopes.Values)
|
||||
{
|
||||
foreach (uint query in state.Queries)
|
||||
_api.DeleteQuery(query);
|
||||
}
|
||||
_scopes.Clear();
|
||||
}
|
||||
|
||||
private sealed class ActiveTimerScope(GlGpuTimerPool pool, string scopeName) : IDisposable
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
pool.EndScope(scopeName);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NullTimerScope : IDisposable
|
||||
{
|
||||
public static NullTimerScope Instance { get; } = new();
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
using System.Reflection;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// The single source of truth mapping each <see cref="GpuPushConstants"/>
|
||||
/// field to the GLSL uniform name the GL backend binds it to — the names
|
||||
/// documented on the fields themselves. <see cref="GlGpuPushConstantBinder"/>
|
||||
/// reads these constants (not string literals of its own) when it calls
|
||||
/// <c>GL.GetUniformLocation</c>, and <see cref="AssertMapsEveryField"/> lets a
|
||||
/// test prove the table cannot silently drop a field that a later slice adds
|
||||
/// to the shared struct.
|
||||
/// </summary>
|
||||
internal static class GlPushConstantUniformNames
|
||||
{
|
||||
public const string ViewProjection = "uViewProjection";
|
||||
public const string DrawIdOffset = "uDrawIDOffset";
|
||||
public const string LightingMode = "uLightingMode";
|
||||
public const string RenderPass = "uRenderPass";
|
||||
public const string LightDebug = "uLightDebug";
|
||||
public const string TextureIndexA = "uTextureIndexA";
|
||||
public const string TextureIndexB = "uTextureIndexB";
|
||||
public const string ParamA = "uParamA";
|
||||
public const string ParamB = "uParamB";
|
||||
|
||||
/// <summary>
|
||||
/// Field name (as declared on <see cref="GpuPushConstants"/>) to GLSL
|
||||
/// uniform name, built from the same constants the binder applies with —
|
||||
/// so the binder and this completeness table can never disagree.
|
||||
/// </summary>
|
||||
public static IReadOnlyDictionary<string, string> ByFieldName { get; } =
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
[nameof(GpuPushConstants.ViewProjection)] = ViewProjection,
|
||||
[nameof(GpuPushConstants.DrawIdOffset)] = DrawIdOffset,
|
||||
[nameof(GpuPushConstants.LightingMode)] = LightingMode,
|
||||
[nameof(GpuPushConstants.RenderPass)] = RenderPass,
|
||||
[nameof(GpuPushConstants.LightDebug)] = LightDebug,
|
||||
[nameof(GpuPushConstants.TextureIndexA)] = TextureIndexA,
|
||||
[nameof(GpuPushConstants.TextureIndexB)] = TextureIndexB,
|
||||
[nameof(GpuPushConstants.ParamA)] = ParamA,
|
||||
[nameof(GpuPushConstants.ParamB)] = ParamB,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Throws if <see cref="GpuPushConstants"/> declares a public instance
|
||||
/// field this table does not map — the drift guard the campaign spec asks
|
||||
/// for. Called from a unit test, not from production code.
|
||||
/// </summary>
|
||||
internal static void AssertMapsEveryField()
|
||||
{
|
||||
FieldInfo[] fields = typeof(GpuPushConstants).GetFields(
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
foreach (FieldInfo field in fields)
|
||||
{
|
||||
if (!ByFieldName.ContainsKey(field.Name))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"GpuPushConstants.{field.Name} has no mapped GLSL uniform name in " +
|
||||
$"{nameof(GlPushConstantUniformNames)}.");
|
||||
}
|
||||
}
|
||||
|
||||
if (ByFieldName.Count != fields.Length)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(GlPushConstantUniformNames)} maps {ByFieldName.Count} names but " +
|
||||
$"{nameof(GpuPushConstants)} declares {fields.Length} fields — a stale entry " +
|
||||
"survives a field rename or removal.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Everything <see cref="GlGpuPipeline"/> bakes plus the handful of fields
|
||||
/// core Vulkan 1.3 (and this backend) makes dynamic per draw.
|
||||
/// <see cref="Program"/> is the GL program name, included so a pipeline
|
||||
/// switch is itself a tracked dimension.
|
||||
/// </summary>
|
||||
internal readonly record struct GlRenderStateSnapshot(
|
||||
uint Program,
|
||||
GpuBlendMode Blend,
|
||||
bool DepthTest,
|
||||
bool DepthWrite,
|
||||
GpuCompareOp DepthCompare,
|
||||
GpuCullMode Cull,
|
||||
GpuFrontFace FrontFace,
|
||||
bool AlphaToCoverage,
|
||||
bool ColorWrite,
|
||||
bool StencilTest,
|
||||
GpuStencilState Stencil);
|
||||
|
||||
/// <summary>Which GL state calls are needed to move from the previous snapshot to the new one.</summary>
|
||||
internal readonly record struct GlRenderStateChanges(
|
||||
bool Program,
|
||||
bool Blend,
|
||||
bool DepthTest,
|
||||
bool DepthWrite,
|
||||
bool DepthCompare,
|
||||
bool Cull,
|
||||
bool FrontFace,
|
||||
bool AlphaToCoverage,
|
||||
bool ColorWrite,
|
||||
bool StencilTest,
|
||||
bool Stencil)
|
||||
{
|
||||
public bool AnyChange =>
|
||||
Program || Blend || DepthTest || DepthWrite || DepthCompare
|
||||
|| Cull || FrontFace || AlphaToCoverage || ColorWrite
|
||||
|| StencilTest || Stencil;
|
||||
|
||||
/// <summary>Every dimension reported changed — used for the first apply after a reset.</summary>
|
||||
internal static GlRenderStateChanges All { get; } =
|
||||
new(true, true, true, true, true, true, true, true, true, true, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure GL-free state cache: given the previously applied
|
||||
/// <see cref="GlRenderStateSnapshot"/> and a newly desired one, reports which
|
||||
/// dimensions actually changed so <see cref="GlGpuPassEncoder"/> only issues
|
||||
/// the GL calls that matter. Starts with no baseline, so the very first
|
||||
/// <see cref="Apply"/> call always reports every dimension changed — "when in
|
||||
/// doubt, set the state" rather than risk stale driver state from before this
|
||||
/// cache existed (e.g. from a previous pass, or default GL state that may not
|
||||
/// match a pipeline's baked defaults).
|
||||
/// </summary>
|
||||
internal sealed class GlRenderStateCache
|
||||
{
|
||||
private GlRenderStateSnapshot? _last;
|
||||
|
||||
public GlRenderStateChanges Apply(GlRenderStateSnapshot desired)
|
||||
{
|
||||
GlRenderStateSnapshot? previous = _last;
|
||||
_last = desired;
|
||||
|
||||
if (previous is not { } p)
|
||||
return GlRenderStateChanges.All;
|
||||
|
||||
return new GlRenderStateChanges(
|
||||
p.Program != desired.Program,
|
||||
p.Blend != desired.Blend,
|
||||
p.DepthTest != desired.DepthTest,
|
||||
p.DepthWrite != desired.DepthWrite,
|
||||
p.DepthCompare != desired.DepthCompare,
|
||||
p.Cull != desired.Cull,
|
||||
p.FrontFace != desired.FrontFace,
|
||||
p.AlphaToCoverage != desired.AlphaToCoverage,
|
||||
p.ColorWrite != desired.ColorWrite,
|
||||
p.StencilTest != desired.StencilTest,
|
||||
p.Stencil != desired.Stencil);
|
||||
}
|
||||
|
||||
/// <summary>Discards the cached baseline — the next <see cref="Apply"/> reports every dimension changed.</summary>
|
||||
public void Reset() => _last = null;
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Pure bookkeeping for one flight slot's upload ring: an allocation cursor
|
||||
/// that only grows across a frame, a "dirty" watermark tracking the byte range
|
||||
/// written since the last flush, and the flushed high-water mark that makes the
|
||||
/// ring's write path safe to issue unsynchronized.
|
||||
///
|
||||
/// This is deliberately GL-free so it can be unit tested without a live
|
||||
/// context. <see cref="GlGpuFrame"/>'s device owns one instance per flight slot
|
||||
/// and pairs it with a managed staging <c>byte[]</c> and a real GL buffer; the
|
||||
/// staging array receives every <see cref="GpuRingAllocation.Data"/> write, and
|
||||
/// immediately before each <c>Draw</c>/<c>DrawIndexed</c>/
|
||||
/// <c>MultiDrawIndexedIndirect</c> the device writes exactly the dirty range
|
||||
/// into the GL buffer through
|
||||
/// <see cref="GlGpuBuffer.WriteRangeUnsynchronized"/> and calls
|
||||
/// <see cref="TakeDirtyRange"/> to reset the watermark. The allocation cursor
|
||||
/// itself only resets at <see cref="Reset"/> (once per <c>BeginFrame</c>) —
|
||||
/// writes made after a flush simply extend the dirty range again, to be picked
|
||||
/// up by the next flush. This is what makes "flush before every draw, not at
|
||||
/// bind time" correct: a renderer that writes after binding still gets uploaded
|
||||
/// before its draw call runs.
|
||||
///
|
||||
/// <para><b>The forward-only invariant is load-bearing, not incidental.</b> The
|
||||
/// device's per-flush write is mapped with <c>GL_MAP_UNSYNCHRONIZED_BIT</c>,
|
||||
/// which means the driver inserts no wait and the caller is asserting that no
|
||||
/// submitted-and-unfinished draw reads the range. Within a frame that holds
|
||||
/// because <see cref="Allocate"/> only ever hands out bytes at or above the
|
||||
/// cursor, so each flush covers a range strictly above every range already
|
||||
/// flushed. Rather than leave that as an emergent property of the arithmetic,
|
||||
/// <see cref="MarkDirty"/> refuses a write below <see cref="FlushedEndBytes"/>:
|
||||
/// a future change that reused ring bytes mid-frame would fail loudly here
|
||||
/// instead of producing an undefined read on the GPU. Across frames the
|
||||
/// invariant belongs to <c>GpuFrameFlightController</c>, which waits on a
|
||||
/// slot's fence in <c>BeginFrame</c> before this state is <see cref="Reset"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal sealed class GlRingBufferState
|
||||
{
|
||||
private readonly int _capacityBytes;
|
||||
private uint _cursor;
|
||||
private int _dirtyStart = -1;
|
||||
private int _dirtyEnd = -1;
|
||||
private int _flushedEnd;
|
||||
|
||||
public GlRingBufferState(int capacityBytes)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacityBytes);
|
||||
_capacityBytes = capacityBytes;
|
||||
}
|
||||
|
||||
public int CapacityBytes => _capacityBytes;
|
||||
|
||||
/// <summary>Bytes handed out since the last <see cref="Reset"/>.</summary>
|
||||
public uint AllocatedBytes => _cursor;
|
||||
|
||||
/// <summary>
|
||||
/// The exclusive end of the bytes already handed to the GPU this frame.
|
||||
/// Every subsequent write must start at or above it.
|
||||
/// </summary>
|
||||
public int FlushedEndBytes => _flushedEnd;
|
||||
|
||||
/// <summary>
|
||||
/// Reserves <paramref name="byteCount"/> bytes aligned to
|
||||
/// <paramref name="alignmentBytes"/>, returning the aligned offset.
|
||||
/// Throws — rather than truncating — when the request would exceed the
|
||||
/// slot's capacity, because a silently shortened allocation would corrupt
|
||||
/// the frame invisibly.
|
||||
/// </summary>
|
||||
public uint Allocate(int byteCount, uint alignmentBytes)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(byteCount);
|
||||
uint aligned = AlignUp(_cursor, alignmentBytes);
|
||||
long end = (long)aligned + byteCount;
|
||||
if (end > _capacityBytes)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Ring allocation of {byteCount} bytes at aligned offset {aligned} " +
|
||||
$"needs {end} bytes; the ring slot is {_capacityBytes} bytes. " +
|
||||
"Increase the per-slot ring capacity (GlGpuDevice's ringCapacityBytesPerSlot).");
|
||||
}
|
||||
|
||||
_cursor = (uint)end;
|
||||
if (byteCount > 0)
|
||||
MarkDirty((int)aligned, (int)end);
|
||||
return aligned;
|
||||
}
|
||||
|
||||
/// <summary>Starts a new frame: rewinds the allocation cursor. Dirty state is untouched — a flush always runs before this is called.</summary>
|
||||
public void Reset()
|
||||
{
|
||||
_cursor = 0;
|
||||
_dirtyStart = -1;
|
||||
_dirtyEnd = -1;
|
||||
_flushedEnd = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the byte range written since the last flush (start, length),
|
||||
/// or (0, 0) when nothing is dirty, and clears the watermark while
|
||||
/// advancing <see cref="FlushedEndBytes"/> over it.
|
||||
/// </summary>
|
||||
public (int Start, int Length) TakeDirtyRange()
|
||||
{
|
||||
if (_dirtyStart < 0)
|
||||
return (0, 0);
|
||||
|
||||
(int start, int length) = (_dirtyStart, _dirtyEnd - _dirtyStart);
|
||||
_flushedEnd = Math.Max(_flushedEnd, _dirtyEnd);
|
||||
_dirtyStart = -1;
|
||||
_dirtyEnd = -1;
|
||||
return (start, length);
|
||||
}
|
||||
|
||||
public bool HasDirtyBytes => _dirtyStart >= 0;
|
||||
|
||||
/// <summary>
|
||||
/// Records that <c>[start, end)</c> was written. Internal rather than
|
||||
/// private for the same reason <see cref="AlignUp"/> is: the forward-only
|
||||
/// guard below is unreachable through <see cref="Allocate"/> by
|
||||
/// construction, so proving it fires at all needs a direct call.
|
||||
/// </summary>
|
||||
internal void MarkDirty(int start, int end)
|
||||
{
|
||||
if (start < _flushedEnd)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Ring write [{start}, {end}) reaches below the {_flushedEnd} bytes already " +
|
||||
"uploaded this frame. Ring uploads are issued with GL_MAP_UNSYNCHRONIZED_BIT, " +
|
||||
"so rewriting bytes a submitted draw may still be reading is undefined — the " +
|
||||
"allocation cursor must only move forward until Reset.");
|
||||
}
|
||||
|
||||
_dirtyStart = _dirtyStart < 0 ? start : Math.Min(_dirtyStart, start);
|
||||
_dirtyEnd = Math.Max(_dirtyEnd, end);
|
||||
}
|
||||
|
||||
internal static uint AlignUp(uint value, uint alignmentBytes) =>
|
||||
alignmentBytes <= 1 ? value : (value + alignmentBytes - 1) / alignmentBytes * alignmentBytes;
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Pure bump-plus-free-list allocator for the GL texture table (the storage
|
||||
/// buffer of bindless handles at <see cref="GpuBindingModel.StorageTextureTable"/>).
|
||||
///
|
||||
/// GL-free by design: it knows nothing about bindless handles, retirement
|
||||
/// queues, or the SSBO itself. <see cref="GlGpuDevice.RegisterTexture"/> calls
|
||||
/// <see cref="Allocate"/> to get a slot to write a handle into;
|
||||
/// <see cref="GlGpuDevice.ReleaseTextureSlot"/> defers the call to
|
||||
/// <see cref="Release"/> through the device's <c>IGpuResourceRetirementQueue</c>
|
||||
/// so a freed slot is never handed back out while a submitted frame could
|
||||
/// still be reading the old handle at that index.
|
||||
/// </summary>
|
||||
internal sealed class GlTextureSlotAllocator
|
||||
{
|
||||
private readonly uint _capacity;
|
||||
private readonly Stack<uint> _freeList = new();
|
||||
private uint _nextBumpSlot;
|
||||
|
||||
public GlTextureSlotAllocator(uint capacity)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfZero(capacity);
|
||||
_capacity = capacity;
|
||||
}
|
||||
|
||||
/// <summary>Slots currently handed out (bumped or reused) and not yet released.</summary>
|
||||
public int LiveCount => (int)_nextBumpSlot - _freeList.Count;
|
||||
|
||||
public uint Allocate()
|
||||
{
|
||||
if (_freeList.Count > 0)
|
||||
return _freeList.Pop();
|
||||
|
||||
if (_nextBumpSlot >= _capacity)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The GL texture table is exhausted: {_capacity} slots are all live. " +
|
||||
"Every texture cache/atlas must release slots it no longer needs before " +
|
||||
"registering new ones.");
|
||||
}
|
||||
|
||||
return _nextBumpSlot++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a slot to the free list. Callers must only call this once the
|
||||
/// retirement queue confirms no live frame could still reference the slot.
|
||||
/// </summary>
|
||||
public void Release(uint slot)
|
||||
{
|
||||
if (slot >= _nextBumpSlot)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(slot),
|
||||
slot,
|
||||
"Cannot release a slot that was never allocated.");
|
||||
}
|
||||
|
||||
_freeList.Push(slot);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,6 @@ internal enum GpuBackendKind
|
|||
/// <summary>Test double — records calls, owns no driver objects.</summary>
|
||||
Recording,
|
||||
|
||||
/// <summary>OpenGL 4.3 + bindless/MDI. Deleted at Campaign V slice V11.</summary>
|
||||
OpenGl,
|
||||
|
||||
/// <summary>Vulkan 1.3 core.</summary>
|
||||
Vulkan,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue