feat(render): Campaign V slice V4b - move the mesh arena onto IGpuBuffer
The shared vertex/index arena is the largest single GPU allocation acdream
makes (384 MiB + 128 MiB) and the one the Vulkan backend has the most specific
plan for (campaign doc section 4.3). This slice swaps the resource handle type
underneath it and changes nothing else: the reclaimable-range allocator, the
growth quanta, the budgeted incremental grow-and-copy, the retirement-ledger
gating, the abort ticket, the LRU that drives eviction, and the 896 MiB
dual-generation physical ceiling are all untouched. That is deliberate - those
are the semantics section 4.3 says the Vulkan arena must mirror exactly, so
preserving them is the point of the slice rather than an incidental constraint.
What moved:
- GlobalMeshBuffer's two GL buffer objects became IGpuBuffer, allocated through
IGpuDevice.CreateBuffer with DeviceLocal residency and Vertex-or-Index plus
both transfer usages (the arena is simultaneously a draw source and both ends
of its own migration, which is exactly why GpuBufferUsage is a flags enum).
- UploadMesh's two hand-rolled BufferSubData sites became IGpuBuffer.Upload.
The old code staged indices through GL_COPY_WRITE_BUFFER specifically so an
upload could not mutate whichever VAO a preceding render pass left bound;
Upload stages through a neutral binding point of the backend's choosing, so
that property now comes for free instead of by hand.
- AdvanceMigration's CopyBufferSubData became IGpuBuffer.CopyTo - a device-side
copy, which the Vulkan backend will record as vkCmdCopyBuffer. The live
prefix still never round-trips through system memory.
- BeginMigration/CommitMigration/AbortMigration/Dispose now carry IGpuBuffer in
the migration record and the abort ticket instead of raw uint names, so the
ticket's identity check is a resource identity rather than a number that goes
stale the moment the buffer is deleted.
What deliberately did not move. A VAO has no RHI verb - Vulkan bakes vertex
input into the pipeline - and WbDrawDispatcher, EnvCellRenderer and
ParticleRenderer still bind VAO/VBO/IBO with raw GL until V4c hands them the
pass encoder. So GlobalMeshBuffer keeps its GL handle for the vertex array and
its attribute layout, and VBO/IBO became computed properties that publish the
backing GL name of the buffer the arena now owns as an IGpuBuffer. One private
RequireGlBuffer helper is the single place that reaches through the interface,
and it disappears with those consumers. ObjectMeshManager therefore needed no
upload-path change at all - it reads those same three properties.
Two decisions worth recording.
First, arena deletes do not route through IGpuBuffer.Dispose. The arena already
gates every delete behind its own GpuRetirementLedger and decrements its
physical-capacity accounting in the same retirement stage; Dispose would defer
the physical free through the device queue a second time, so the accounting
would run ahead of real GPU residency and could admit a migration that breaches
the 896 MiB ceiling. GlGpuBuffer gains DeleteRetired for callers that have
already proved flight safety, and GlobalMeshBuffer composes it into a release
whose four stages match TrackedGlResource.CreateRetryableBufferDeletion exactly
- precondition, mutation-with-validation, byte accounting, resource-count
accounting - so a driver failure re-issues only the delete and never
double-counts.
Second, two corrections in the GL backend, both required to keep this port
behaviour-preserving rather than merely compiling. GlGpuBuffer's glBufferData
usage hint now follows residency (DeviceLocal -> StaticDraw), which is what the
arena has always requested; the host-writable rings and texture table keep
DynamicDraw and are unaffected. And a failed allocation now releases the GL
name it had already created - GL_OUT_OF_MEMORY is a real outcome for a 384 MiB
growth destination, and the previous code leaked the name on that path.
Plumbing: the device reaches the arena through WbMeshAdapter and
ObjectMeshManager. Their constructors became internal because IGpuDevice is an
internal type by the pinned contract, matching what V4a did for BitmapFont,
DebugLineRenderer and TextRenderer; both classes stay public and every caller
already lives inside AcDream.App or its InternalsVisibleTo test assemblies. The
unused public GlobalMeshBuffer(GL) convenience constructor is gone - it could
not supply a device and had no callers.
Gates. Release build green with TreatWarningsAsErrors. App tests 3,843 passed /
3 skipped, exactly the slice baseline; complete Release suite 8,906 passed / 5
skipped. Offline pixel gate against 79ee2361: 25 differing pixels of 563,200
(fraction 4.44e-05), against a same-commit control captured immediately
afterwards of 24 - the change is indistinguishable from capture noise and sits
40x under the 0.001 threshold. An earlier gate run was discarded rather than
interpreted: its client log showed real ScrollUp/ScrollDown input reaching the
offline window, which zoomed the camera, and a camera-motion difference is not
a rendering result.
No divergence-register row: this slice changes no retail-facing behaviour.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
79ee2361ad
commit
e946b46f75
9 changed files with 302 additions and 142 deletions
|
|
@ -114,6 +114,7 @@ internal interface IWorldRenderCompositionFactory
|
|||
Shader CreateMeshShader(GL gl, string shadersDirectory);
|
||||
WbMeshAdapter CreateMeshAdapter(
|
||||
GL gl,
|
||||
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
|
|
@ -291,12 +292,14 @@ internal sealed class RetailWorldRenderCompositionFactory
|
|||
|
||||
public WbMeshAdapter CreateMeshAdapter(
|
||||
GL gl,
|
||||
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
ResidencyBudgetOptions budgets) =>
|
||||
new(
|
||||
gl,
|
||||
device,
|
||||
dats,
|
||||
preparedAssets,
|
||||
NullLogger<WbMeshAdapter>.Instance,
|
||||
|
|
@ -540,6 +543,7 @@ internal sealed class WorldRenderCompositionPhase
|
|||
"WB mesh adapter",
|
||||
() => _factory.CreateMeshAdapter(
|
||||
gl,
|
||||
_dependencies.GpuDevice,
|
||||
content.Dats,
|
||||
content.PreparedAssets,
|
||||
_dependencies.ResourceRetirement,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ namespace AcDream.App.Rendering.Gpu.Gl;
|
|||
/// data; every write after that is <c>glBufferSubData</c> — no persistent
|
||||
/// mapping, matching the campaign's "GL backend is deliberately behaviour-
|
||||
/// preserving" rule for this slice.
|
||||
///
|
||||
/// The <c>glBufferData</c> usage hint follows
|
||||
/// <see cref="GpuBufferDescription.Residency"/>:
|
||||
/// <see cref="GpuMemoryResidency.DeviceLocal"/> is written rarely and read by
|
||||
/// many draws, so it takes <c>StaticDraw</c>; the host-writable rings and
|
||||
/// tables are rewritten every frame and take <c>DynamicDraw</c>. The hint is
|
||||
/// advisory to the driver, but keeping it per-residency is what let the mesh
|
||||
/// arena (Campaign V slice V4b) move onto this class without changing the
|
||||
/// allocation it has always requested.
|
||||
/// </summary>
|
||||
internal sealed class GlGpuBuffer : IGpuBuffer
|
||||
{
|
||||
|
|
@ -35,13 +44,29 @@ internal sealed class GlGpuBuffer : IGpuBuffer
|
|||
Residency = description.Residency;
|
||||
|
||||
_name = GlResourceCommand.CreateName(_gl, $"buffer '{Name}'", _gl.GenBuffer, _gl.DeleteBuffer);
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, _name);
|
||||
unsafe
|
||||
try
|
||||
{
|
||||
_gl.BufferData(GLEnum.CopyWriteBuffer, (nuint)SizeBytes, null, BufferUsageARB.DynamicDraw);
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, _name);
|
||||
unsafe
|
||||
{
|
||||
_gl.BufferData(GLEnum.CopyWriteBuffer, (nuint)SizeBytes, null, UsageHintFor(Residency));
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"allocate buffer '{Name}' ({SizeBytes} bytes)");
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A rejected data store (GL_OUT_OF_MEMORY is a real outcome for the
|
||||
// mesh arena's 384 MiB growth destination) must not strand the name
|
||||
// that was already created for it. The caller sees the original
|
||||
// failure and owns nothing.
|
||||
GlResourceCommand.DeleteBuffer(
|
||||
_gl,
|
||||
_name,
|
||||
$"rollback buffer '{Name}' after a failed allocation");
|
||||
_name = 0;
|
||||
throw;
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"allocate buffer '{Name}' ({SizeBytes} bytes)");
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, 0);
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
|
@ -52,6 +77,43 @@ internal sealed class GlGpuBuffer : IGpuBuffer
|
|||
/// <summary>The physical GL buffer name. For bind calls issued by <see cref="GlGpuPassEncoder"/>.</summary>
|
||||
internal uint GlName => _name;
|
||||
|
||||
private static BufferUsageARB UsageHintFor(GpuMemoryResidency residency) =>
|
||||
residency == GpuMemoryResidency.DeviceLocal
|
||||
? BufferUsageARB.StaticDraw
|
||||
: BufferUsageARB.DynamicDraw;
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the physical buffer on the calling thread instead of deferring it
|
||||
/// through the device's retirement queue the way <see cref="Dispose"/> does.
|
||||
///
|
||||
/// Campaign V slice V4b: the mesh arena (<c>GlobalMeshBuffer</c>) already gates
|
||||
/// every arena delete behind its own <c>GpuRetirementLedger</c> and decrements
|
||||
/// its <c>MaximumPhysicalArenaBytes</c> accounting in the same retirement stage.
|
||||
/// Routing the physical free through the queue a second time would delay it by
|
||||
/// a further flight generation, so the arena's physical-capacity accounting
|
||||
/// would run ahead of real GPU residency and could admit a migration that
|
||||
/// breaches the 896 MiB dual-generation ceiling. A caller of this method must
|
||||
/// therefore already have proved no submitted frame can still reference the
|
||||
/// buffer.
|
||||
///
|
||||
/// Retryable by construction: the managed name is cleared only after the driver
|
||||
/// reports success, so a failed delete is re-issued by the next attempt, and any
|
||||
/// later call (including <see cref="Dispose"/>) is a no-op.
|
||||
/// </summary>
|
||||
internal void DeleteRetired(string context)
|
||||
{
|
||||
uint name = _name;
|
||||
if (name == 0)
|
||||
return;
|
||||
|
||||
_gl.DeleteBuffer(name);
|
||||
// Per the GL error contract a command which generates an error does not
|
||||
// change object state, so validation stays in the same stage as the
|
||||
// mutation and the name is only surrendered once deletion committed.
|
||||
GLHelpers.ThrowOnResourceError(_gl, context);
|
||||
_name = 0;
|
||||
}
|
||||
|
||||
public void Upload(long offsetBytes, ReadOnlySpan<byte> data)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ public static class RenderBootstrap
|
|||
var wbLogger = NullLogger<Wb.WbMeshAdapter>.Instance;
|
||||
var meshAdapter = Wb.WbMeshAdapter.CreateWithLiveDatPreparedAssets(
|
||||
gl,
|
||||
gpuDevice,
|
||||
dats,
|
||||
wbLogger,
|
||||
frameFlights);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using AcDream.Content;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
|
|
@ -23,7 +26,7 @@ internal readonly record struct GlobalMeshMaintenanceStep(
|
|||
bool Completed);
|
||||
|
||||
/// <summary>
|
||||
/// Retains the staged GL name and its exact release cursor while an aborted
|
||||
/// Retains the staged buffer and its exact release cursor while an aborted
|
||||
/// arena migration is being unwound. The owner may only forget the migration
|
||||
/// after this ticket has converged.
|
||||
/// </summary>
|
||||
|
|
@ -32,19 +35,18 @@ internal sealed class GlobalMeshMigrationAbortTicket
|
|||
private readonly RetryableGpuResourceRelease _release;
|
||||
|
||||
public GlobalMeshMigrationAbortTicket(
|
||||
uint buffer,
|
||||
IGpuBuffer buffer,
|
||||
long capacityBytes,
|
||||
RetryableGpuResourceRelease release)
|
||||
{
|
||||
if (buffer == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(buffer));
|
||||
ArgumentNullException.ThrowIfNull(buffer);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacityBytes);
|
||||
Buffer = buffer;
|
||||
CapacityBytes = capacityBytes;
|
||||
_release = release ?? throw new ArgumentNullException(nameof(release));
|
||||
}
|
||||
|
||||
public uint Buffer { get; }
|
||||
public IGpuBuffer Buffer { get; }
|
||||
public long CapacityBytes { get; }
|
||||
public bool IsComplete => _release.IsComplete;
|
||||
|
||||
|
|
@ -72,6 +74,19 @@ internal enum GlobalMeshCapacityResult
|
|||
/// Shared modern-rendering vertex/index buffers with reclaimable ranges.
|
||||
/// ObjectMeshManager owns allocation lifetime and releases a mesh's ranges
|
||||
/// when its zero-reference LRU entry is evicted.
|
||||
///
|
||||
/// Campaign V slice V4b moved the two backing stores onto
|
||||
/// <see cref="IGpuBuffer"/>: allocation is <see cref="IGpuDevice.CreateBuffer"/>,
|
||||
/// mesh upload is <see cref="IGpuBuffer.Upload"/>, and the grow-and-copy
|
||||
/// migration is <see cref="IGpuBuffer.CopyTo"/> — a device-side copy the Vulkan
|
||||
/// backend implements with <c>vkCmdCopyBuffer</c>. The reclaimable-range
|
||||
/// allocator, growth quanta, budgeted incremental migration, retirement-ledger
|
||||
/// gating and the dual-generation physical ceiling are unchanged; only the
|
||||
/// resource handle type moved. The vertex array object stays raw GL because a
|
||||
/// VAO has no RHI equivalent (Vulkan bakes vertex input into the pipeline) and
|
||||
/// <c>WbDrawDispatcher</c>, <c>EnvCellRenderer</c> and <c>ParticleRenderer</c>
|
||||
/// still bind <see cref="VAO"/>/<see cref="VBO"/>/<see cref="IBO"/> directly
|
||||
/// until slice V4c moves them onto the pass encoder.
|
||||
/// </summary>
|
||||
public sealed class GlobalMeshBuffer : IDisposable
|
||||
{
|
||||
|
|
@ -90,16 +105,41 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
internal const int MaximumIndexCapacity =
|
||||
(int)(MaximumIndexBufferBytes / sizeof(ushort));
|
||||
|
||||
// Retained only for the vertex array object and its attribute layout, which
|
||||
// the RHI has no verb for. Slice V4c retires this field with the raw-GL
|
||||
// dispatcher.
|
||||
private readonly GL _gl;
|
||||
private readonly IGpuDevice _device;
|
||||
private readonly GpuRetirementLedger _retirementLedger;
|
||||
private readonly GpuRetiredRangeAllocator _vertices;
|
||||
private readonly GpuRetiredRangeAllocator _indices;
|
||||
private IGpuBuffer? _vertexBuffer;
|
||||
private IGpuBuffer? _indexBuffer;
|
||||
private BufferMigration? _migration;
|
||||
private GlobalMeshMigrationAbortTicket? _migrationAbort;
|
||||
private long _retiredCapacityBytes;
|
||||
private int _storeGeneration;
|
||||
private bool _disposed;
|
||||
private RetryableResourceReleaseLedger? _disposeResources;
|
||||
|
||||
private static IGpuBuffer RequireStore(IGpuBuffer? store) =>
|
||||
store ?? throw new InvalidOperationException(
|
||||
"The global mesh arena has no live backing store.");
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4b transitional bridge. The arena owns its stores as
|
||||
/// <see cref="IGpuBuffer"/>, but its consumers — the vertex array object here,
|
||||
/// and <c>WbDrawDispatcher</c>/<c>EnvCellRenderer</c>/<c>ParticleRenderer</c>
|
||||
/// through <see cref="VBO"/>/<see cref="IBO"/> — are still raw GL until slice
|
||||
/// V4c. This is the only place that reaches through the interface, and it
|
||||
/// disappears with those consumers.
|
||||
/// </summary>
|
||||
private static GlGpuBuffer RequireGlBuffer(IGpuBuffer buffer) =>
|
||||
buffer as GlGpuBuffer
|
||||
?? throw new NotSupportedException(
|
||||
"The global mesh arena requires a GL-backed buffer while its draw paths "
|
||||
+ "still bind raw GL names (Campaign V slice V4c retires that requirement).");
|
||||
|
||||
private enum BufferKind
|
||||
{
|
||||
Vertices,
|
||||
|
|
@ -108,8 +148,8 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
|
||||
private sealed record BufferMigration(
|
||||
BufferKind Kind,
|
||||
uint OldBuffer,
|
||||
uint NewBuffer,
|
||||
IGpuBuffer OldBuffer,
|
||||
IGpuBuffer NewBuffer,
|
||||
int OldCapacity,
|
||||
int NewCapacity,
|
||||
long OldCapacityBytes,
|
||||
|
|
@ -120,8 +160,17 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
}
|
||||
|
||||
public uint VAO { get; private set; }
|
||||
public uint VBO { get; private set; }
|
||||
public uint IBO { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The vertex store's raw GL name. Transitional: the modern draw paths still
|
||||
/// bind the arena themselves until Campaign V slice V4c hands them the pass
|
||||
/// encoder, so the arena keeps publishing the backend name of the buffer it
|
||||
/// now owns as an <see cref="IGpuBuffer"/>.
|
||||
/// </summary>
|
||||
public uint VBO => _vertexBuffer is null ? 0u : RequireGlBuffer(_vertexBuffer).GlName;
|
||||
|
||||
/// <summary>The index store's raw GL name. See <see cref="VBO"/>.</summary>
|
||||
public uint IBO => _indexBuffer is null ? 0u : RequireGlBuffer(_indexBuffer).GlName;
|
||||
internal long UploadCount { get; private set; }
|
||||
internal long UploadedBytes { get; private set; }
|
||||
internal long CapacityBytes =>
|
||||
|
|
@ -193,14 +242,10 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
newBuffers);
|
||||
}
|
||||
|
||||
public GlobalMeshBuffer(GL gl)
|
||||
: this(gl, ImmediateGpuResourceRetirementQueue.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
internal GlobalMeshBuffer(GL gl, IGpuResourceRetirementQueue retirement)
|
||||
internal GlobalMeshBuffer(GL gl, IGpuDevice device, IGpuResourceRetirementQueue retirement)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
ArgumentNullException.ThrowIfNull(retirement);
|
||||
_retirementLedger = new GpuRetirementLedger(retirement);
|
||||
_vertices = new GpuRetiredRangeAllocator(InitialVertexCapacity, retirement); // ~32 MB
|
||||
|
|
@ -208,11 +253,27 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
InitBuffers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The mesh arena is simultaneously a draw source and both ends of the
|
||||
/// grow-and-copy migration, which is why <see cref="GpuBufferUsage"/> is a
|
||||
/// flags enum: Vulkan must name every usage at creation time.
|
||||
/// </summary>
|
||||
private static GpuBufferDescription DescribeStore(BufferKind kind, long sizeBytes, int generation) =>
|
||||
new(
|
||||
kind == BufferKind.Vertices
|
||||
? $"mesh-arena-vertex-{generation}"
|
||||
: $"mesh-arena-index-{generation}",
|
||||
sizeBytes,
|
||||
(kind == BufferKind.Vertices ? GpuBufferUsage.Vertex : GpuBufferUsage.Index)
|
||||
| GpuBufferUsage.TransferSource
|
||||
| GpuBufferUsage.TransferDestination,
|
||||
GpuMemoryResidency.DeviceLocal);
|
||||
|
||||
private unsafe void InitBuffers()
|
||||
{
|
||||
uint vao = 0;
|
||||
uint vbo = 0;
|
||||
uint ibo = 0;
|
||||
IGpuBuffer? vbo = null;
|
||||
IGpuBuffer? ibo = null;
|
||||
long vertexBytes = (long)_vertices.Capacity * VertexPositionNormalTexture.Size;
|
||||
long indexBytes = (long)_indices.Capacity * sizeof(ushort);
|
||||
bool vaoTracked = false;
|
||||
|
|
@ -222,18 +283,16 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
try
|
||||
{
|
||||
_gl.GenVertexArrays(1, out vao);
|
||||
_gl.GenBuffers(1, out vbo);
|
||||
_gl.GenBuffers(1, out ibo);
|
||||
if (vao == 0 || vbo == 0 || ibo == 0)
|
||||
if (vao == 0)
|
||||
throw new InvalidOperationException("OpenGL did not create the global mesh-buffer objects.");
|
||||
vbo = _device.CreateBuffer(DescribeStore(BufferKind.Vertices, vertexBytes, _storeGeneration));
|
||||
ibo = _device.CreateBuffer(DescribeStore(BufferKind.Indices, indexBytes, _storeGeneration));
|
||||
|
||||
_gl.BindVertexArray(vao);
|
||||
_gl.BindBuffer(GLEnum.ArrayBuffer, vbo);
|
||||
_gl.BufferData(GLEnum.ArrayBuffer, ToNativeSize(vertexBytes), null, GLEnum.StaticDraw);
|
||||
_gl.BindBuffer(GLEnum.ArrayBuffer, RequireGlBuffer(vbo).GlName);
|
||||
ConfigureVertexAttributes();
|
||||
|
||||
_gl.BindBuffer(GLEnum.ElementArrayBuffer, ibo);
|
||||
_gl.BufferData(GLEnum.ElementArrayBuffer, ToNativeSize(indexBytes), null, GLEnum.StaticDraw);
|
||||
_gl.BindBuffer(GLEnum.ElementArrayBuffer, RequireGlBuffer(ibo).GlName);
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
$"creating global mesh buffers ({vertexBytes} vertex bytes, {indexBytes} index bytes)");
|
||||
|
|
@ -248,13 +307,19 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
indexTracked = true;
|
||||
|
||||
VAO = vao;
|
||||
VBO = vbo;
|
||||
IBO = ibo;
|
||||
_vertexBuffer = vbo;
|
||||
_indexBuffer = ibo;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (ibo != 0) _gl.DeleteBuffer(ibo);
|
||||
if (vbo != 0) _gl.DeleteBuffer(vbo);
|
||||
// Construction rollback: nothing was ever submitted, so the physical
|
||||
// stores are released on the spot rather than deferred. Pattern-matched
|
||||
// rather than RequireGlBuffer'd so a non-GL store could never raise a
|
||||
// cast failure that masks the original construction exception.
|
||||
if (ibo is GlGpuBuffer stagedIndexStore)
|
||||
stagedIndexStore.DeleteRetired("rolling back the global index arena buffer");
|
||||
if (vbo is GlGpuBuffer stagedVertexStore)
|
||||
stagedVertexStore.DeleteRetired("rolling back the global vertex arena buffer");
|
||||
if (vao != 0) _gl.DeleteVertexArray(vao);
|
||||
if (indexTracked)
|
||||
{
|
||||
|
|
@ -287,7 +352,7 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
_gl.VertexAttribPointer(2, 2, GLEnum.Float, false, (uint)stride, (void*)(6 * sizeof(float)));
|
||||
}
|
||||
|
||||
internal unsafe GlobalMeshAllocation UploadMesh(
|
||||
internal GlobalMeshAllocation UploadMesh(
|
||||
VertexPositionNormalTexture[] vertices,
|
||||
IReadOnlyList<ushort[]> indexBatches)
|
||||
{
|
||||
|
|
@ -326,22 +391,17 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
var firstIndices = new int[indexBatches.Count];
|
||||
try
|
||||
{
|
||||
_gl.BindBuffer(GLEnum.ArrayBuffer, VBO);
|
||||
fixed (VertexPositionNormalTexture* ptr = vertices)
|
||||
{
|
||||
long vertexOffsetBytes = checked((long)vertexRange.Offset * VertexPositionNormalTexture.Size);
|
||||
long vertexUploadBytes = checked((long)vertices.Length * VertexPositionNormalTexture.Size);
|
||||
_gl.BufferSubData(
|
||||
GLEnum.ArrayBuffer,
|
||||
ToNativeOffset(vertexOffsetBytes),
|
||||
ToNativeSize(vertexUploadBytes),
|
||||
ptr);
|
||||
}
|
||||
// IGpuBuffer.Upload stages through a neutral binding point of the
|
||||
// backend's choosing, so a mesh upload can no longer disturb whichever
|
||||
// vertex array a preceding render pass happened to leave bound — the
|
||||
// property the old hand-rolled ElementArrayBuffer/CopyWriteBuffer split
|
||||
// was protecting.
|
||||
long vertexOffsetBytes = checked((long)vertexRange.Offset * VertexPositionNormalTexture.Size);
|
||||
RequireStore(_vertexBuffer).Upload(
|
||||
vertexOffsetBytes,
|
||||
MemoryMarshal.AsBytes(new ReadOnlySpan<VertexPositionNormalTexture>(vertices)));
|
||||
|
||||
// ElementArrayBuffer binding is VAO state. Use the neutral copy
|
||||
// target for staging so uploads cannot mutate whichever VAO the
|
||||
// preceding render pass happened to leave bound.
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, IBO);
|
||||
IGpuBuffer indexStore = RequireStore(_indexBuffer);
|
||||
int indexOffset = indexRange.Offset;
|
||||
for (int i = 0; i < indexBatches.Count; i++)
|
||||
{
|
||||
|
|
@ -349,22 +409,13 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
firstIndices[i] = indexOffset;
|
||||
if (batch.Length > 0)
|
||||
{
|
||||
fixed (ushort* ptr = batch)
|
||||
{
|
||||
long indexOffsetBytes = checked((long)indexOffset * sizeof(ushort));
|
||||
long indexUploadBytes = checked((long)batch.Length * sizeof(ushort));
|
||||
_gl.BufferSubData(
|
||||
GLEnum.CopyWriteBuffer,
|
||||
ToNativeOffset(indexOffsetBytes),
|
||||
ToNativeSize(indexUploadBytes),
|
||||
ptr);
|
||||
}
|
||||
long indexOffsetBytes = checked((long)indexOffset * sizeof(ushort));
|
||||
indexStore.Upload(
|
||||
indexOffsetBytes,
|
||||
MemoryMarshal.AsBytes(new ReadOnlySpan<ushort>(batch)));
|
||||
indexOffset = checked(indexOffset + batch.Length);
|
||||
}
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
$"uploading global mesh ({vertices.Length} vertices, {totalIndices} indices)");
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -603,18 +654,13 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
{
|
||||
if (chunk != 0)
|
||||
{
|
||||
_gl.BindBuffer(GLEnum.CopyReadBuffer, migration.OldBuffer);
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, migration.NewBuffer);
|
||||
_gl.CopyBufferSubData(
|
||||
GLEnum.CopyReadBuffer,
|
||||
GLEnum.CopyWriteBuffer,
|
||||
ToNativeOffset(migration.CopiedBytes),
|
||||
ToNativeOffset(migration.CopiedBytes),
|
||||
ToNativeSize(chunk));
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
$"migrating {migration.Kind} arena bytes "
|
||||
+ $"{migration.CopiedBytes:N0}..{migration.CopiedBytes + chunk:N0}");
|
||||
// Device-side copy: the live prefix never round-trips through
|
||||
// system memory. The Vulkan backend records vkCmdCopyBuffer here.
|
||||
migration.OldBuffer.CopyTo(
|
||||
migration.NewBuffer,
|
||||
migration.CopiedBytes,
|
||||
migration.CopiedBytes,
|
||||
chunk);
|
||||
migration.CopiedBytes = checked(migration.CopiedBytes + chunk);
|
||||
}
|
||||
|
||||
|
|
@ -704,33 +750,18 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
return Math.Min(totalBytes - copiedBytes, maximumCopyBytes);
|
||||
}
|
||||
|
||||
private unsafe void BeginMigration(BufferKind kind, int newCapacity, long copyBytes)
|
||||
private void BeginMigration(BufferKind kind, int newCapacity, long copyBytes)
|
||||
{
|
||||
if (_migration is not null || _migrationAbort is not null)
|
||||
throw new InvalidOperationException("Only one global mesh backing buffer may migrate at a time.");
|
||||
int oldCapacity = kind == BufferKind.Vertices ? _vertices.Capacity : _indices.Capacity;
|
||||
uint oldBuffer = kind == BufferKind.Vertices ? VBO : IBO;
|
||||
IGpuBuffer oldBuffer = RequireStore(
|
||||
kind == BufferKind.Vertices ? _vertexBuffer : _indexBuffer);
|
||||
long oldBytes = CapacityBytesFor(kind, oldCapacity);
|
||||
long newBytes = CapacityBytesFor(kind, newCapacity);
|
||||
uint newBuffer = 0;
|
||||
|
||||
try
|
||||
{
|
||||
_gl.GenBuffers(1, out newBuffer);
|
||||
if (newBuffer == 0)
|
||||
throw new InvalidOperationException($"OpenGL did not create a staged {kind} arena buffer.");
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, newBuffer);
|
||||
_gl.BufferData(GLEnum.CopyWriteBuffer, ToNativeSize(newBytes), null, GLEnum.StaticDraw);
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
$"allocating staged {kind} arena buffer ({newBytes:N0} bytes)");
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (newBuffer != 0)
|
||||
_gl.DeleteBuffer(newBuffer);
|
||||
throw;
|
||||
}
|
||||
IGpuBuffer newBuffer = _device.CreateBuffer(
|
||||
DescribeStore(kind, newBytes, checked(++_storeGeneration)));
|
||||
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
||||
GpuMemoryTracker.TrackAllocation(newBytes, GpuResourceType.Buffer);
|
||||
|
|
@ -752,12 +783,12 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
_gl.BindVertexArray(VAO);
|
||||
if (migration.Kind == BufferKind.Vertices)
|
||||
{
|
||||
_gl.BindBuffer(GLEnum.ArrayBuffer, migration.NewBuffer);
|
||||
_gl.BindBuffer(GLEnum.ArrayBuffer, RequireGlBuffer(migration.NewBuffer).GlName);
|
||||
ConfigureVertexAttributes();
|
||||
}
|
||||
else
|
||||
{
|
||||
_gl.BindBuffer(GLEnum.ElementArrayBuffer, migration.NewBuffer);
|
||||
_gl.BindBuffer(GLEnum.ElementArrayBuffer, RequireGlBuffer(migration.NewBuffer).GlName);
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(_gl, $"publishing staged {migration.Kind} arena buffer");
|
||||
}
|
||||
|
|
@ -766,12 +797,12 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
_gl.BindVertexArray(VAO);
|
||||
if (migration.Kind == BufferKind.Vertices)
|
||||
{
|
||||
_gl.BindBuffer(GLEnum.ArrayBuffer, migration.OldBuffer);
|
||||
_gl.BindBuffer(GLEnum.ArrayBuffer, RequireGlBuffer(migration.OldBuffer).GlName);
|
||||
ConfigureVertexAttributes();
|
||||
}
|
||||
else
|
||||
{
|
||||
_gl.BindBuffer(GLEnum.ElementArrayBuffer, migration.OldBuffer);
|
||||
_gl.BindBuffer(GLEnum.ElementArrayBuffer, RequireGlBuffer(migration.OldBuffer).GlName);
|
||||
}
|
||||
_gl.BindVertexArray(0);
|
||||
throw;
|
||||
|
|
@ -783,7 +814,7 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
|
||||
if (migration.Kind == BufferKind.Vertices)
|
||||
{
|
||||
VBO = migration.NewBuffer;
|
||||
_vertexBuffer = migration.NewBuffer;
|
||||
if (migration.NewCapacity > migration.OldCapacity)
|
||||
_vertices.Grow(migration.NewCapacity);
|
||||
else
|
||||
|
|
@ -791,7 +822,7 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
}
|
||||
else
|
||||
{
|
||||
IBO = migration.NewBuffer;
|
||||
_indexBuffer = migration.NewBuffer;
|
||||
if (migration.NewCapacity > migration.OldCapacity)
|
||||
_indices.Grow(migration.NewCapacity);
|
||||
else
|
||||
|
|
@ -801,11 +832,10 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
_migration = null;
|
||||
_retiredCapacityBytes = checked(_retiredCapacityBytes + migration.OldCapacityBytes);
|
||||
RetryableGpuResourceRelease oldBufferRelease =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
CreateRetryableStoreDeletion(
|
||||
migration.OldBuffer,
|
||||
migration.OldCapacityBytes,
|
||||
$"retiring replaced global {migration.Kind} arena buffer {migration.OldBuffer}");
|
||||
$"retiring replaced global {migration.Kind} arena buffer '{migration.OldBuffer.Name}'");
|
||||
_retirementLedger.Retire(new RetryableGpuResourceRelease(
|
||||
oldBufferRelease.Run,
|
||||
() => _retiredCapacityBytes = checked(
|
||||
|
|
@ -819,14 +849,41 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
_migrationAbort ??= new GlobalMeshMigrationAbortTicket(
|
||||
migration.NewBuffer,
|
||||
migration.NewCapacityBytes,
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
CreateRetryableStoreDeletion(
|
||||
migration.NewBuffer,
|
||||
migration.NewCapacityBytes,
|
||||
$"aborting staged global {migration.Kind} arena buffer {migration.NewBuffer}"));
|
||||
$"aborting staged global {migration.Kind} arena buffer '{migration.NewBuffer.Name}'"));
|
||||
RetryPendingMigrationAbort();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The arena's own flight gate — <see cref="_retirementLedger"/> and the abort
|
||||
/// ticket — already proves no submitted frame can reference the store, so the
|
||||
/// physical delete runs here rather than being deferred a second time by
|
||||
/// <see cref="IGpuBuffer.Dispose"/>. Stages match
|
||||
/// <c>TrackedGlResource.CreateRetryableBufferDeletion</c> exactly: precondition,
|
||||
/// mutation-with-validation, byte accounting, then resource-count accounting,
|
||||
/// so a driver failure re-issues only the delete and never double-counts.
|
||||
/// </summary>
|
||||
private RetryableGpuResourceRelease CreateRetryableStoreDeletion(
|
||||
IGpuBuffer buffer,
|
||||
long capacityBytes,
|
||||
string context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(buffer);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacityBytes);
|
||||
GL gl = _gl;
|
||||
return new RetryableGpuResourceRelease(
|
||||
() => GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"),
|
||||
() => RequireGlBuffer(buffer).DeleteRetired(context),
|
||||
() =>
|
||||
{
|
||||
if (capacityBytes != 0)
|
||||
GpuMemoryTracker.TrackDeallocation(capacityBytes, GpuResourceType.Buffer);
|
||||
},
|
||||
() => GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer));
|
||||
}
|
||||
|
||||
private void RetryPendingMigrationAbort()
|
||||
{
|
||||
GlobalMeshMigrationAbortTicket? ticket = _migrationAbort;
|
||||
|
|
@ -844,7 +901,7 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
BufferMigration migration = _migration
|
||||
?? throw new InvalidOperationException(
|
||||
"A staged-buffer abort ticket outlived its migration record.");
|
||||
if (migration.NewBuffer != ticket.Buffer
|
||||
if (!ReferenceEquals(migration.NewBuffer, ticket.Buffer)
|
||||
|| migration.NewCapacityBytes != ticket.CapacityBytes)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
|
|
@ -874,21 +931,11 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
return checked((int)rounded);
|
||||
}
|
||||
|
||||
private static nint ToNativeOffset(long value)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(value);
|
||||
if (IntPtr.Size == 4 && value > int.MaxValue)
|
||||
throw new NotSupportedException("The requested GPU byte offset exceeds this process's native pointer range.");
|
||||
return checked((nint)value);
|
||||
}
|
||||
|
||||
private static nuint ToNativeSize(long value)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(value);
|
||||
if (UIntPtr.Size == 4 && value > uint.MaxValue)
|
||||
throw new NotSupportedException("The requested GPU byte count exceeds this process's native pointer range.");
|
||||
return checked((nuint)value);
|
||||
}
|
||||
// The former ToNativeOffset/ToNativeSize narrowing guards went with the raw
|
||||
// glBufferSubData/glCopyBufferSubData calls they wrapped (Campaign V slice
|
||||
// V4b). IGpuBuffer speaks in long, and every arena offset and length is
|
||||
// bounded by an int-typed element capacity times a 32- or 2-byte stride, so
|
||||
// the arena can never present a value a backend cannot express.
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
@ -903,11 +950,10 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
if (_migration is { } migration)
|
||||
{
|
||||
RetryableGpuResourceRelease release =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
CreateRetryableStoreDeletion(
|
||||
migration.NewBuffer,
|
||||
migration.NewCapacityBytes,
|
||||
$"deleting staged global {migration.Kind} arena buffer {migration.NewBuffer}");
|
||||
$"deleting staged global {migration.Kind} arena buffer '{migration.NewBuffer.Name}'");
|
||||
releases.Add(("staged-migration-buffer", release.Run));
|
||||
}
|
||||
|
||||
|
|
@ -921,24 +967,22 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
GlobalMeshVaoAccounting.TrackDeallocation);
|
||||
releases.Add(("global-vao", release.Run));
|
||||
}
|
||||
if (VBO != 0)
|
||||
if (_vertexBuffer is { } vertexStore)
|
||||
{
|
||||
RetryableGpuResourceRelease release =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
VBO,
|
||||
CreateRetryableStoreDeletion(
|
||||
vertexStore,
|
||||
(long)_vertices.Capacity * VertexPositionNormalTexture.Size,
|
||||
$"deleting global mesh vertex buffer {VBO}");
|
||||
$"deleting global mesh vertex buffer '{vertexStore.Name}'");
|
||||
releases.Add(("global-vbo", release.Run));
|
||||
}
|
||||
if (IBO != 0)
|
||||
if (_indexBuffer is { } indexStore)
|
||||
{
|
||||
RetryableGpuResourceRelease release =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
IBO,
|
||||
CreateRetryableStoreDeletion(
|
||||
indexStore,
|
||||
(long)_indices.Capacity * sizeof(ushort),
|
||||
$"deleting global mesh index buffer {IBO}");
|
||||
$"deleting global mesh index buffer '{indexStore.Name}'");
|
||||
releases.Add(("global-ibo", release.Run));
|
||||
}
|
||||
_disposeResources = new RetryableResourceReleaseLedger(releases);
|
||||
|
|
@ -951,7 +995,9 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
|
||||
_migration = null;
|
||||
_migrationAbort = null;
|
||||
VAO = VBO = IBO = 0;
|
||||
VAO = 0;
|
||||
_vertexBuffer = null;
|
||||
_indexBuffer = null;
|
||||
_disposeResources = null;
|
||||
_disposed = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -406,8 +406,13 @@ namespace AcDream.App.Rendering.Wb
|
|||
public bool IsQueued { get; set; }
|
||||
}
|
||||
|
||||
public ObjectMeshManager(
|
||||
// internal, not public: IGpuDevice is an internal type (the pinned RHI
|
||||
// contract), and the shared mesh arena is created from it. Every caller
|
||||
// already lives inside AcDream.App or its InternalsVisibleTo test
|
||||
// assemblies.
|
||||
internal ObjectMeshManager(
|
||||
OpenGLGraphicsDevice graphicsDevice,
|
||||
AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
ILogger<ObjectMeshManager> logger,
|
||||
ResidencyBudgetOptions? budgets = null)
|
||||
|
|
@ -415,6 +420,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
budgets ??= ResidencyBudgetOptions.Default;
|
||||
_graphicsDevice = graphicsDevice
|
||||
?? throw new ArgumentNullException(nameof(graphicsDevice));
|
||||
ArgumentNullException.ThrowIfNull(gpuDevice);
|
||||
_preparedAssets = preparedAssets
|
||||
?? throw new ArgumentNullException(nameof(preparedAssets));
|
||||
_logger = logger
|
||||
|
|
@ -432,6 +438,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
{
|
||||
GlobalBuffer = new GlobalMeshBuffer(
|
||||
_graphicsDevice.GL,
|
||||
gpuDevice,
|
||||
_graphicsDevice.ResourceRetirement);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,16 +109,28 @@ public sealed class WbMeshAdapter
|
|||
/// Constructs the UI-Studio/tooling WB pipeline. Production composition
|
||||
/// supplies the validated pak source through the internal overload below;
|
||||
/// this explicit tooling seam retains live-DAT extraction.
|
||||
///
|
||||
/// internal, not public, since Campaign V slice V4b: the shared mesh arena
|
||||
/// allocates its backing stores from <c>IGpuDevice</c>, which is an internal
|
||||
/// type by the pinned RHI contract. Every caller already lives inside
|
||||
/// AcDream.App or its InternalsVisibleTo test assemblies.
|
||||
/// </summary>
|
||||
/// <param name="gl">Active Silk.NET GL context. Must be bound to the current
|
||||
/// thread (construction runs GL queries; call from OnLoad).</param>
|
||||
/// <param name="gpuDevice">The one process RHI device. Supplies the mesh
|
||||
/// arena's vertex/index buffers.</param>
|
||||
/// <param name="dats">acdream's shared runtime DAT facade. Tooling uses it
|
||||
/// through an explicitly owned <see cref="DatPreparedAssetSource"/>.</param>
|
||||
/// <param name="logger">Logger for the adapter; ObjectMeshManager uses
|
||||
/// NullLogger internally.</param>
|
||||
public WbMeshAdapter(GL gl, IDatReaderWriter dats, ILogger<WbMeshAdapter> logger)
|
||||
internal WbMeshAdapter(
|
||||
GL gl,
|
||||
AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice,
|
||||
IDatReaderWriter dats,
|
||||
ILogger<WbMeshAdapter> logger)
|
||||
: this(
|
||||
gl,
|
||||
gpuDevice,
|
||||
dats,
|
||||
preparedAssets: null,
|
||||
logger,
|
||||
|
|
@ -130,11 +142,13 @@ public sealed class WbMeshAdapter
|
|||
|
||||
internal static WbMeshAdapter CreateWithLiveDatPreparedAssets(
|
||||
GL gl,
|
||||
AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice,
|
||||
IDatReaderWriter dats,
|
||||
ILogger<WbMeshAdapter> logger,
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement) =>
|
||||
new(
|
||||
gl,
|
||||
gpuDevice,
|
||||
dats,
|
||||
preparedAssets: null,
|
||||
logger,
|
||||
|
|
@ -144,6 +158,7 @@ public sealed class WbMeshAdapter
|
|||
|
||||
internal WbMeshAdapter(
|
||||
GL gl,
|
||||
AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
ILogger<WbMeshAdapter> logger,
|
||||
|
|
@ -151,6 +166,7 @@ public sealed class WbMeshAdapter
|
|||
ResidencyBudgetOptions? budgets = null)
|
||||
: this(
|
||||
gl,
|
||||
gpuDevice,
|
||||
dats,
|
||||
preparedAssets,
|
||||
logger,
|
||||
|
|
@ -162,6 +178,7 @@ public sealed class WbMeshAdapter
|
|||
|
||||
private WbMeshAdapter(
|
||||
GL gl,
|
||||
AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource? preparedAssets,
|
||||
ILogger<WbMeshAdapter> logger,
|
||||
|
|
@ -170,6 +187,7 @@ public sealed class WbMeshAdapter
|
|||
ResidencyBudgetOptions budgets)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentNullException.ThrowIfNull(gpuDevice);
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
ArgumentNullException.ThrowIfNull(budgets);
|
||||
|
|
@ -212,6 +230,7 @@ public sealed class WbMeshAdapter
|
|||
// (ObjectMeshManager.PrepareMeshData try/catch at line ~589).
|
||||
meshManager = new ObjectMeshManager(
|
||||
graphicsDevice,
|
||||
gpuDevice,
|
||||
resolvedPreparedAssets,
|
||||
new ConsoleErrorLogger<ObjectMeshManager>(),
|
||||
budgets);
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ public sealed class WorldRenderCompositionTests
|
|||
|
||||
public WbMeshAdapter CreateMeshAdapter(
|
||||
GL gl,
|
||||
IGpuDevice device,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Tests.Rendering.Gpu;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
|
@ -224,14 +226,28 @@ public sealed class GpuResourceRetirementTransactionTests
|
|||
Assert.False(device.HasPendingGLWork);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4b: the mesh arena's staged store is an
|
||||
/// <see cref="IGpuBuffer"/> rather than a raw GL name, so the abort ticket
|
||||
/// retains a resource identity. The invariant under test is unchanged — the
|
||||
/// owner may only forget the migration once every release stage converged.
|
||||
/// </summary>
|
||||
private static IGpuBuffer StagedArenaBuffer(string name, long sizeBytes) =>
|
||||
new RecordingGpuBuffer(new GpuBufferDescription(
|
||||
name,
|
||||
sizeBytes,
|
||||
GpuBufferUsage.Vertex | GpuBufferUsage.TransferDestination,
|
||||
GpuMemoryResidency.DeviceLocal));
|
||||
|
||||
[Fact]
|
||||
public void MigrationAbortTicket_RetainsBufferUntilEveryReleaseStageConverges()
|
||||
{
|
||||
int deleteCalls = 0;
|
||||
int accountingCalls = 0;
|
||||
bool failAccounting = true;
|
||||
IGpuBuffer staged = StagedArenaBuffer("staged-37", 4096);
|
||||
var ticket = new GlobalMeshMigrationAbortTicket(
|
||||
buffer: 37,
|
||||
buffer: staged,
|
||||
capacityBytes: 4096,
|
||||
new RetryableGpuResourceRelease(
|
||||
() => deleteCalls++,
|
||||
|
|
@ -247,7 +263,7 @@ public sealed class GpuResourceRetirementTransactionTests
|
|||
|
||||
Assert.Throws<InvalidOperationException>(ticket.Advance);
|
||||
Assert.False(ticket.IsComplete);
|
||||
Assert.Equal((uint)37, ticket.Buffer);
|
||||
Assert.Same(staged, ticket.Buffer);
|
||||
Assert.Equal(4096, ticket.CapacityBytes);
|
||||
Assert.Equal(1, deleteCalls);
|
||||
Assert.Equal(0, accountingCalls);
|
||||
|
|
@ -267,7 +283,7 @@ public sealed class GpuResourceRetirementTransactionTests
|
|||
int accountingCalls = 0;
|
||||
bool failDeleteValidation = true;
|
||||
var ticket = new GlobalMeshMigrationAbortTicket(
|
||||
buffer: 41,
|
||||
buffer: StagedArenaBuffer("staged-41", 8192),
|
||||
capacityBytes: 8192,
|
||||
new RetryableGpuResourceRelease(
|
||||
() =>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ public sealed class WbMeshAdapterTests
|
|||
// We can't pass a real GL (no context in tests), so we verify only the
|
||||
// null-GL guard. The real pipeline is tested via integration.
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new WbMeshAdapter(gl: null!, dats: null!, logger: NullLogger<WbMeshAdapter>.Instance));
|
||||
new WbMeshAdapter(
|
||||
gl: null!,
|
||||
gpuDevice: null!,
|
||||
dats: null!,
|
||||
logger: NullLogger<WbMeshAdapter>.Instance));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue