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,8 +1,8 @@
|
|||
// ClipFrame.cs
|
||||
//
|
||||
// Phase U.3: the GPU-side container + uploader 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
|
||||
// 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.
|
||||
|
|
@ -22,21 +22,23 @@
|
|||
// terrain OutsideView planes, then points each renderer's per-instance slot
|
||||
// buffer at the right slots.
|
||||
//
|
||||
// Pure CPU byte-packing + a thin GL upload. The byte layout is asserted by
|
||||
// ClipFrameLayoutTests so a silent
|
||||
// std430/std140 drift can't reach the GPU.
|
||||
// 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.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame container + uploader 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
|
||||
/// 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>
|
||||
|
|
@ -61,13 +63,12 @@ public sealed class ClipFrame : IDisposable
|
|||
/// coincidence of the 16-byte vec4 rule, but a DIFFERENT layout family.</summary>
|
||||
public const int TerrainUboBytes = 16 + MaxPlanes * 16; // 144
|
||||
|
||||
/// <summary>SSBO binding index for the shared per-cell clip regions
|
||||
/// (mesh_modern.vert binding=2).</summary>
|
||||
public const uint MeshClipSsboBinding = 2;
|
||||
|
||||
/// <summary>UBO binding index for the terrain OutsideView clip region
|
||||
/// (terrain_modern.vert binding=2). UBO namespace — distinct from the SSBO
|
||||
/// binding=2 above.</summary>
|
||||
/// (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 ------------------------------------------------------
|
||||
|
|
@ -79,55 +80,14 @@ public sealed class ClipFrame : IDisposable
|
|||
// Packed std140 bytes for the terrain UBO (always TerrainUboBytes long).
|
||||
private readonly byte[] _terrainBytes = new byte[TerrainUboBytes];
|
||||
|
||||
// ---- GL-side state (lazily created on first upload) ----------------------
|
||||
|
||||
private uint _regionSsbo;
|
||||
private uint _terrainUbo;
|
||||
private RetryableResourceReleaseLedger? _disposeResources;
|
||||
private bool _disposing;
|
||||
private bool _disposed;
|
||||
|
||||
internal const int FrameSlotCount = GpuFrameFlightController.DefaultMaximumFramesInFlight;
|
||||
|
||||
private sealed class RegionBuffer
|
||||
{
|
||||
public uint Name;
|
||||
public int CapacityBytes;
|
||||
public ClipBufferCapacityPolicy CapacityPolicy;
|
||||
}
|
||||
|
||||
private sealed class TerrainBufferArena
|
||||
{
|
||||
public uint Name;
|
||||
public int CapacityBytes;
|
||||
public ClipBufferCapacityPolicy CapacityPolicy;
|
||||
}
|
||||
|
||||
// A full clip-region table is immutable once ClipFrameAssembler has packed
|
||||
// the frame. Keep exactly one SSBO per GPU-fenced frame slot. Terrain clip
|
||||
// state changes between outside-view slices, so each frame slot owns one
|
||||
// UBO arena and each draw binds a distinct aligned range within that arena.
|
||||
// This bounds native object retention at six buffers total instead of one
|
||||
// region/terrain pair per outside-view slice.
|
||||
private readonly ClipFrameResourceRing<RegionBuffer> _regionBuffers =
|
||||
new(FrameSlotCount);
|
||||
private readonly ClipFrameResourceRing<TerrainBufferArena> _terrainBuffers =
|
||||
new(FrameSlotCount);
|
||||
private readonly ClipFrameUploadState _uploadState = new();
|
||||
private int _dynamicFrameSlot;
|
||||
private bool _dynamicFrameStarted;
|
||||
private int _terrainRecordStrideBytes;
|
||||
private TerrainClipBufferBinding _terrainBinding;
|
||||
|
||||
internal int DynamicBufferSetCount =>
|
||||
Math.Max(_regionBuffers.Count, _terrainBuffers.Count);
|
||||
internal int RegionBufferCount => _regionBuffers.Count;
|
||||
internal int TerrainBufferCount => _terrainBuffers.Count;
|
||||
internal int TerrainRecordStrideBytes => _terrainRecordStrideBytes;
|
||||
|
||||
// GL reference captured on the first upload so Dispose can delete each
|
||||
// frame-slot buffer. ClipFrame is long-lived and owns their teardown.
|
||||
private GL? _gl;
|
||||
/// <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)
|
||||
{
|
||||
|
|
@ -156,9 +116,10 @@ public sealed class ClipFrame : IDisposable
|
|||
/// <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 or new GL buffers. The single long-lived <c>_clipFrame</c> in
|
||||
/// GameWindow is reset + re-packed every frame by <see cref="ClipFrameAssembler"/>,
|
||||
/// then uploaded through one SSBO and one terrain arena per fenced frame slot.
|
||||
/// 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()
|
||||
{
|
||||
|
|
@ -175,35 +136,18 @@ public sealed class ClipFrame : IDisposable
|
|||
Array.Clear(_terrainBytes);
|
||||
}
|
||||
|
||||
/// <summary>The shared mesh-clip SSBO id, or 0 before the first
|
||||
/// <see cref="UploadRegions"/>. Renderers may bind this directly if they don't
|
||||
/// receive it via a parameter; <see cref="UploadRegions"/> already binds it to
|
||||
/// <see cref="MeshClipSsboBinding"/>.</summary>
|
||||
public uint RegionSsbo => _regionSsbo;
|
||||
|
||||
/// <summary>The terrain-clip UBO id, or 0 before the first
|
||||
/// <see cref="UploadTerrainClip"/>. The buffer alone does not identify the
|
||||
/// active range; consumers should use <see cref="TerrainBinding"/>.</summary>
|
||||
public uint TerrainUbo => _terrainUbo;
|
||||
|
||||
/// <summary>The currently installed terrain-clip range. The buffer name is
|
||||
/// stable for the current GPU-fenced frame slot; the offset advances once
|
||||
/// for every outside-view draw.</summary>
|
||||
public TerrainClipBufferBinding TerrainBinding => _terrainBinding;
|
||||
|
||||
/// <summary>
|
||||
/// Selects one GPU-fenced frame slot. Its region SSBO and terrain arena are
|
||||
/// safe to reuse because the frame-flight controller already waited for the
|
||||
/// slot's preceding submission.
|
||||
/// 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)
|
||||
{
|
||||
if ((uint)frameSlot >= FrameSlotCount)
|
||||
throw new ArgumentOutOfRangeException(nameof(frameSlot));
|
||||
_dynamicFrameSlot = frameSlot;
|
||||
_dynamicFrameStarted = true;
|
||||
_terrainBinding = default;
|
||||
_uploadState.BeginFrame();
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -272,266 +216,14 @@ public sealed class ClipFrame : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compatibility entry point for one-region/one-terrain callers. Complex
|
||||
/// PView frames should reserve their complete terrain sequence, upload the
|
||||
/// regions once, then call <see cref="UploadTerrainClip"/> per slice.
|
||||
/// 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 unsafe void UploadShared(GL gl)
|
||||
{
|
||||
ReserveTerrainUploads(gl, 1);
|
||||
UploadRegions(gl);
|
||||
UploadTerrainClip(gl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates the current frame slot's terrain arena before any draw uses it.
|
||||
/// The arena has one alignment-safe range per subsequent
|
||||
/// <see cref="UploadTerrainClip"/> call, so later slices never overwrite
|
||||
/// uniform data referenced by earlier GPU commands.
|
||||
/// </summary>
|
||||
public void ReserveTerrainUploads(GL gl, int uploadCount)
|
||||
{
|
||||
ValidateGlContext(gl);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(uploadCount, 1);
|
||||
_uploadState.ValidateTerrainReservation(uploadCount);
|
||||
|
||||
if (_terrainRecordStrideBytes == 0)
|
||||
{
|
||||
GLHelpers.ThrowOnResourceError(gl, "ClipFrame terrain alignment query (precondition)");
|
||||
gl.GetInteger(GetPName.UniformBufferOffsetAlignment, out int alignment);
|
||||
GLHelpers.ThrowOnResourceError(gl, "ClipFrame terrain alignment query");
|
||||
_terrainRecordStrideBytes = ClipFrameArenaLayout.RecordStride(alignment);
|
||||
}
|
||||
|
||||
TerrainBufferArena arena = GetOrCreateTerrainArena(gl);
|
||||
int requiredBytes = ClipFrameArenaLayout.RequiredBytes(
|
||||
uploadCount,
|
||||
_terrainRecordStrideBytes);
|
||||
int targetCapacity = arena.CapacityPolicy.SelectCapacity(
|
||||
arena.CapacityBytes,
|
||||
requiredBytes);
|
||||
if (targetCapacity != arena.CapacityBytes)
|
||||
{
|
||||
ClipBufferCapacityTransaction.Resize(
|
||||
ref arena.CapacityBytes,
|
||||
targetCapacity,
|
||||
(previousBytes, newBytes) =>
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
gl,
|
||||
GLEnum.UniformBuffer,
|
||||
arena.Name,
|
||||
previousBytes,
|
||||
newBytes,
|
||||
GLEnum.DynamicDraw,
|
||||
"ClipFrame terrain UBO arena resize"));
|
||||
}
|
||||
|
||||
_terrainUbo = arena.Name;
|
||||
_uploadState.CommitTerrainReservation(uploadCount);
|
||||
}
|
||||
|
||||
/// <summary>Uploads and binds the immutable clip-region table once for the
|
||||
/// assembled frame.</summary>
|
||||
public unsafe void UploadRegions(GL gl)
|
||||
{
|
||||
ValidateGlContext(gl);
|
||||
_uploadState.ValidateRegionsNotUploaded();
|
||||
|
||||
RegionBuffer region = GetOrCreateRegionBuffer(gl);
|
||||
int regionByteCount = checked(_slotCount * CellClipStrideBytes);
|
||||
int targetCapacity = region.CapacityPolicy.SelectCapacity(
|
||||
region.CapacityBytes,
|
||||
regionByteCount);
|
||||
if (targetCapacity != region.CapacityBytes)
|
||||
{
|
||||
ClipBufferCapacityTransaction.Resize(
|
||||
ref region.CapacityBytes,
|
||||
targetCapacity,
|
||||
(previousBytes, newBytes) =>
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
region.Name,
|
||||
previousBytes,
|
||||
newBytes,
|
||||
GLEnum.DynamicDraw,
|
||||
"ClipFrame region SSBO resize"));
|
||||
}
|
||||
|
||||
fixed (byte* p = _regionBytes)
|
||||
{
|
||||
TrackedGlResource.UpdateBufferSubData(
|
||||
gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
region.Name,
|
||||
0,
|
||||
regionByteCount,
|
||||
p,
|
||||
"ClipFrame region SSBO update");
|
||||
}
|
||||
gl.BindBufferBase(
|
||||
BufferTargetARB.ShaderStorageBuffer,
|
||||
MeshClipSsboBinding,
|
||||
region.Name);
|
||||
GLHelpers.ThrowOnResourceError(gl, "ClipFrame region SSBO binding");
|
||||
|
||||
_regionSsbo = region.Name;
|
||||
_uploadState.MarkRegionsUploaded();
|
||||
}
|
||||
|
||||
/// <summary>Uploads the current terrain clip bytes into the next unique
|
||||
/// aligned arena record and installs that range at UBO binding 2.</summary>
|
||||
public unsafe TerrainClipBufferBinding UploadTerrainClip(GL gl)
|
||||
{
|
||||
ValidateGlContext(gl);
|
||||
int recordIndex = _uploadState.NextTerrainRecord();
|
||||
TerrainBufferArena arena = _terrainBuffers.GetRequired(_dynamicFrameSlot);
|
||||
int offsetBytes = ClipFrameArenaLayout.RecordOffset(
|
||||
recordIndex,
|
||||
_terrainRecordStrideBytes);
|
||||
|
||||
fixed (byte* p = _terrainBytes)
|
||||
{
|
||||
TrackedGlResource.UpdateBufferSubData(
|
||||
gl,
|
||||
GLEnum.UniformBuffer,
|
||||
arena.Name,
|
||||
offsetBytes,
|
||||
TerrainUboBytes,
|
||||
p,
|
||||
"ClipFrame terrain UBO range update");
|
||||
}
|
||||
|
||||
_terrainBinding = new TerrainClipBufferBinding(
|
||||
arena.Name,
|
||||
offsetBytes,
|
||||
TerrainUboBytes);
|
||||
_terrainBinding.Bind(gl);
|
||||
_terrainUbo = arena.Name;
|
||||
return _terrainBinding;
|
||||
}
|
||||
|
||||
public void BindTerrainClip(GL gl)
|
||||
{
|
||||
ValidateGlContext(gl);
|
||||
if (!_terrainBinding.IsValid)
|
||||
throw new InvalidOperationException("No terrain clip range has been uploaded for this frame.");
|
||||
_terrainBinding.Bind(gl);
|
||||
}
|
||||
|
||||
private RegionBuffer GetOrCreateRegionBuffer(GL gl)
|
||||
{
|
||||
if (_regionBuffers.TryGet(_dynamicFrameSlot, out RegionBuffer? existing))
|
||||
return existing!;
|
||||
|
||||
uint name = TrackedGlResource.CreateBuffer(gl, "ClipFrame region SSBO creation");
|
||||
var created = new RegionBuffer { Name = name };
|
||||
try
|
||||
{
|
||||
_regionBuffers.Set(_dynamicFrameSlot, created);
|
||||
return created;
|
||||
}
|
||||
catch
|
||||
{
|
||||
TrackedGlResource.DeleteBuffer(gl, name, 0, "ClipFrame region SSBO rollback");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private TerrainBufferArena GetOrCreateTerrainArena(GL gl)
|
||||
{
|
||||
if (_terrainBuffers.TryGet(_dynamicFrameSlot, out TerrainBufferArena? existing))
|
||||
return existing!;
|
||||
|
||||
uint name = TrackedGlResource.CreateBuffer(gl, "ClipFrame terrain UBO creation");
|
||||
var created = new TerrainBufferArena { Name = name };
|
||||
try
|
||||
{
|
||||
_terrainBuffers.Set(_dynamicFrameSlot, created);
|
||||
return created;
|
||||
}
|
||||
catch
|
||||
{
|
||||
TrackedGlResource.DeleteBuffer(gl, name, 0, "ClipFrame terrain UBO rollback");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateGlContext(GL gl)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_dynamicFrameStarted)
|
||||
throw new InvalidOperationException("BeginFrame must be called before uploading clip data.");
|
||||
if (_gl is null)
|
||||
_gl = gl;
|
||||
else if (!ReferenceEquals(_gl, gl))
|
||||
throw new InvalidOperationException("ClipFrame cannot span OpenGL contexts.");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed || _disposing) return;
|
||||
_disposing = true;
|
||||
try
|
||||
{
|
||||
if (_disposeResources is null)
|
||||
{
|
||||
var releases = new List<(string Name, Action Release)>();
|
||||
if (_gl is not null)
|
||||
{
|
||||
int regionIndex = 0;
|
||||
foreach (RegionBuffer set in _regionBuffers.Values)
|
||||
{
|
||||
RetryableGpuResourceRelease release =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
set.Name,
|
||||
set.CapacityBytes,
|
||||
"ClipFrame region SSBO disposal");
|
||||
releases.Add(($"region-ssbo-{regionIndex++}", release.Run));
|
||||
}
|
||||
|
||||
int terrainIndex = 0;
|
||||
foreach (TerrainBufferArena arena in _terrainBuffers.Values)
|
||||
{
|
||||
RetryableGpuResourceRelease release =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
arena.Name,
|
||||
arena.CapacityBytes,
|
||||
"ClipFrame terrain UBO disposal");
|
||||
releases.Add(($"terrain-ubo-{terrainIndex++}", release.Run));
|
||||
}
|
||||
}
|
||||
|
||||
_disposeResources = new RetryableResourceReleaseLedger(releases);
|
||||
}
|
||||
|
||||
ResourceReleaseAttempt attempt = _disposeResources.Advance();
|
||||
if (!_disposeResources.IsComplete)
|
||||
{
|
||||
throw attempt.ToException(
|
||||
"One or more ClipFrame GPU buffers could not be released.");
|
||||
}
|
||||
|
||||
_regionSsbo = 0;
|
||||
_terrainUbo = 0;
|
||||
_terrainBinding = default;
|
||||
_dynamicFrameStarted = false;
|
||||
_disposeResources = null;
|
||||
_disposed = true;
|
||||
|
||||
if (attempt.HasFailures)
|
||||
{
|
||||
throw attempt.ToException(
|
||||
"ClipFrame GPU buffers released with exceptional committed outcomes.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_disposing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- byte helpers (little-endian; matches x86/x64 GPU upload) ------------
|
||||
|
|
@ -573,11 +265,11 @@ public sealed class ClipFrame : IDisposable
|
|||
/// <summary>
|
||||
/// The packed std430 region table for slots 0..<see cref="SlotCount"/>-1.
|
||||
///
|
||||
/// <para>Campaign V slice V6j: the GL arm hands these to
|
||||
/// <c>glBufferSubData</c> against a renderer-owned SSBO; the RHI arm copies
|
||||
/// them straight into a frame ring slice and publishes the range. Same bytes,
|
||||
/// two destinations — which is why the packing above is backend-neutral and
|
||||
/// stays where it is.</para>
|
||||
/// <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);
|
||||
|
|
@ -594,230 +286,3 @@ public sealed class ClipFrame : IDisposable
|
|||
/// <summary>Test seam: the packed std140 terrain UBO bytes.</summary>
|
||||
internal ReadOnlySpan<byte> TerrainBytesForTest => TerrainBytes;
|
||||
}
|
||||
|
||||
/// <summary>A single std140 terrain-clip record within a frame-slot UBO arena.</summary>
|
||||
public readonly record struct TerrainClipBufferBinding(
|
||||
uint Buffer,
|
||||
int OffsetBytes,
|
||||
int SizeBytes)
|
||||
{
|
||||
public bool IsValid => Buffer != 0 && OffsetBytes >= 0 && SizeBytes > 0;
|
||||
|
||||
public void Bind(GL gl)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Cannot bind an empty terrain clip range.");
|
||||
|
||||
gl.BindBufferRange(
|
||||
BufferTargetARB.UniformBuffer,
|
||||
ClipFrame.TerrainClipUboBinding,
|
||||
Buffer,
|
||||
(nint)OffsetBytes,
|
||||
(nuint)SizeBytes);
|
||||
GLHelpers.ThrowOnResourceError(gl, "ClipFrame terrain UBO range binding");
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ClipFrameArenaLayout
|
||||
{
|
||||
public static int RecordStride(int uniformBufferOffsetAlignment)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(uniformBufferOffsetAlignment, 1);
|
||||
long stride = ((long)ClipFrame.TerrainUboBytes + uniformBufferOffsetAlignment - 1L)
|
||||
/ uniformBufferOffsetAlignment
|
||||
* uniformBufferOffsetAlignment;
|
||||
if (stride > int.MaxValue)
|
||||
throw new OverflowException("Terrain clip UBO record stride exceeds Int32.MaxValue.");
|
||||
return (int)stride;
|
||||
}
|
||||
|
||||
public static int RecordOffset(int recordIndex, int recordStrideBytes)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(recordIndex);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(recordStrideBytes, ClipFrame.TerrainUboBytes);
|
||||
return checked(recordIndex * recordStrideBytes);
|
||||
}
|
||||
|
||||
public static int RequiredBytes(int recordCount, int recordStrideBytes)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(recordCount, 1);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(recordStrideBytes, ClipFrame.TerrainUboBytes);
|
||||
return checked(recordCount * recordStrideBytes);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ClipBufferCapacityTransaction
|
||||
{
|
||||
/// <summary>Publishes capacity only after BufferData succeeds, and before
|
||||
/// any later upload/bind operation can throw.</summary>
|
||||
public static void Resize(
|
||||
ref int publishedCapacityBytes,
|
||||
int targetCapacityBytes,
|
||||
Action<int, int> allocate)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(publishedCapacityBytes);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(targetCapacityBytes, 1);
|
||||
ArgumentNullException.ThrowIfNull(allocate);
|
||||
|
||||
int previousCapacityBytes = publishedCapacityBytes;
|
||||
allocate(previousCapacityBytes, targetCapacityBytes);
|
||||
publishedCapacityBytes = targetCapacityBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Growth is immediate; shrinking requires repeated severe under-use. This
|
||||
/// avoids reallocating during ordinary portal-count jitter while ensuring a
|
||||
/// one-off pathological flood does not permanently pin its peak GPU storage.
|
||||
/// </summary>
|
||||
internal struct ClipBufferCapacityPolicy
|
||||
{
|
||||
internal const int ShrinkAfterUnderusedFrames = 3;
|
||||
internal const int ShrinkUtilizationDivisor = 4;
|
||||
|
||||
private int _underusedFrames;
|
||||
|
||||
public int SelectCapacity(int currentBytes, int requiredBytes)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(currentBytes);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(requiredBytes, 1);
|
||||
|
||||
if (requiredBytes > currentBytes)
|
||||
{
|
||||
_underusedFrames = 0;
|
||||
return DynamicBufferCapacity.Grow(currentBytes, requiredBytes);
|
||||
}
|
||||
|
||||
if ((long)requiredBytes * ShrinkUtilizationDivisor <= currentBytes)
|
||||
{
|
||||
_underusedFrames++;
|
||||
if (_underusedFrames >= ShrinkAfterUnderusedFrames)
|
||||
{
|
||||
_underusedFrames = 0;
|
||||
return DynamicBufferCapacity.Grow(0, requiredBytes);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_underusedFrames = 0;
|
||||
}
|
||||
|
||||
return currentBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fixed-cardinality resource ownership indexed by the same frame
|
||||
/// slots protected by <see cref="GpuFrameFlightController"/>.</summary>
|
||||
internal sealed class ClipFrameResourceRing<T>(int slotCount) where T : class
|
||||
{
|
||||
private readonly T?[] _slots = new T?[slotCount > 0
|
||||
? slotCount
|
||||
: throw new ArgumentOutOfRangeException(nameof(slotCount))];
|
||||
|
||||
public int Count { get; private set; }
|
||||
|
||||
public bool TryGet(int slot, out T? resource)
|
||||
{
|
||||
ValidateSlot(slot);
|
||||
resource = _slots[slot];
|
||||
return resource is not null;
|
||||
}
|
||||
|
||||
public T GetRequired(int slot)
|
||||
{
|
||||
ValidateSlot(slot);
|
||||
return _slots[slot]
|
||||
?? throw new InvalidOperationException($"Frame slot {slot} has no resource.");
|
||||
}
|
||||
|
||||
public void Set(int slot, T resource)
|
||||
{
|
||||
ValidateSlot(slot);
|
||||
ArgumentNullException.ThrowIfNull(resource);
|
||||
if (_slots[slot] is not null)
|
||||
throw new InvalidOperationException($"Frame slot {slot} already owns a resource.");
|
||||
_slots[slot] = resource;
|
||||
Count++;
|
||||
}
|
||||
|
||||
public IEnumerable<T> Values
|
||||
{
|
||||
get
|
||||
{
|
||||
for (int i = 0; i < _slots.Length; i++)
|
||||
if (_slots[i] is T value)
|
||||
yield return value;
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateSlot(int slot)
|
||||
{
|
||||
if ((uint)slot >= (uint)_slots.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(slot));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Pure sequencing state for one ClipFrame render submission.</summary>
|
||||
internal sealed class ClipFrameUploadState
|
||||
{
|
||||
private bool _frameStarted;
|
||||
private bool _regionsUploaded;
|
||||
private int _terrainReserved;
|
||||
private int _terrainCursor;
|
||||
|
||||
public int TerrainReserved => _terrainReserved;
|
||||
public int TerrainUploaded => _terrainCursor;
|
||||
public bool RegionsUploaded => _regionsUploaded;
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
_frameStarted = true;
|
||||
_regionsUploaded = false;
|
||||
_terrainReserved = 0;
|
||||
_terrainCursor = 0;
|
||||
}
|
||||
|
||||
public void ValidateRegionsNotUploaded()
|
||||
{
|
||||
EnsureFrameStarted();
|
||||
if (_regionsUploaded)
|
||||
throw new InvalidOperationException("Clip regions may be uploaded only once per frame.");
|
||||
}
|
||||
|
||||
public void MarkRegionsUploaded()
|
||||
{
|
||||
ValidateRegionsNotUploaded();
|
||||
_regionsUploaded = true;
|
||||
}
|
||||
|
||||
public void ValidateTerrainReservation(int uploadCount)
|
||||
{
|
||||
EnsureFrameStarted();
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(uploadCount, 1);
|
||||
if (_terrainReserved != 0)
|
||||
throw new InvalidOperationException("Terrain uploads have already been reserved for this frame.");
|
||||
}
|
||||
|
||||
public void CommitTerrainReservation(int uploadCount)
|
||||
{
|
||||
ValidateTerrainReservation(uploadCount);
|
||||
_terrainReserved = uploadCount;
|
||||
}
|
||||
|
||||
public int NextTerrainRecord()
|
||||
{
|
||||
EnsureFrameStarted();
|
||||
if (_terrainReserved == 0)
|
||||
throw new InvalidOperationException("ReserveTerrainUploads must run before terrain clip uploads.");
|
||||
if (_terrainCursor >= _terrainReserved)
|
||||
throw new InvalidOperationException("Terrain clip uploads exceeded the reserved arena record count.");
|
||||
return _terrainCursor++;
|
||||
}
|
||||
|
||||
private void EnsureFrameStarted()
|
||||
{
|
||||
if (!_frameStarted)
|
||||
throw new InvalidOperationException("BeginFrame must be called before uploading clip data.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue