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:
Erik 2026-07-29 02:19:53 +02:00
parent b70b9832ff
commit 8a7a0837e1
121 changed files with 1243 additions and 19840 deletions

View file

@ -1,198 +0,0 @@
using AcDream.App.Rendering.Gpu.Gl;
namespace AcDream.App.Tests.Rendering.Gpu.Gl;
/// <summary>
/// The texture table's flush is mapped with GL_MAP_UNSYNCHRONIZED_BIT and
/// GL_MAP_INVALIDATE_RANGE_BIT, so a run this tracker reports must contain
/// nothing but slots that were actually written. A run that swallowed a clean
/// slot in between would let the driver discard a live bindless handle while a
/// submitted draw was reading it — invisible in any pixel gate, and exactly the
/// class of fault the ring rewrite exists to remove. These tests pin that.
/// </summary>
public sealed class GlDirtySlotRunsTests
{
[Fact]
public void NothingMarkedYieldsNoRuns()
{
var runs = new GlDirtySlotRuns(16);
Assert.False(runs.HasDirtySlots);
Assert.False(runs.TryTakeNextRun(out _, out _));
}
[Fact]
public void OneMarkedSlotIsOneRunOfOne()
{
var runs = new GlDirtySlotRuns(16);
runs.Mark(7);
Assert.True(runs.HasDirtySlots);
Assert.True(runs.TryTakeNextRun(out uint first, out uint count));
Assert.Equal(7u, first);
Assert.Equal(1u, count);
Assert.False(runs.TryTakeNextRun(out _, out _));
}
[Fact]
public void ConsecutiveSlotsMergeIntoOneRun()
{
var runs = new GlDirtySlotRuns(16);
runs.Mark(4);
runs.Mark(5);
runs.Mark(6);
Assert.True(runs.TryTakeNextRun(out uint first, out uint count));
Assert.Equal(4u, first);
Assert.Equal(3u, count);
Assert.False(runs.TryTakeNextRun(out _, out _));
}
[Fact]
public void AGapBetweenMarkedSlotsSplitsTheRuns()
{
var runs = new GlDirtySlotRuns(64);
runs.Mark(5);
runs.Mark(50);
Assert.True(runs.TryTakeNextRun(out uint firstStart, out uint firstCount));
Assert.Equal(5u, firstStart);
Assert.Equal(1u, firstCount);
Assert.True(runs.TryTakeNextRun(out uint secondStart, out uint secondCount));
Assert.Equal(50u, secondStart);
Assert.Equal(1u, secondCount);
Assert.False(runs.TryTakeNextRun(out _, out _));
}
[Fact]
public void RunsComeBackInAscendingSlotOrderRegardlessOfMarkOrder()
{
var runs = new GlDirtySlotRuns(64);
runs.Mark(40);
runs.Mark(1);
runs.Mark(41);
runs.Mark(20);
runs.Mark(0);
Assert.True(runs.TryTakeNextRun(out uint start, out uint count));
Assert.Equal(0u, start);
Assert.Equal(2u, count);
Assert.True(runs.TryTakeNextRun(out start, out count));
Assert.Equal(20u, start);
Assert.Equal(1u, count);
Assert.True(runs.TryTakeNextRun(out start, out count));
Assert.Equal(40u, start);
Assert.Equal(2u, count);
Assert.False(runs.TryTakeNextRun(out _, out _));
}
[Fact]
public void MarkingTheSameSlotTwiceStillYieldsOneRun()
{
var runs = new GlDirtySlotRuns(16);
runs.Mark(3);
runs.Mark(3);
Assert.True(runs.TryTakeNextRun(out uint start, out uint count));
Assert.Equal(3u, start);
Assert.Equal(1u, count);
Assert.False(runs.TryTakeNextRun(out _, out _));
}
[Fact]
public void DrainingLeavesTheTrackerCleanForTheNextFlush()
{
var runs = new GlDirtySlotRuns(16);
runs.Mark(2);
runs.Mark(9);
while (runs.TryTakeNextRun(out _, out _))
{
}
Assert.False(runs.HasDirtySlots);
runs.Mark(11);
Assert.True(runs.TryTakeNextRun(out uint start, out uint count));
Assert.Equal(11u, start);
Assert.Equal(1u, count);
Assert.False(runs.TryTakeNextRun(out _, out _));
}
[Fact]
public void SlotsMarkedAfterAPartialDrainAreStillReported()
{
var runs = new GlDirtySlotRuns(32);
runs.Mark(4);
runs.Mark(20);
Assert.True(runs.TryTakeNextRun(out uint start, out uint count));
Assert.Equal(4u, start);
Assert.Equal(1u, count);
// A draw between two flushes can register another texture.
runs.Mark(21);
Assert.True(runs.TryTakeNextRun(out start, out count));
Assert.Equal(20u, start);
Assert.Equal(2u, count);
Assert.False(runs.TryTakeNextRun(out _, out _));
}
[Fact]
public void TheLastSlotInTheTableIsAddressable()
{
var runs = new GlDirtySlotRuns(8);
runs.Mark(7);
Assert.True(runs.TryTakeNextRun(out uint start, out uint count));
Assert.Equal(7u, start);
Assert.Equal(1u, count);
}
[Fact]
public void MarkingBeyondCapacityThrows()
{
var runs = new GlDirtySlotRuns(8);
Assert.Throws<ArgumentOutOfRangeException>(() => runs.Mark(8));
}
[Fact]
public void ZeroCapacityIsRejected()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new GlDirtySlotRuns(0));
}
/// <summary>
/// The whole point, stated as one property: every slot a run covers was
/// marked, and every marked slot is covered exactly once.
/// </summary>
[Fact]
public void EveryReportedSlotWasMarkedAndEveryMarkedSlotIsReportedOnce()
{
const int Capacity = 200;
var random = new Random(20260727);
var expected = new HashSet<uint>();
var runs = new GlDirtySlotRuns(Capacity);
for (int i = 0; i < 60; i++)
{
uint slot = (uint)random.Next(Capacity);
expected.Add(slot);
runs.Mark(slot);
}
var reported = new List<uint>();
while (runs.TryTakeNextRun(out uint start, out uint count))
{
for (uint slot = start; slot < start + count; slot++)
reported.Add(slot);
}
Assert.Equal(reported.Count, reported.Distinct().Count());
Assert.Equal(expected.OrderBy(slot => slot), reported);
}
}

View file

@ -1,79 +0,0 @@
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Gl;
using Silk.NET.OpenGL;
namespace AcDream.App.Tests.Rendering.Gpu.Gl;
public sealed class GlEnumMappingTests
{
[Fact]
public void VertexShapesMatchTheDeclaredComponentCounts()
{
Assert.Equal(new GlVertexAttributeShape(1, VertexAttribPointerType.Float, false), GlEnumMapping.VertexShapeOf(GpuVertexFormat.Float1));
Assert.Equal(new GlVertexAttributeShape(2, VertexAttribPointerType.Float, false), GlEnumMapping.VertexShapeOf(GpuVertexFormat.Float2));
Assert.Equal(new GlVertexAttributeShape(3, VertexAttribPointerType.Float, false), GlEnumMapping.VertexShapeOf(GpuVertexFormat.Float3));
Assert.Equal(new GlVertexAttributeShape(4, VertexAttribPointerType.Float, false), GlEnumMapping.VertexShapeOf(GpuVertexFormat.Float4));
Assert.Equal(new GlVertexAttributeShape(4, VertexAttribPointerType.UnsignedByte, true), GlEnumMapping.VertexShapeOf(GpuVertexFormat.UByte4Normalized));
}
[Fact]
public void IndexTypesMapToTheirGlSizesAndEnums()
{
Assert.Equal(DrawElementsType.UnsignedShort, GlEnumMapping.DrawElementsTypeOf(GpuIndexType.UInt16));
Assert.Equal(2, GlEnumMapping.IndexSizeBytesOf(GpuIndexType.UInt16));
Assert.Equal(DrawElementsType.UnsignedInt, GlEnumMapping.DrawElementsTypeOf(GpuIndexType.UInt32));
Assert.Equal(4, GlEnumMapping.IndexSizeBytesOf(GpuIndexType.UInt32));
}
[Fact]
public void BlendFactorsMatchTheDocumentedFormulas()
{
(BlendingFactor source, BlendingFactor destination) straight = GlEnumMapping.BlendFactorsOf(GpuBlendMode.StraightAlpha);
Assert.Equal((BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha), straight);
(BlendingFactor source, BlendingFactor destination) additive = GlEnumMapping.BlendFactorsOf(GpuBlendMode.Additive);
Assert.Equal((BlendingFactor.SrcAlpha, BlendingFactor.One), additive);
}
[Fact]
public void BlendNoneHasNoGlFactors()
{
Assert.Throws<NotSupportedException>(() => GlEnumMapping.BlendFactorsOf(GpuBlendMode.None));
}
[Fact]
public void CullNoneHasNoGlCullFaceMode()
{
Assert.Throws<NotSupportedException>(() => GlEnumMapping.CullFaceModeOf(GpuCullMode.None));
}
[Fact]
public void DepthCompareOpsMapOneToOne()
{
Assert.Equal(DepthFunction.Never, GlEnumMapping.DepthFunctionOf(GpuCompareOp.Never));
Assert.Equal(DepthFunction.Less, GlEnumMapping.DepthFunctionOf(GpuCompareOp.Less));
Assert.Equal(DepthFunction.Lequal, GlEnumMapping.DepthFunctionOf(GpuCompareOp.LessOrEqual));
Assert.Equal(DepthFunction.Equal, GlEnumMapping.DepthFunctionOf(GpuCompareOp.Equal));
Assert.Equal(DepthFunction.Greater, GlEnumMapping.DepthFunctionOf(GpuCompareOp.Greater));
Assert.Equal(DepthFunction.Gequal, GlEnumMapping.DepthFunctionOf(GpuCompareOp.GreaterOrEqual));
Assert.Equal(DepthFunction.Always, GlEnumMapping.DepthFunctionOf(GpuCompareOp.Always));
}
[Fact]
public void FilterCombinationsProduceTheExpectedMinFilter()
{
Assert.Equal(TextureMinFilter.Nearest, GlEnumMapping.MinFilterOf(GpuFilter.Nearest, GpuMipFilter.None));
Assert.Equal(TextureMinFilter.Linear, GlEnumMapping.MinFilterOf(GpuFilter.Linear, GpuMipFilter.None));
Assert.Equal(TextureMinFilter.LinearMipmapLinear, GlEnumMapping.MinFilterOf(GpuFilter.Linear, GpuMipFilter.Linear));
Assert.Equal(TextureMinFilter.NearestMipmapNearest, GlEnumMapping.MinFilterOf(GpuFilter.Nearest, GpuMipFilter.Nearest));
}
[Fact]
public void EveryNonNoneEnumValueResolvesForFrontFaceAndWrapMode()
{
Assert.Equal(FrontFaceDirection.Ccw, GlEnumMapping.FrontFaceDirectionOf(GpuFrontFace.CounterClockwise));
Assert.Equal(FrontFaceDirection.CW, GlEnumMapping.FrontFaceDirectionOf(GpuFrontFace.Clockwise));
Assert.Equal(TextureWrapMode.Repeat, GlEnumMapping.WrapModeOf(GpuAddressMode.Repeat));
Assert.Equal(TextureWrapMode.ClampToEdge, GlEnumMapping.WrapModeOf(GpuAddressMode.ClampToEdge));
}
}

View file

@ -1,75 +0,0 @@
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Gl;
using Silk.NET.OpenGL;
namespace AcDream.App.Tests.Rendering.Gpu.Gl;
public sealed class GlGpuTextureFormatMappingTests
{
[Fact]
public void Rgba8AndItsRenderTargetVariantShareTheSameGlShape()
{
GlTextureFormatInfo plain = GlGpuTextureFormatMapping.Resolve(GpuTextureFormat.Rgba8Unorm);
GlTextureFormatInfo target = GlGpuTextureFormatMapping.Resolve(GpuTextureFormat.Rgba8UnormRenderTarget);
Assert.Equal(plain, target);
Assert.Equal(SizedInternalFormat.Rgba8, plain.SizedInternalFormat);
Assert.Equal(PixelFormat.Rgba, plain.UploadPixelFormat);
Assert.Equal(PixelType.UnsignedByte, plain.UploadPixelType);
Assert.False(plain.IsCompressed);
Assert.Equal(4, plain.LayerByteCount(1, 1));
Assert.Equal(4 * 16 * 16, plain.LayerByteCount(16, 16));
}
[Fact]
public void R8IsOneByteUncompressed()
{
GlTextureFormatInfo info = GlGpuTextureFormatMapping.Resolve(GpuTextureFormat.R8Unorm);
Assert.Equal(SizedInternalFormat.R8, info.SizedInternalFormat);
Assert.False(info.IsCompressed);
Assert.Equal(1, info.LayerByteCount(1, 1));
Assert.Equal(64, info.LayerByteCount(8, 8));
}
[Fact]
public void Bc1IsCompressedFourByFourEightByteBlocks() =>
AssertBcFormat(GpuTextureFormat.Bc1Unorm, expectedBlockBytes: 8);
[Fact]
public void Bc2IsCompressedFourByFourSixteenByteBlocks() =>
AssertBcFormat(GpuTextureFormat.Bc2Unorm, expectedBlockBytes: 16);
[Fact]
public void Bc3IsCompressedFourByFourSixteenByteBlocks() =>
AssertBcFormat(GpuTextureFormat.Bc3Unorm, expectedBlockBytes: 16);
private static void AssertBcFormat(GpuTextureFormat format, int expectedBlockBytes)
{
GlTextureFormatInfo info = GlGpuTextureFormatMapping.Resolve(format);
Assert.True(info.IsCompressed);
Assert.Equal(4, info.BlockDimension);
Assert.Equal(expectedBlockBytes, info.BlockOrTexelBytes);
// A single 4x4 block for a 4x4 texture.
Assert.Equal(expectedBlockBytes, info.LayerByteCount(4, 4));
// Partial blocks round up: a 5x5 texture needs a 2x2 block grid.
Assert.Equal(expectedBlockBytes * 4, info.LayerByteCount(5, 5));
}
[Fact]
public void Depth24Stencil8IsAttachmentShapedNotCompressed()
{
GlTextureFormatInfo info = GlGpuTextureFormatMapping.Resolve(GpuTextureFormat.Depth24Stencil8);
Assert.Equal(SizedInternalFormat.Depth24Stencil8, info.SizedInternalFormat);
Assert.Equal(PixelFormat.DepthStencil, info.UploadPixelFormat);
Assert.Equal(PixelType.UnsignedInt248, info.UploadPixelType);
Assert.False(info.IsCompressed);
}
[Fact]
public void EveryGpuTextureFormatValueResolvesWithoutThrowing()
{
foreach (GpuTextureFormat format in Enum.GetValues<GpuTextureFormat>())
GlGpuTextureFormatMapping.Resolve(format);
}
}

View file

@ -1,123 +0,0 @@
using AcDream.App.Rendering.Gpu.Gl;
namespace AcDream.App.Tests.Rendering.Gpu.Gl;
public sealed class GlGpuTimerPoolTests
{
[Fact]
public void UnsupportedPoolReturnsNoOpScopesAndNeverResolves()
{
var pool = new GlGpuTimerPool(new FakeTimerQueryApi(), isSupported: false);
using (pool.BeginScope("world"))
{
}
Assert.False(pool.TryResolve("world", out _));
}
[Fact]
public void ScopeNamesGetIndependentDoubleBufferedQueries()
{
var api = new FakeTimerQueryApi();
var pool = new GlGpuTimerPool(api, isSupported: true);
using (pool.BeginScope("world"))
{
}
using (pool.BeginScope("ui"))
{
}
Assert.Equal(4, api.CreatedQueryCount);
}
[Fact]
public void NestingAScopeBeforeDisposingThePreviousOneThrows()
{
var pool = new GlGpuTimerPool(new FakeTimerQueryApi(), isSupported: true);
pool.BeginScope("world");
Assert.Throws<InvalidOperationException>(() => pool.BeginScope("ui"));
}
[Fact]
public void AResolvedResultIsPromotedTheNextTimeTheSameScopeBegins()
{
var api = new FakeTimerQueryApi();
var pool = new GlGpuTimerPool(api, isSupported: true);
using (pool.BeginScope("world"))
{
}
// No result available yet — the first slot hasn't been revisited.
Assert.False(pool.TryResolve("world", out _));
api.MakeNextResultReady(milliseconds: 1.5);
using (pool.BeginScope("world"))
{
}
Assert.True(pool.TryResolve("world", out double milliseconds));
Assert.Equal(1.5, milliseconds);
}
[Fact]
public void DisposeQueriesDeletesEveryCreatedQuery()
{
var api = new FakeTimerQueryApi();
var pool = new GlGpuTimerPool(api, isSupported: true);
using (pool.BeginScope("world"))
{
}
pool.DisposeQueries();
Assert.Equal(2, api.DeletedQueryCount);
}
private sealed class FakeTimerQueryApi : IGlTimerQueryApi
{
private uint _nextQuery = 1;
private bool _nextResultReady;
private double _nextResultMilliseconds;
public int CreatedQueryCount { get; private set; }
public int DeletedQueryCount { get; private set; }
public uint CreateQuery()
{
CreatedQueryCount++;
return _nextQuery++;
}
public void DeleteQuery(uint query) => DeletedQueryCount++;
public void Begin(uint query)
{
}
public void End()
{
}
public void MakeNextResultReady(double milliseconds)
{
_nextResultReady = true;
_nextResultMilliseconds = milliseconds;
}
public bool TryGetResult(uint query, out double milliseconds)
{
if (_nextResultReady)
{
milliseconds = _nextResultMilliseconds;
_nextResultReady = false;
return true;
}
milliseconds = 0;
return false;
}
}
}

View file

@ -1,35 +0,0 @@
using AcDream.App.Rendering.Gpu.Gl;
namespace AcDream.App.Tests.Rendering.Gpu.Gl;
public sealed class GlPushConstantUniformNamesTests
{
[Fact]
public void EveryDocumentedFieldMapsToItsDocumentedUniformName()
{
Assert.Equal("uViewProjection", GlPushConstantUniformNames.ByFieldName["ViewProjection"]);
Assert.Equal("uDrawIDOffset", GlPushConstantUniformNames.ByFieldName["DrawIdOffset"]);
Assert.Equal("uLightingMode", GlPushConstantUniformNames.ByFieldName["LightingMode"]);
Assert.Equal("uRenderPass", GlPushConstantUniformNames.ByFieldName["RenderPass"]);
Assert.Equal("uLightDebug", GlPushConstantUniformNames.ByFieldName["LightDebug"]);
Assert.Equal("uTextureIndexA", GlPushConstantUniformNames.ByFieldName["TextureIndexA"]);
Assert.Equal("uTextureIndexB", GlPushConstantUniformNames.ByFieldName["TextureIndexB"]);
Assert.Equal("uParamA", GlPushConstantUniformNames.ByFieldName["ParamA"]);
Assert.Equal("uParamB", GlPushConstantUniformNames.ByFieldName["ParamB"]);
}
[Fact]
public void TableCoversEveryFieldOnTheSharedStructWithNoStaleEntries()
{
// This is the drift guard: if a later slice adds/removes/renames a
// GpuPushConstants field without updating the mapping table, this
// test fails instead of the GL backend silently skipping a uniform.
GlPushConstantUniformNames.AssertMapsEveryField();
}
[Fact]
public void TableHasExactlyNineEntries()
{
Assert.Equal(9, GlPushConstantUniformNames.ByFieldName.Count);
}
}

View file

@ -1,140 +0,0 @@
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Gl;
namespace AcDream.App.Tests.Rendering.Gpu.Gl;
public sealed class GlRenderStateCacheTests
{
private static GlRenderStateSnapshot Default(uint program = 1) => new(
program,
GpuBlendMode.None,
DepthTest: true,
DepthWrite: true,
GpuCompareOp.LessOrEqual,
GpuCullMode.Back,
GpuFrontFace.CounterClockwise,
AlphaToCoverage: false,
ColorWrite: true,
// Slice V6l: the stencil dimension. Off is what every pipeline in the
// tree but the portal depth mask asks for.
StencilTest: false,
GpuStencilState.Default);
[Fact]
public void FirstApplyReportsEveryDimensionChanged()
{
var cache = new GlRenderStateCache();
GlRenderStateChanges changes = cache.Apply(Default());
Assert.Equal(GlRenderStateChanges.All, changes);
Assert.True(changes.AnyChange);
}
[Fact]
public void ReapplyingTheIdenticalSnapshotReportsNoChanges()
{
var cache = new GlRenderStateCache();
GlRenderStateSnapshot snapshot = Default();
cache.Apply(snapshot);
GlRenderStateChanges changes = cache.Apply(snapshot);
Assert.False(changes.AnyChange);
}
[Fact]
public void ChangingOnlyCullModeReportsOnlyThatDimension()
{
var cache = new GlRenderStateCache();
cache.Apply(Default());
GlRenderStateChanges changes = cache.Apply(Default() with { Cull = GpuCullMode.None });
Assert.True(changes.Cull);
Assert.True(changes.AnyChange);
Assert.False(changes.Program);
Assert.False(changes.Blend);
Assert.False(changes.DepthTest);
Assert.False(changes.DepthWrite);
Assert.False(changes.DepthCompare);
Assert.False(changes.FrontFace);
Assert.False(changes.AlphaToCoverage);
Assert.False(changes.ColorWrite);
Assert.False(changes.StencilTest);
Assert.False(changes.Stencil);
}
[Fact]
public void ChangingOnlyTheStencilValuesReportsOnlyThatDimension()
{
// Slice V6l. #117's portal punch changes compare, ops, reference and
// masks between its mark pass and its punch pass while the test stays
// enabled, so the two stencil dimensions have to move independently.
var cache = new GlRenderStateCache();
cache.Apply(Default() with { StencilTest = true });
GlRenderStateChanges changes = cache.Apply(Default() with
{
StencilTest = true,
Stencil = GpuStencilState.Default with
{
Compare = GpuCompareOp.Equal,
Pass = GpuStencilOp.Zero,
Reference = 1,
},
});
Assert.True(changes.Stencil);
Assert.False(changes.StencilTest);
Assert.False(changes.DepthTest);
Assert.False(changes.ColorWrite);
}
[Fact]
public void ChangingTheProgramReportsProgramChangedEvenWhenEveryOtherFieldMatches()
{
var cache = new GlRenderStateCache();
cache.Apply(Default(program: 1));
GlRenderStateChanges changes = cache.Apply(Default(program: 2));
Assert.True(changes.Program);
Assert.False(changes.Blend);
}
[Fact]
public void ResetForcesTheNextApplyToReportEveryDimensionAgain()
{
var cache = new GlRenderStateCache();
GlRenderStateSnapshot snapshot = Default();
cache.Apply(snapshot);
cache.Reset();
GlRenderStateChanges changes = cache.Apply(snapshot);
Assert.Equal(GlRenderStateChanges.All, changes);
}
[Fact]
public void MultipleDimensionsChangingAreAllReported()
{
var cache = new GlRenderStateCache();
cache.Apply(Default());
GlRenderStateChanges changes = cache.Apply(Default() with
{
Blend = GpuBlendMode.StraightAlpha,
DepthWrite = false,
FrontFace = GpuFrontFace.Clockwise,
});
Assert.True(changes.Blend);
Assert.True(changes.DepthWrite);
Assert.True(changes.FrontFace);
Assert.False(changes.Cull);
Assert.False(changes.DepthTest);
Assert.False(changes.DepthCompare);
Assert.False(changes.AlphaToCoverage);
Assert.False(changes.ColorWrite);
Assert.False(changes.Program);
}
}

View file

