feat(render): Campaign V slice V6i-2 commit 2 — world texture creation crosses to IGpuTexture
Plan §5.5.11 recorded what V4t deliberately left behind: it moved the table
ENTRY of every world texture to the device and kept CREATION with the caches,
because "creating world textures through IGpuTexture is real remaining work and
it belongs with the Vulkan world arm, which is the first thing that cannot use a
GL handle at all." §5.5.12 item 1 handed it forward and named the missing piece
exactly — "an ITextureArray implementation over IGpuTexture, not a codec",
because V6b's BlockCompressionCodec and BlockCompressionMipChain already supply
the BC chains. This is that work.
IWorldTextureArray is the seam, and the slot is what crosses it. Before this
commit ObjectMeshManager read BindlessWrapHandle/BindlessClampHandle off the
concrete GL array and interned them into the device table itself. A 64-bit
ARB_bindless_texture handle has no Vulkan spelling, so the array now answers the
question the caller was really asking — ResolveSlot(wrapping) — and each arm gets
there its own way: ManagedGLTextureArray makes the same idempotent interning call
one level down, and RhiWorldTextureArray returns a pair it registered at
construction. ReleaseTextureSlots replaces the snapshot dictionary the manager
kept for the same reason, and still runs only once physical retirement completes.
Which implementation exists is decided ONCE, by the IWorldTextureArrayFactory
composition builds — plan §3.1's no-runtime-fork rule. Everything above the seam
(capacity policy, slot allocation, ref counting, layer retirement, empty-atlas
eviction, and the whole of ObjectMeshManager's atlas policy) is written once and
branches on nothing.
Three things the RHI array does differently, each because the backends genuinely
differ rather than by choice: BC mip chains are CPU-built through
BlockCompressionMipChain, since Vulkan cannot blit into a compressed image, while
RGBA8 uses the device's blit; filtering lives in an immutable sampler rather than
a texture parameter, so both address modes are registered up front exactly as the
GL array holds two resident handles; and RGB8/A8/Rgba32f are refused at creation
with the reason named. A8 is the interesting refusal — the GL array serves it by
swizzling R into A, and a Vulkan swizzle lives in the image VIEW, which the pinned
GpuTextureDescription does not describe. A silent substitution would render wrong
and look like a shader bug.
TerrainAtlas gains the second construction path V6i drafted and reverted. The
decode is factored out and shared, so both arms read the same DATs, in the same
order, with the same resize-to-max policy; only the upload forks.
ICompositeTextureArrayBackend gains its RHI arm, which is four small methods
because that seam was already a seam.
The Vulkan arm is EXERCISED, not merely present. That is the whole reason the
V6i draft was reverted rather than landed — "built then reverted because nothing
exercised it" — and it is the same failure §5.5.12 measured twice in the
descriptor layouts. So the composition host now builds the real terrain atlas
through IGpuDevice.CreateTexture on the arm with no GL context, and creates and
releases one shared array of each format family plus one composite array at
startup. Creation only; nothing draws them. Releasing them in the same statement
covers one thing a retained bundle would not — that both slot pairs come back and
the images route through the retirement queue.
Gates: Release build; App tests 4,104 / 3 skips; strict GL offline pixel gate vs
0ca802cd 3.20e-05 (18 px of 563,200, inside the documented 9–31 px control band);
GL connected tools/run-repeat-connected-gate.ps1 -Runs 3 at 3/3 RENDERED on the
desktop witness AND 3/3 on the client capture; one Vulkan composition-host run
with VK_LAYER_KHRONOS_validation proven inserted by the loader at zero errors,
zero warnings, no [shutdown] diagnostic, and a captured frame. That run built
terrain-atlas 512x512x33 with 10 mip levels, terrain-alpha-atlas 512x512x8, RGBA8
64x64x32 (slots 3/4, 174,720 mip bytes blitted), BC1 64x64x32 (slots 5/6, 696 mip
bytes encoded) and composite 32x32x8 (slot 7).
One whole-suite run failed Issue181WallPressEquilibriumTests once; it passed
alone and did not recur in five further runs. Seven test classes mutate the same
process-global CameraDiagnostics switches with no xUnit collection isolation, and
this diff touches no camera, visibility or physics code. A separate run of the
UNCHANGED parent tree failed a different zero-allocation test, which is `#250`'s
documented class. Both are filed rather than attributed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f7344758f8
commit
c8d0f70bbe
12 changed files with 1662 additions and 95 deletions
|
|
@ -133,6 +133,12 @@ namespace AcDream.App.Rendering.Wb
|
|||
|
||||
internal AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable => _worldTextureTable;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: how a shared atlas's physical array is made.
|
||||
/// Composed once; see <see cref="IWorldTextureArrayFactory"/>.
|
||||
/// </summary>
|
||||
private readonly IWorldTextureArrayFactory _atlasArrays;
|
||||
|
||||
/// <summary>
|
||||
/// The immutable prepared-payload source is injected by composition.
|
||||
/// Production uses the validated pak; UI Studio explicitly supplies the
|
||||
|
|
@ -190,13 +196,13 @@ namespace AcDream.App.Rendering.Wb
|
|||
// the owners here makes that overlap both retryable and observable.
|
||||
private readonly List<TextureAtlasManager> _retiringAtlases = [];
|
||||
|
||||
// Campaign V slice V4t: the two bindless handles a retiring atlas had
|
||||
// when it left the live set. ManagedGLTextureArray.Dispose zeroes its
|
||||
// own copies as its first act, so the values must be snapshotted at the
|
||||
// moment of eviction to be releasable from the device's texture table
|
||||
// once physical retirement completes.
|
||||
private readonly Dictionary<TextureAtlasManager, (ulong Wrap, ulong Clamp)>
|
||||
_retiringAtlasTextureHandles = [];
|
||||
// Campaign V slice V4t recorded the two bindless handles a retiring
|
||||
// atlas held so its table entries could be released once physical
|
||||
// retirement completed. Slice V6i-2 moved that bookkeeping into the
|
||||
// array itself — a 64-bit ARB_bindless_texture handle has no Vulkan
|
||||
// spelling, so the array answers IWorldTextureArray.ReleaseTextureSlots
|
||||
// and each implementation snapshots whatever it needs. The retiring set
|
||||
// still carries the owners, which is what makes the release retryable.
|
||||
|
||||
// CPU-side cache for prepared mesh data (to avoid re-reading/decoding from DAT)
|
||||
private readonly CpuMeshUploadCache _cpuMeshCache;
|
||||
|
|
@ -459,6 +465,14 @@ namespace AcDream.App.Rendering.Wb
|
|||
// OpenGLGraphicsDevice — so the backend cast states that fact rather
|
||||
// than narrowing anything.
|
||||
_worldTextureTable = (AcDream.App.Rendering.Gpu.Gl.GlGpuDevice)gpuDevice;
|
||||
// Slice V6i-2: which physical array a shared atlas gets is decided
|
||||
// once, here. Everything below — capacity, slot allocation, ref
|
||||
// counting, layer retirement, eviction — is written against
|
||||
// IWorldTextureArray and does not branch on the backend.
|
||||
_atlasArrays = new GlWorldTextureArrayFactory(
|
||||
graphicsDevice,
|
||||
_worldTextureTable,
|
||||
logger ?? throw new ArgumentNullException(nameof(logger)));
|
||||
_preparedAssets = preparedAssets
|
||||
?? throw new ArgumentNullException(nameof(preparedAssets));
|
||||
_logger = logger
|
||||
|
|
@ -582,9 +596,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
_dirtyAtlases.Remove(victim);
|
||||
_retiringAtlases.Add(victim);
|
||||
_retiringAtlasTextureHandles[victim] = (
|
||||
victim.TextureArray.BindlessWrapHandle,
|
||||
victim.TextureArray.BindlessClampHandle);
|
||||
victim.Dispose();
|
||||
RemoveCompletedAtlasRetirements();
|
||||
return true;
|
||||
|
|
@ -611,19 +622,11 @@ namespace AcDream.App.Rendering.Wb
|
|||
// the interim per-renderer tables grew without bound instead, so
|
||||
// this is stricter than what it replaces, not looser. The
|
||||
// device defers the index itself behind its retirement queue.
|
||||
ReleaseAtlasTextureSlots(_retiringAtlases[i]);
|
||||
_retiringAtlases[i].TextureArray.ReleaseTextureSlots();
|
||||
_retiringAtlases.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseAtlasTextureSlots(TextureAtlasManager atlas)
|
||||
{
|
||||
if (!_retiringAtlasTextureHandles.Remove(atlas, out (ulong Wrap, ulong Clamp) handles))
|
||||
return;
|
||||
_worldTextureTable.ReleaseWorldTextureHandle(handles.Wrap);
|
||||
_worldTextureTable.ReleaseWorldTextureHandle(handles.Clamp);
|
||||
}
|
||||
|
||||
private void OnAtlasGpuSafeEmpty(TextureAtlasManager atlas)
|
||||
{
|
||||
if (IsDisposed || !atlas.IsGpuSafeEmpty || _safeEmptyAtlases.Contains(atlas))
|
||||
|
|
@ -2042,7 +2045,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
if (atlasManager == null)
|
||||
{
|
||||
atlasManager = new TextureAtlasManager(
|
||||
_graphicsDevice,
|
||||
_atlasArrays,
|
||||
format.Width,
|
||||
format.Height,
|
||||
format.Format,
|
||||
|
|
@ -2095,18 +2098,16 @@ namespace AcDream.App.Rendering.Wb
|
|||
legacyIndexBuffers.Add((ibo, indexArray.Length * sizeof(ushort)));
|
||||
}
|
||||
|
||||
// Campaign V slice V4t: intern the atlas's resident
|
||||
// handle into the device's one texture table and carry
|
||||
// the slot. Registration is idempotent by handle, so the
|
||||
// many batches sharing an atlas share its entry;
|
||||
// ManagedGLTextureArray still owns the residency and the
|
||||
// GL texture, and the entry is retired when the array's
|
||||
// physical retirement completes.
|
||||
ulong bindlessHandle = batch.HasWrappingUVs
|
||||
? atlasManager.TextureArray.BindlessWrapHandle
|
||||
: atlasManager.TextureArray.BindlessClampHandle;
|
||||
// Campaign V slice V4t interned the atlas's resident
|
||||
// handle into the device's one texture table here and
|
||||
// carried the slot. Slice V6i-2 asks the array for the
|
||||
// slot instead: the GL array makes the same idempotent
|
||||
// interning call one level down, and the RHI array
|
||||
// returns the entry it registered at construction. The
|
||||
// array still owns residency and the image; the entry is
|
||||
// retired when its physical retirement completes.
|
||||
AcDream.App.Rendering.Gpu.GpuTextureSlot textureSlot =
|
||||
_worldTextureTable.RegisterWorldTextureHandle(bindlessHandle);
|
||||
atlasManager.TextureArray.ResolveSlot(batch.HasWrappingUVs);
|
||||
|
||||
renderBatches.Add(new ObjectRenderBatch
|
||||
{
|
||||
|
|
@ -2804,13 +2805,12 @@ namespace AcDream.App.Rendering.Wb
|
|||
_uploadRollbacks.Clear();
|
||||
_uploadRollbackQueue.Clear();
|
||||
_globalAtlases.Clear();
|
||||
// Slice V4t: teardown drops the retiring owners without releasing
|
||||
// their table entries. The device is torn down alongside this
|
||||
// manager, so there is nothing left to recycle a slot into — and
|
||||
// asking a possibly-already-disposed device to defer work through
|
||||
// its retirement queue would turn a clean shutdown into a throw.
|
||||
_retiringAtlases.Clear();
|
||||
// Slice V4t: teardown drops the snapshots without releasing their
|
||||
// table entries. The device is torn down alongside this manager, so
|
||||
// there is nothing left to recycle a slot into — and asking a
|
||||
// possibly-already-disposed device to defer work through its
|
||||
// retirement queue would turn a clean shutdown into a throw.
|
||||
_retiringAtlasTextureHandles.Clear();
|
||||
_dirtyAtlases.Clear();
|
||||
_safeEmptyAtlases.Clear();
|
||||
_currentNonArenaGpuMemory = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue