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>
288 lines
13 KiB
C#
288 lines
13 KiB
C#
// ClipFrame.cs
|
||
//
|
||
// Phase U.3: the per-frame container for the SHARED per-frame clip data
|
||
// consumed by mesh_modern.vert (SSBO binding=2) and terrain_modern.vert (UBO
|
||
// binding=2). This is the "shared" half of the U.3 clip mechanism; the
|
||
// per-instance slot index buffer (SSBO binding=3) is PER-RENDERER and owned by
|
||
// each renderer (WbDrawDispatcher / EnvCellRenderer), parallel to its instance
|
||
// buffer — it is NOT here.
|
||
//
|
||
// === The contract (both shader sides obey) ===================================
|
||
// binding=2 mesh SSBO holds an array of CellClip, one per "slot":
|
||
// struct CellClip { uint count; uint _p0; uint _p1; uint _p2; vec4 planes[8]; };
|
||
// std430 layout: count at byte 0, three pad uints at 4/8/12, planes[8] at 16
|
||
// (vec4 stride 16) → 144 bytes per slot. Slot 0 is RESERVED = no-clip (count 0).
|
||
// binding=2 terrain UBO holds the single OutsideView region:
|
||
// layout(std140) { int uTerrainClipCount; vec4 uTerrainClipPlanes[8]; };
|
||
// std140 layout: count at byte 0 (padded to 16), planes[8] at 16 → 144 bytes.
|
||
//
|
||
// In U.3 a ClipFrame is built via NoClip(): one slot (slot 0, count 0) and a
|
||
// terrain count of 0. Everything renders exactly as before. U.4 populates real
|
||
// slots from a PortalVisibilityFrame (one CellClip per visible cell) and sets the
|
||
// terrain OutsideView planes, then points each renderer's per-instance slot
|
||
// buffer at the right slots.
|
||
//
|
||
// Pure CPU byte-packing. The GL upload machinery this file used to carry
|
||
// alongside the packing (a per-flight-slot region SSBO + terrain UBO arena,
|
||
// reservation/upload-once bookkeeping, and their disposal) was deleted at
|
||
// Campaign V slice V11: the RHI arm (see RhiWorldPassSurface.PrepareClipFrame /
|
||
// SetTerrainClip in WorldPassSurface.cs) reads RegionBytes/TerrainBytes below and
|
||
// copies them into a frame ring allocation instead, so nothing here owns a GPU
|
||
// resource anymore. The byte layout is asserted by ClipFrameLayoutTests so a
|
||
// silent std430/std140 drift can't reach the GPU.
|
||
using System;
|
||
using System.Numerics;
|
||
|
||
namespace AcDream.App.Rendering;
|
||
|
||
/// <summary>
|
||
/// Per-frame container for the SHARED clip data: the binding=2 mesh SSBO (one
|
||
/// <c>CellClip</c> per slot, slot 0 reserved no-clip) and the binding=2 terrain
|
||
/// UBO (the single OutsideView region). See the file header for the exact
|
||
/// std430 / std140 byte layout. Per-instance slot buffers (binding=3) are owned by
|
||
/// each renderer, not here.
|
||
/// </summary>
|
||
public sealed class ClipFrame : IDisposable
|
||
{
|
||
// ---- Layout constants (mirror mesh_modern.vert + terrain_modern.vert) ----
|
||
|
||
/// <summary>Max planes per clip region — matches the shader's <c>planes[8]</c>
|
||
/// and GL's guaranteed <c>GL_MAX_CLIP_DISTANCES >= 8</c>.</summary>
|
||
public const int MaxPlanes = 8;
|
||
|
||
/// <summary>std430 stride of one <c>CellClip</c>: 16 (count + 3 pad uints) +
|
||
/// 8 × 16 (vec4 planes) = 144 bytes.</summary>
|
||
public const int CellClipStrideBytes = 16 + MaxPlanes * 16; // 144
|
||
|
||
/// <summary>Byte offset of <c>planes[0]</c> within a <c>CellClip</c> (after the
|
||
/// count + 3 pad uints).</summary>
|
||
public const int CellClipPlanesOffset = 16;
|
||
|
||
/// <summary>std140 size of the terrain UBO block: int count padded to 16, then
|
||
/// 8 × 16 (vec4 planes) = 144 bytes. Same number as the SSBO stride by
|
||
/// coincidence of the 16-byte vec4 rule, but a DIFFERENT layout family.</summary>
|
||
public const int TerrainUboBytes = 16 + MaxPlanes * 16; // 144
|
||
|
||
/// <summary>UBO binding index for the terrain OutsideView clip region
|
||
/// (terrain_modern.vert binding=2). Read directly by both the RHI world-pass
|
||
/// section binder (<c>WorldFrameSectionBinding.BindTerrainClip</c>) and
|
||
/// <c>PortalDepthMaskRenderer</c>, so unlike the mesh SSBO binding (which the
|
||
/// RHI arm addresses through its own <c>GpuBindingModel.StorageClipRegions</c>
|
||
/// instead) this one is still genuinely shared.</summary>
|
||
public const uint TerrainClipUboBinding = 2;
|
||
|
||
// ---- CPU-side state ------------------------------------------------------
|
||
|
||
// Packed std430 bytes for clipRegions[]. Always holds at least slot 0.
|
||
private byte[] _regionBytes;
|
||
private int _slotCount;
|
||
|
||
// Packed std140 bytes for the terrain UBO (always TerrainUboBytes long).
|
||
private readonly byte[] _terrainBytes = new byte[TerrainUboBytes];
|
||
|
||
/// <summary>
|
||
/// The GL arm's per-flight-slot region/terrain buffer rings this used to
|
||
/// report on were deleted at Campaign V slice V11: every publication now
|
||
/// comes from the current frame's own ring allocator (see
|
||
/// <c>RhiWorldPassSurface.Publish</c>), which lives until its frame retires,
|
||
/// so there is no persistent dynamic-buffer pool left to size.
|
||
/// </summary>
|
||
internal int DynamicBufferSetCount => 0;
|
||
|
||
private ClipFrame(byte[] regionBytes, int slotCount)
|
||
{
|
||
_regionBytes = regionBytes;
|
||
_slotCount = slotCount;
|
||
// Terrain defaults to count 0 (ungated). _terrainBytes is already all
|
||
// zeros, which encodes count=0 + zeroed (unused) planes.
|
||
}
|
||
|
||
/// <summary>
|
||
/// The U.3 default frame: exactly slot 0 (no-clip, count 0) and a terrain
|
||
/// count of 0. The whole scene renders ungated — identical to pre-U.3. U.4
|
||
/// replaces this with a frame built from real portal visibility.
|
||
/// </summary>
|
||
public static ClipFrame NoClip()
|
||
{
|
||
// One slot, all zeros: count=0 ⇒ shader passes every plane.
|
||
var bytes = new byte[CellClipStrideBytes];
|
||
return new ClipFrame(bytes, slotCount: 1);
|
||
}
|
||
|
||
/// <summary>Number of clip slots currently packed (always >= 1 — slot 0 is
|
||
/// the reserved no-clip slot).</summary>
|
||
public int SlotCount => _slotCount;
|
||
|
||
/// <summary>
|
||
/// Phase U.4: reset this frame back to the NoClip state — exactly slot 0
|
||
/// (no-clip, count 0) and a terrain count of 0 — WITHOUT allocating a new
|
||
/// frame. The single long-lived <c>_clipFrame</c> in GameWindow is reset +
|
||
/// re-packed every frame by <see cref="ClipFrameAssembler"/>, then published
|
||
/// through one frame-ring allocation per section (see
|
||
/// <c>RhiWorldPassSurface.PrepareClipFrame</c>).
|
||
/// </summary>
|
||
public void Reset()
|
||
{
|
||
// Slot 0 = no-clip (count 0). Zero just the slot-0 region; the tail beyond
|
||
// _slotCount is never uploaded, so it needn't be cleared. AppendSlot writes
|
||
// each new slot's count + planes in full, so stale bytes there are
|
||
// overwritten before they can be uploaded.
|
||
if (_regionBytes.Length < CellClipStrideBytes)
|
||
EnsureRegionCapacity(CellClipStrideBytes);
|
||
Array.Clear(_regionBytes, 0, CellClipStrideBytes);
|
||
_slotCount = 1;
|
||
|
||
// Terrain back to count 0 (ungated) until SetTerrainClip is called again.
|
||
Array.Clear(_terrainBytes);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Per-frame hook kept for the shared render-frame-begin ordering — it is
|
||
/// called once per GPU-fenced frame slot alongside every other renderer's
|
||
/// <c>BeginFrame</c>, regardless of backend. The GL arm's frame-slot-scoped
|
||
/// region SSBO + terrain UBO arena this used to select was deleted at
|
||
/// Campaign V slice V11: the RHI arm takes a fresh ring allocation from the
|
||
/// current GPU frame for every publication instead, so there is no
|
||
/// frame-slot state left here to reset — only the argument to validate.
|
||
/// </summary>
|
||
public void BeginFrame(int frameSlot)
|
||
{
|
||
ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Append one clip region (becomes the next slot index) from a
|
||
/// <see cref="ClipPlaneSet"/>. Only the convex-plane case is supported in
|
||
/// U.3 — <c>Count > 0</c> packs that many planes; <c>Count == 0</c> packs a
|
||
/// no-clip region (pass-all). The scissor / nothing-visible fallbacks that
|
||
/// <see cref="ClipPlaneSet"/> can carry are deferred to U.4 (which will draw
|
||
/// the AABB box or skip the cell on the CPU side, not via this slot). Returns
|
||
/// the new slot's index.
|
||
/// </summary>
|
||
public int AppendSlot(ClipPlaneSet set)
|
||
{
|
||
int count = Math.Min(set.Count, MaxPlanes);
|
||
if (count == 0)
|
||
return AppendSlot(ReadOnlySpan<Vector4>.Empty);
|
||
|
||
Span<Vector4> planes = stackalloc Vector4[count];
|
||
for (int i = 0; i < count; i++)
|
||
planes[i] = set.Planes[i];
|
||
return AppendSlot(planes);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Append one clip region from a raw plane list. <paramref name="planes"/>
|
||
/// length 0 packs a no-clip (pass-all) region; otherwise up to
|
||
/// <see cref="MaxPlanes"/> planes are packed (extras ignored). Each plane is
|
||
/// <c>(nx, ny, 0, dw)</c> in clip space; a clip-space vertex is inside iff
|
||
/// <c>dot(plane, gl_Position) >= 0</c> for every plane. Returns the new
|
||
/// slot index.
|
||
/// </summary>
|
||
public int AppendSlot(ReadOnlySpan<Vector4> planes)
|
||
{
|
||
int count = Math.Min(planes.Length, MaxPlanes);
|
||
|
||
int slot = _slotCount;
|
||
int byteOffset = slot * CellClipStrideBytes;
|
||
EnsureRegionCapacity(byteOffset + CellClipStrideBytes);
|
||
|
||
// count (uint) at byteOffset; the 3 pad uints stay zero.
|
||
WriteUInt(_regionBytes, byteOffset, (uint)count);
|
||
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
int po = byteOffset + CellClipPlanesOffset + i * 16;
|
||
WriteVec4(_regionBytes, po, planes[i]);
|
||
}
|
||
|
||
_slotCount++;
|
||
return slot;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Set the terrain OutsideView clip region (the single region the terrain
|
||
/// shader gates against). <paramref name="planes"/> length 0 ungates terrain
|
||
/// (count 0). U.3 callers never touch this — <see cref="NoClip"/> leaves it
|
||
/// at count 0. U.4 calls it with the OutsideView planes.
|
||
/// </summary>
|
||
public void SetTerrainClip(ReadOnlySpan<Vector4> planes)
|
||
{
|
||
int count = Math.Min(planes.Length, MaxPlanes);
|
||
Array.Clear(_terrainBytes);
|
||
WriteInt(_terrainBytes, 0, count);
|
||
for (int i = 0; i < count; i++)
|
||
WriteVec4(_terrainBytes, CellClipPlanesOffset + i * 16, planes[i]);
|
||
}
|
||
|
||
/// <summary>
|
||
/// No-op: this container owns no GPU resource of its own on the RHI arm —
|
||
/// the deleted GL arm's per-frame-slot region SSBO + terrain UBO arena was
|
||
/// the only thing to release. Kept as a method (and <see cref="ClipFrame"/>
|
||
/// kept as <see cref="IDisposable"/>) so <c>GameWindowLifetime</c>'s ordered
|
||
/// shutdown doesn't need a special case for this one resource.
|
||
/// </summary>
|
||
public void Dispose()
|
||
{
|
||
}
|
||
|
||
// ---- byte helpers (little-endian; matches x86/x64 GPU upload) ------------
|
||
|
||
private void EnsureRegionCapacity(int requiredBytes)
|
||
{
|
||
if (_regionBytes.Length >= requiredBytes) return;
|
||
int newLen = Math.Max(requiredBytes, _regionBytes.Length * 2);
|
||
Array.Resize(ref _regionBytes, newLen);
|
||
}
|
||
|
||
private static void WriteUInt(byte[] dst, int offset, uint value)
|
||
{
|
||
dst[offset + 0] = (byte)(value & 0xFF);
|
||
dst[offset + 1] = (byte)((value >> 8) & 0xFF);
|
||
dst[offset + 2] = (byte)((value >> 16) & 0xFF);
|
||
dst[offset + 3] = (byte)((value >> 24) & 0xFF);
|
||
}
|
||
|
||
private static void WriteInt(byte[] dst, int offset, int value)
|
||
=> WriteUInt(dst, offset, unchecked((uint)value));
|
||
|
||
private static void WriteVec4(byte[] dst, int offset, Vector4 v)
|
||
{
|
||
WriteFloat(dst, offset + 0, v.X);
|
||
WriteFloat(dst, offset + 4, v.Y);
|
||
WriteFloat(dst, offset + 8, v.Z);
|
||
WriteFloat(dst, offset + 12, v.W);
|
||
}
|
||
|
||
private static void WriteFloat(byte[] dst, int offset, float value)
|
||
{
|
||
uint bits = BitConverter.SingleToUInt32Bits(value);
|
||
WriteUInt(dst, offset, bits);
|
||
}
|
||
|
||
// ---- Packed bytes --------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// The packed std430 region table for slots 0..<see cref="SlotCount"/>-1.
|
||
///
|
||
/// <para>The raw-GL arm this used to feed via <c>glBufferSubData</c> against
|
||
/// a renderer-owned SSBO was deleted at Campaign V slice V11; the RHI arm
|
||
/// (<c>RhiWorldPassSurface.PrepareClipFrame</c>) now copies these bytes
|
||
/// straight into a frame ring slice and publishes the range. The packing
|
||
/// above stays backend-neutral regardless.</para>
|
||
/// </summary>
|
||
internal ReadOnlySpan<byte> RegionBytes =>
|
||
_regionBytes.AsSpan(0, _slotCount * CellClipStrideBytes);
|
||
|
||
/// <summary>The packed std140 terrain-clip block. See <see cref="RegionBytes"/>.</summary>
|
||
internal ReadOnlySpan<byte> TerrainBytes => _terrainBytes;
|
||
|
||
// ---- Test seams ----------------------------------------------------------
|
||
|
||
/// <summary>Test seam: the packed std430 region bytes (slot 0..SlotCount-1).
|
||
/// Read-only snapshot used by ClipFrameLayoutTests to assert the byte layout.</summary>
|
||
internal ReadOnlySpan<byte> RegionBytesForTest => RegionBytes;
|
||
|
||
/// <summary>Test seam: the packed std140 terrain UBO bytes.</summary>
|
||
internal ReadOnlySpan<byte> TerrainBytesForTest => TerrainBytes;
|
||
}
|