@ -1,182 +0,0 @@
using AcDream.App.Rendering.Gpu.Gl;
namespace AcDream.App.Tests.Rendering.Gpu.Gl;
public sealed class GlRingBufferStateTests
{
[Fact]
public void FirstAllocationStartsAtZero()
{
var state = new GlRingBufferState(1024);
uint offset = state.Allocate(64, alignmentBytes: 16);
Assert.Equal(0u, offset);
Assert.Equal(64u, state.AllocatedBytes);
}
[Fact]
public void SubsequentAllocationsAlignUpToTheRequestedBoundary()
{
var state = new GlRingBufferState(1024);
state.Allocate(12, alignmentBytes: 4);
uint second = state.Allocate(64, alignmentBytes: 256);
Assert.Equal(0u, second % 256);
Assert.True(second >= 12);
}
[Fact]
public void AllocationsMergeIntoOneDirtyRange()
{
var state = new GlRingBufferState(1024);
state.Allocate(16, alignmentBytes: 4);
state.Allocate(32, alignmentBytes: 4);
(int start, int length) = state.TakeDirtyRange();
Assert.Equal(0, start);
Assert.Equal(48, length);
}
[Fact]
public void TakingTheDirtyRangeClearsItUntilTheNextWrite()
{
var state = new GlRingBufferState(1024);
state.Allocate(16, alignmentBytes: 4);
state.TakeDirtyRange();
(int start, int length) = state.TakeDirtyRange();
Assert.Equal(0, start);
Assert.Equal(0, length);
Assert.False(state.HasDirtyBytes);
}
[Fact]
public void WritesAfterAFlushExtendANewDirtyRangeWithoutRewindingTheCursor()
{
var state = new GlRingBufferState(1024);
state.Allocate(16, alignmentBytes: 4);
state.TakeDirtyRange();
uint secondOffset = state.Allocate(8, alignmentBytes: 4);
Assert.Equal(16u, secondOffset);
(int start, int length) = state.TakeDirtyRange();
Assert.Equal(16, start);
Assert.Equal(8, length);
}
/// <summary>
/// The precondition for issuing the ring's writes with
/// GL_MAP_UNSYNCHRONIZED_BIT: within one frame, no flush may cover a byte an
/// earlier flush already handed to the GPU. This walks a frame's worth of
/// mixed-alignment allocations and asserts the ranges are disjoint and
/// ascending.
/// </summary>
[Fact]
public void SuccessiveDirtyRangesWithinAFrameNeverOverlap()
{
var state = new GlRingBufferState(64 * 1024);
int previousEnd = 0;
foreach (int size in new[] { 100, 4, 1024, 36, 7, 512, 3 })
{
state.Allocate(size, alignmentBytes: 256);
state.Allocate(size, alignmentBytes: 4);
(int start, int length) = state.TakeDirtyRange();
Assert.True(
start >= previousEnd,
$"flush [{start}, {start + length}) reaches below the already-flushed {previousEnd} bytes");
previousEnd = start + length;
Assert.Equal(previousEnd, state.FlushedEndBytes);
}
}
[Fact]
public void AWriteBelowTheFlushedHighWaterMarkIsRefused()
{
var state = new GlRingBufferState(1024);
state.Allocate(128, alignmentBytes: 4);
state.TakeDirtyRange();
Assert.Equal(128, state.FlushedEndBytes);
InvalidOperationException error = Assert.Throws<InvalidOperationException>(
() => state.MarkDirty(64, 96));
Assert.Contains("GL_MAP_UNSYNCHRONIZED_BIT", error.Message, StringComparison.Ordinal);
}
[Fact]
public void FlushedHighWaterMarkOnlyAdvancesWhenSomethingWasFlushed()
{
var state = new GlRingBufferState(1024);
state.Allocate(48, alignmentBytes: 4);
state.TakeDirtyRange();
Assert.Equal(48, state.FlushedEndBytes);
// A draw with nothing newly written must not move the mark, or the next
// allocation's guard would compare against a number no flush produced.
state.TakeDirtyRange();
Assert.Equal(48, state.FlushedEndBytes);
}
[Fact]
public void ResetClearsTheFlushedHighWaterMarkSoTheSlotIsWritableAgain()
{
var state = new GlRingBufferState(1024);
state.Allocate(256, alignmentBytes: 4);
state.TakeDirtyRange();
Assert.Equal(256, state.FlushedEndBytes);
// BeginFrame has waited on this slot's fence by the time Reset runs, so
// the whole slot is writable from zero again.
state.Reset();
Assert.Equal(0, state.FlushedEndBytes);
Assert.Equal(0u, state.Allocate(64, alignmentBytes: 4));
}
[Fact]
public void ResetRewindsTheCursorAndClearsDirtyState()
{
var state = new GlRingBufferState(1024);
state.Allocate(64, alignmentBytes: 4);
state.Reset();
Assert.Equal(0u, state.AllocatedBytes);
Assert.False(state.HasDirtyBytes);
Assert.Equal(0u, state.Allocate(4, alignmentBytes: 4));
}
[Fact]
public void OverCapacityRequestThrowsRatherThanTruncating()
{
var state = new GlRingBufferState(64);
Assert.Throws<InvalidOperationException>(() => state.Allocate(128, alignmentBytes: 4));
}
[Fact]
public void AlignmentPushingPastCapacityAlsoThrows()
{
var state = new GlRingBufferState(64);
state.Allocate(60, alignmentBytes: 4);
Assert.Throws<InvalidOperationException>(() => state.Allocate(8, alignmentBytes: 4));
}
[Theory]
[InlineData(0u, 256u, 0u)]
[InlineData(1u, 256u, 256u)]
[InlineData(255u, 256u, 256u)]
[InlineData(256u, 256u, 256u)]
[InlineData(10u, 1u, 10u)]
public void AlignUpMatchesStandardAlignmentArithmetic(uint value, uint alignment, uint expected)
{
Assert.Equal(expected, GlRingBufferState.AlignUp(value, alignment));
}
[Fact]
public void ZeroByteAllocationDoesNotMarkAnythingDirty()
{
var state = new GlRingBufferState(1024);
state.Allocate(0, alignmentBytes: 4);
Assert.False(state.HasDirtyBytes);
}
}

