feat(render): Vulkan campaign V11 step 3 — drop the GL packages and shaders

Commit 2 deleted the GL rendering backend's implementations; this step
removes the package references and shader vocabulary they leave behind,
so nothing in the App project still spells Silk.NET.OpenGL.

Silk.NET.OpenGL and Silk.NET.OpenGL.Extensions.ARB are dropped from
AcDream.App.csproj. Chorizite.Core stays — the audit is NOT clean: its
Render.Enums (TextureFormat, BufferUsage) and Lib.BoundingBox types are
used directly and extensively across the Wb texture/mesh pipeline,
independent of the deleted GL IUniformBuffer implementers the package
comment used to cite. The stale comment is corrected in place.

IMeshPipelineDevice.Gl is removed along with the GL? gl parameter
threaded through WbMeshAdapter's four constructors, WorldRenderComposition's
CreateMeshAdapter, and VulkanMeshPipelineDevice's Gl => null
implementation — nothing read any of them once the legacy per-mesh
upload bodies were gone (confirmed by grep: the sole non-doc-comment hit
was a test assertion). While in WbMeshAdapter.Dispose(), found and fixed
a real bug along the way: its teardown still pattern-matched the deleted
GL GpuFrameFlightController to decide whether to wait for submitted work,
which VulkanFrameFlightController replaced at slice V6a without this site
being updated — so the wait had been silently dead on every Vulkan run
since then. Retargeted to VulkanFrameFlightController, which carries the
same WaitForSubmittedWork().

The GL pixel-format vocabulary (Silk.NET.OpenGL.PixelFormat/PixelType) that
WorldTextureArray/TextureFormatExtensions/TextureAtlasManager used for
upload validation is replaced by AcDream.Content's existing Silk.NET-free
UploadPixelFormat/UploadPixelType enums (added at MP1a to keep the bake
tool GL-free); two new members (Rgb, Red, Float) extend that enum with
their GL ABI constants to cover the full vocabulary WorldTextureArray
needs, since MP1a's original set only covered what the extractor itself
emits. ObjectMeshManager's App-boundary cast
`(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` becomes a direct
pass-through now that both sides share the type.

GpuBindingModel.StorageTextureTable (the GL-only binding=9 emulation of
the Vulkan texture table) is deleted and StorageBindingCount drops from
10 to 9; the descriptor-set-layout code that builds from that count
(VulkanPipelineLayouts, VulkanFrameBindings) is untouched and just
allocates one fewer always-dummy-seeded, always-unused binding.

Several fully dead GL-only classes came along for the ride, confirmed by
zero construction sites: SilkFramebufferViewportTarget
(NullFramebufferViewportTarget is the sole production
IFramebufferViewportTarget), SilkRenderGlStateReader
(NullRenderGlStateReader.Instance is the sole IRenderGlStateReader),
RuntimeRenderFrameClearPhase (VulkanRenderFrameClearPhase is the sole
IRenderFrameClearPhase, expressing the same atmosphere-clear logic as a
pass load-op instead), and GpuFrameTimer plus FrameProfiler's
GL-owning FrameBoundary(GL) overload and BeginGpuFrame/EndGpuFrame
bracket (RecordGpuSample is the only GPU-timing path any backend uses
now — the ACDREAM_WB_DIAG nested-query exclusion these existed for no
longer applies, since WbDrawDispatcher's own diagnostic GPU sampling
already moved to the device's Vulkan timer pool). GpuFrameFlightController
itself stays (never constructed with a real fence API in production, but
its retirement-ledger/serial-ring logic is backend-neutral and still
covered by its own unit tests) — only its GL-specific parts (the public
GL constructor overload, SilkGpuFenceApi) are deleted, since removing the
whole class would mean restructuring the frozen Slice-8 composition
shape's GpuFrameFlightController? threading, which is out of this
commit's scope. TextureParameters.cs and BufferUsageExtensions.cs
(zero callers each) are deleted outright.

common.glsl is deleted: nothing in the actual Vulkan .spv build reads
it. tools/ShaderCompiler/Program.cs compiles each .vert/.frag pair
directly and tools/ShaderCompiler/VulkanGlslPreamble.cs injects its own
complete self-contained preamble per file; common.glsl's textual
concatenation was exclusively Shader.cs's GL-only mechanism, deleted at
Commit 2. The five shader files that named it in comments
(mesh_modern.vert, particle.vert, particle.frag, sky.frag,
terrain_modern.frag) are corrected to point at VulkanGlslPreamble.cs
instead. mesh.vert/mesh.frag — the pre-N.5 legacy shader pair the
mandatory modern path already made unreachable, with zero C# consumers
and no compiled .spv — are deleted too. Regenerated via
tools/compile-shaders.ps1: 9/9 remaining shader pairs compile
(previously 9/10, with mesh the sole failure — the VulkanShaderManifestTests
doc comment's "nine of ten are not Vulkan-expressible" was already
stale before this commit).

Test fallout: dead-subject test methods/files are deleted rather than
patched (TextRendererFailureSafetyTests.cs, ClipFrameUploadTests.cs,
GpuResourceRetirementTransactionTests.cs's GL queue tests, one
WorldRenderDiagnosticsTests source-order test, one
RenderFrameResourceControllerTests clear-phase-order test); tests whose
subject moved or was renamed are updated in place rather than deleted
(GpuContractTests, VulkanCapabilityGateTests, MeshPipelineDeviceSeamTests'
pinned seven-member surface now reads six, ParticleBindlessInstanceTests'
cross-dialect check now covers the one surviving dialect,
WbMeshAdapterTests' misleadingly-named null-gl test — gpuDevice was
always the parameter that actually threw).

Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors,
with the Silk.NET.OpenGL/.Extensions.ARB package references physically
removed from the csproj (not just unreferenced in code).
Tests: full-solution `dotnet test` green across every project.
Zero remaining `using Silk.NET.OpenGL` anywhere in src/ or tests/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-29 02:58:15 +02:00
parent 8a7a0837e1
commit 7a0227c12e
44 changed files with 300 additions and 977 deletions

View file

@ -5,7 +5,6 @@ using System.Globalization;
using System.IO;
using System.Text;
using AcDream.Core.Rendering;
using Silk.NET.OpenGL;
namespace AcDream.App.Diagnostics;
@ -30,7 +29,8 @@ public enum FrameStage
/// <c>names</c> array) — if <see cref="FrameStage"/> grows, extend this
/// record, <see cref="FrameProfiler.WriteHistoryCsv"/>, and the CSV header
/// together. <c>GpuUs</c> is <c>-1</c> for a frame with no available GPU
/// sample (warm-up, or <c>ACDREAM_WB_DIAG=1</c> self-disable).
/// sample yet (warm-up: the Vulkan arm's timestamp samples resolve two or
/// three frames late).
/// </summary>
internal readonly record struct FrameHistoryRecord(
int FrameIndex,
@ -48,17 +48,22 @@ internal readonly record struct FrameHistoryRecord(
/// <c>FrameBoundary</c> call at the top of the accepted render transaction
/// measures CPU frame time as the delta between consecutive boundaries
/// (captures the FULL frame including present) and samples per-frame allocated
/// bytes + GC collection counts. <see cref="BeginGpuFrame"/> and
/// <see cref="EndGpuFrame"/> separately bracket only submitted render work.
/// Stage scopes
/// bytes + GC collection counts. <see cref="RecordGpuSample"/> separately
/// reports each backend-timed GPU sample as it resolves. Stage scopes
/// (<see cref="BeginStage"/>) attribute CPU time to Update / Upload /
/// ImGui. Emits one <c>[frame-prof]</c> line every ~5 s while
/// <see cref="RenderingDiagnostics.FrameProfEnabled"/> is true; costs one
/// bool check per frame when off.
///
/// <para>Permanent apparatus — every MP-track gate reads it; do not strip.
/// Whole-frame GPU timing self-disables under <c>ACDREAM_WB_DIAG=1</c>
/// (nested TimeElapsed is illegal GL; see GpuFrameTimer).</para>
/// Campaign V slice V11 deleted the GL arm's own <c>TimeElapsed</c> query ring
/// (<c>GpuFrameTimer</c>), the GL-owning <c>FrameBoundary</c> overload it fed,
/// and the <c>ACDREAM_WB_DIAG=1</c> self-disable that existed only to avoid
/// two simultaneously active GL queries (that env var no longer touches this
/// profiler at all — the WB diagnostic's own GPU samples come from the
/// device's Vulkan timer pool now, not a nested raw-GL query, so the two were
/// never in conflict here to begin with). Every backend reports GPU time
/// through <see cref="RecordGpuSample"/> instead.</para>
///
/// <para>2026-07-24 measurement-tooling review — the aggregated report
/// resets its ring buffers every ~5 s (<see cref="FrameStatsBuffer.Reset"/>),
@ -91,14 +96,11 @@ public sealed class FrameProfiler : IDisposable
private readonly FrameStatsBuffer[] _stageUs;
private readonly long[] _stageAccumTicks;
private readonly long[] _lastStageUs;
private readonly bool _wbDiagActive =
Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG") == "1";
private readonly List<FrameHistoryRecord>? _history;
private readonly long _profilerStartTimestamp;
private readonly DateTime _profilerStartUtc;
private int _currentFrameIndex = -1;
private GpuFrameTimer? _gpuTimer;
private bool _externalGpuActive;
private long _lastBoundaryTimestamp;
private long _lastAllocBytes;
@ -107,7 +109,6 @@ public sealed class FrameProfiler : IDisposable
private int _framesInWindow;
private int _ownerThreadId;
private bool _threadWarned;
private bool _wbDiagNoticePrinted;
private bool _wasEnabled;
/// <summary>Most recent immutable report line, for explicit automation checkpoints.</summary>
@ -115,9 +116,10 @@ public sealed class FrameProfiler : IDisposable
/// <summary>
/// Index of the frame currently being measured, or -1 before the first
/// enabled boundary. Campaign V slice V8: a backend whose GPU timer is not
/// GL's query ring pairs its delayed samples with this index, exactly as
/// <see cref="GpuFrameTimer"/> does internally on the GL arm.
/// enabled boundary. Campaign V slice V8: a backend whose GPU timer
/// resolves samples late (Vulkan's timestamp queries) pairs its delayed
/// samples with this index — the same pairing the deleted GL query ring
/// (<c>GpuFrameTimer</c>) used to do internally.
/// </summary>
public int CurrentFrameIndex => _currentFrameIndex;
@ -134,36 +136,21 @@ public sealed class FrameProfiler : IDisposable
}
/// <summary>
/// Call once at the accepted render-transaction boundary, before
/// <see cref="BeginGpuFrame"/>.
/// Call once at the accepted render-transaction boundary. Campaign V
/// slice V11 deleted the GL-owning overload this used to have alongside
/// it (GL is gone; there is no other backend that measures GPU time by
/// owning a query ring from inside this call) — every backend now drives
/// this same no-argument boundary and feeds GPU time in separately through
/// <see cref="RecordGpuSample"/>, so a perf gate that compared two
/// differently-measured numbers never had anything to compare.
/// </summary>
public void FrameBoundary(GL gl) => FrameBoundary(gl, ownsGpuTimer: true);
/// <summary>
/// Campaign V slice V8: the same CPU/allocation boundary with no GL context.
/// The Vulkan arm has no <c>TimeElapsed</c> query ring to own, so it drives
/// this overload and feeds GPU time in through
/// <see cref="RecordGpuSample"/>. Everything else — the CPU delta, the
/// per-thread allocation delta, the stage buffers, the history row and the
/// five-second report — is the identical code path GL uses, because a
/// perf gate that compared two different instruments would compare nothing.
/// </summary>
public void FrameBoundary() => FrameBoundary(null, ownsGpuTimer: false);
private void FrameBoundary(GL? gl, bool ownsGpuTimer)
public void FrameBoundary()
{
bool enabled = RenderingDiagnostics.FrameProfEnabled;
if (!enabled)
{
if (_wasEnabled)
{
// Dispose (not just Stop) so a later re-enable rebuilds the
// query ring fresh — a kept instance would poll slots left
// pending from BEFORE the pause and report temporally stale
// GPU samples. Safe here: this runs at the top of OnRender
// with the GL context current.
_gpuTimer?.Dispose();
_gpuTimer = null;
_wasEnabled = false;
_lastBoundaryTimestamp = 0;
_currentFrameIndex = -1;
@ -193,14 +180,7 @@ public sealed class FrameProfiler : IDisposable
_lastReportTicks = DateTime.UtcNow.Ticks;
Array.Clear(_stageAccumTicks);
_gc0Base = GC.CollectionCount(0); _gc1Base = GC.CollectionCount(1); _gc2Base = GC.CollectionCount(2);
if (ownsGpuTimer && gl is not null && _gpuTimer is null && !_wbDiagActive)
_gpuTimer = new GpuFrameTimer(gl);
_currentFrameIndex = 0;
if (_wbDiagActive && !_wbDiagNoticePrinted)
{
_wbDiagNoticePrinted = true;
Console.WriteLine("[frame-prof] GPU frame timing OFF: ACDREAM_WB_DIAG=1 owns TimeElapsed queries (nested queries are illegal GL)");
}
}
else
{
@ -242,7 +222,7 @@ public sealed class FrameProfiler : IDisposable
int gc1 = GC.CollectionCount(1) - _gc1Base;
int gc2 = GC.CollectionCount(2) - _gc2Base;
LastReport = FormatReport(_framesInWindow, _cpuUs, _gpuUs,
gpuActive: _gpuTimer is not null || _externalGpuActive,
gpuActive: _externalGpuActive,
_allocBytes, gc0, gc1, gc2, _stageUs);
Console.WriteLine(LastReport);
_lastReportTicks = nowTicks;
@ -253,45 +233,15 @@ public sealed class FrameProfiler : IDisposable
}
}
/// <summary>
/// Begin GPU timing immediately before render-resource, world, and private
/// presentation submission. Completed delayed samples are associated with
/// their owning history row rather than the frame that happened to poll
/// them.
/// </summary>
public void BeginGpuFrame()
{
if (!_wasEnabled || _gpuTimer is null || _currentFrameIndex < 0)
return;
Span<GpuFrameSample> completed = stackalloc GpuFrameSample[4];
int completedCount = _gpuTimer.BeginFrame(_currentFrameIndex, completed);
for (int index = 0; index < completedCount; index++)
{
GpuFrameSample sample = completed[index];
_gpuUs.Push(sample.ElapsedUs);
if (_history is not null
&& (uint)sample.FrameIndex < (uint)_history.Count)
{
FrameHistoryRecord row = _history[sample.FrameIndex];
_history[sample.FrameIndex] = row with { GpuUs = sample.ElapsedUs };
}
}
}
/// <summary>End GPU timing immediately after render submission.</summary>
public void EndGpuFrame()
{
_gpuTimer?.EndFrame();
}
/// <summary>
/// Campaign V slice V8: publish one delayed GPU sample measured by a backend
/// that owns its own timer (Vulkan timestamp queries). Identical bookkeeping
/// to <see cref="BeginGpuFrame"/>'s promotion loop — the sample joins the
/// that owns its own timer (Vulkan timestamp queries). The sample joins the
/// five-second window and back-fills the history row of the frame that
/// issued it, so <c>gpu_us</c> in the CSV is never attributed to a frame
/// that merely happened to poll it.
/// that merely happened to poll it. Campaign V slice V11 deleted the GL
/// arm's own <c>BeginGpuFrame</c>/<c>EndGpuFrame</c> bracket (the query-ring
/// promotion loop this comment used to describe) along with the GL query
/// ring itself — this is now the only way any backend reports GPU time.
/// </summary>
public void RecordGpuSample(int frameIndex, long elapsedUs)
{
@ -383,7 +333,6 @@ public sealed class FrameProfiler : IDisposable
/// </summary>
public void Dispose()
{
_gpuTimer?.Dispose();
if (_history is { Count: > 0 } && RenderingDiagnostics.FrameHistoryPath is { } path)
{
try

View file

@ -1,123 +0,0 @@
using System;
using Silk.NET.OpenGL;
namespace AcDream.App.Diagnostics;
internal readonly record struct GpuFrameSample(int FrameIndex, long ElapsedUs);
/// <summary>
/// MP0 (2026-07-05) — render-transaction GPU time via a ring of
/// <see cref="QueryTarget.TimeElapsed"/> queries (depth 4, so results are
/// read several frames late and never stall). Mirrors WbDrawDispatcher's query
/// idiom including the #125 lesson: a glGenQueries name is not a query
/// OBJECT until first glBeginQuery, so never-begun slots are skipped via
/// the pending flags.
///
/// <para>MUST NOT be active while ACDREAM_WB_DIAG=1: GL forbids two
/// simultaneously active TimeElapsed queries and WbDrawDispatcher brackets
/// its passes with them under that flag. The caller (FrameProfiler)
/// enforces the exclusion; this class just does the ring.</para>
///
/// <para>The query begins immediately before render-resource/world/private
/// presentation submission and ends immediately after it. It is deliberately
/// NOT left active until the next render callback: doing so includes display
/// pacing and an arbitrary CPU idle interval in the GPU clock. Completed
/// samples retain the frame index that owned the query so the delayed result
/// can be paired with the correct CPU/history row.</para>
/// </summary>
internal sealed class GpuFrameTimer : IDisposable
{
private const int RingDepth = 4;
private readonly GL _gl;
private readonly uint[] _queries = new uint[RingDepth];
private readonly bool[] _pending = new bool[RingDepth];
private readonly int[] _frameIndices = new int[RingDepth];
private int _nextSlot;
private int _activeSlot = -1;
public GpuFrameTimer(GL gl)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
for (int i = 0; i < RingDepth; i++)
_queries[i] = _gl.GenQuery();
}
/// <summary>
/// Poll every ended query non-blocking, then begin this frame's render
/// transaction in the first free ring slot. If all slots are still in
/// flight the current frame is intentionally left unmeasured rather than
/// stalling or overwriting a pending result.
/// </summary>
public int BeginFrame(int frameIndex, Span<GpuFrameSample> completed)
{
if (_activeSlot >= 0)
throw new InvalidOperationException("GPU frame timing was begun twice without an end.");
if (completed.Length < RingDepth)
throw new ArgumentException(
$"Completed-sample storage must hold at least {RingDepth} entries.",
nameof(completed));
int completedCount = 0;
for (int slot = 0; slot < RingDepth; slot++)
{
if (!_pending[slot])
continue;
_gl.GetQueryObject(
_queries[slot],
QueryObjectParameterName.ResultAvailable,
out int available);
if (available == 0)
continue;
_gl.GetQueryObject(
_queries[slot],
QueryObjectParameterName.Result,
out ulong elapsedNanoseconds);
completed[completedCount++] = new GpuFrameSample(
_frameIndices[slot],
(long)(elapsedNanoseconds / 1000UL));
_pending[slot] = false;
}
for (int offset = 0; offset < RingDepth; offset++)
{
int slot = (_nextSlot + offset) % RingDepth;
if (_pending[slot])
continue;
_gl.BeginQuery(QueryTarget.TimeElapsed, _queries[slot]);
_frameIndices[slot] = frameIndex;
_activeSlot = slot;
_nextSlot = (slot + 1) % RingDepth;
break;
}
return completedCount;
}
/// <summary>End the query around the current render transaction.</summary>
public void EndFrame()
{
if (_activeSlot < 0)
return;
_gl.EndQuery(QueryTarget.TimeElapsed);
_pending[_activeSlot] = true;
_activeSlot = -1;
}
/// <summary>End an active query without beginning another.</summary>
public void Stop()
{
EndFrame();
}
public void Dispose()
{
Stop();
for (int i = 0; i < RingDepth; i++)
_gl.DeleteQuery(_queries[i]);
}
}