View file

@ -1,60 +0,0 @@
using AcDream.App.Rendering.Gpu.Gl;
namespace AcDream.App.Tests.Rendering.Gpu.Gl;
public sealed class GlTextureSlotAllocatorTests
{
[Fact]
public void AllocationsBumpSequentiallyFromZero()
{
var allocator = new GlTextureSlotAllocator(capacity: 4);
Assert.Equal(0u, allocator.Allocate());
Assert.Equal(1u, allocator.Allocate());
Assert.Equal(2u, allocator.Allocate());
Assert.Equal(3, allocator.LiveCount);
}
[Fact]
public void ExhaustingCapacityThrows()
{
var allocator = new GlTextureSlotAllocator(capacity: 2);
allocator.Allocate();
allocator.Allocate();
Assert.Throws<InvalidOperationException>(() => allocator.Allocate());
}
[Fact]
public void ReleasedSlotsAreReusedBeforeBumpingFurther()
{
var allocator = new GlTextureSlotAllocator(capacity: 4);
uint first = allocator.Allocate();
allocator.Allocate();
allocator.Release(first);
Assert.Equal(1, allocator.LiveCount);
uint reused = allocator.Allocate();
Assert.Equal(first, reused);
Assert.Equal(2, allocator.LiveCount);
}
[Fact]
public void ReleasingAnUnallocatedSlotThrows()
{
var allocator = new GlTextureSlotAllocator(capacity: 4);
allocator.Allocate();
Assert.Throws<ArgumentOutOfRangeException>(() => allocator.Release(3));
}
[Fact]
public void FreeingThenExhaustingTheBumpRangeStillThrowsPastCapacity()
{
var allocator = new GlTextureSlotAllocator(capacity: 2);
uint first = allocator.Allocate();
allocator.Allocate();
allocator.Release(first);
allocator.Allocate();
Assert.Throws<InvalidOperationException>(() => allocator.Allocate());
}
}