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
|
|
@ -127,9 +127,20 @@ internal readonly record struct CompositeTextureKey(
|
|||
|
||||
internal sealed class CompositeTextureArrayResource
|
||||
{
|
||||
/// <summary>The GL texture name, or 0 on the backend-neutral arm.</summary>
|
||||
public required uint Name { get; init; }
|
||||
|
||||
/// <summary>The resident bindless handle, or 0 on the backend-neutral arm.</summary>
|
||||
public required ulong Handle { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: the RHI image, on the arm that owns one. Null on
|
||||
/// GL, where <see cref="Name"/> and <see cref="Handle"/> are the identity.
|
||||
/// The cache above touches neither — it only ever hands a resource back to
|
||||
/// the backend that made it.
|
||||
/// </summary>
|
||||
public Gpu.IGpuTexture? Image { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4t: this array's entry in the device texture table.
|
||||
/// The backend that made <see cref="Handle"/> resident also interned it, so
|
||||
|
|
@ -338,6 +349,108 @@ internal sealed unsafe class GlCompositeTextureArrayBackend : ICompositeTextureA
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: the backend-neutral composite array backend.
|
||||
///
|
||||
/// <para>Plan §5.5.12 item 1 noted that <see cref="ICompositeTextureArrayBackend"/>
|
||||
/// "is already a seam and takes an RHI backend directly" — this is that arm. It
|
||||
/// is deliberately the smallest of the three texture paths: one mip level, one
|
||||
/// sampler, RGBA8 only, no residency negotiation. The composited 32×32 item art
|
||||
/// and per-entity material surfaces it holds are exactly the textures retail
|
||||
/// releases the moment the surface is built, so a mip chain would be paid for
|
||||
/// nothing.</para>
|
||||
///
|
||||
/// <para><b>Clamped, matching what the GL backend's texture parameters say for
|
||||
/// the modes that matter.</b> The GL backend sets <c>Repeat</c>, but a composite
|
||||
/// array's neighbouring layers are unrelated surfaces; the wrap mode only
|
||||
/// affects UVs outside [0,1], which the composite path does not generate. The
|
||||
/// slice that draws these on Vulkan is the one that can see a difference, and
|
||||
/// it inherits a named decision rather than an accident.</para>
|
||||
/// </summary>
|
||||
internal sealed class RhiCompositeTextureArrayBackend : ICompositeTextureArrayBackend
|
||||
{
|
||||
private readonly Gpu.IGpuDevice _device;
|
||||
private readonly Gpu.IGpuSampler _sampler;
|
||||
|
||||
internal RhiCompositeTextureArrayBackend(Gpu.IGpuDevice device)
|
||||
{
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
_sampler = device.CreateSampler(Gpu.GpuSamplerDescription.WorldClamp with
|
||||
{
|
||||
MipFilter = Gpu.GpuMipFilter.None,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The GL backend reads <c>GL_MAX_ARRAY_TEXTURE_LAYERS</c>. The pinned
|
||||
/// <see cref="Gpu.GpuCapabilityRecord"/> has no array-layer field and §3.3 is
|
||||
/// frozen, so this reports Vulkan's guaranteed <c>maxImageArrayLayers</c>
|
||||
/// minimum of 256. That is not a limitation in practice:
|
||||
/// <see cref="CompositeTextureArrayCache.MaximumLayersPerArray"/> caps every
|
||||
/// array at 64, so the true device limit is never the binding constraint.
|
||||
/// </summary>
|
||||
public int MaximumArrayLayers => 256;
|
||||
|
||||
public CompositeTextureArrayResource Create(int width, int height, int capacity)
|
||||
{
|
||||
Gpu.IGpuTexture? image = null;
|
||||
try
|
||||
{
|
||||
image = _device.CreateTexture(new Gpu.GpuTextureDescription(
|
||||
$"composite-array-{width}x{height}x{capacity}",
|
||||
Gpu.GpuTextureKind.Texture2DArray,
|
||||
Gpu.GpuTextureFormat.Rgba8Unorm,
|
||||
width,
|
||||
height,
|
||||
capacity,
|
||||
MipLevelCount: 1));
|
||||
Gpu.GpuTextureSlot slot = _device.RegisterTexture(image, _sampler);
|
||||
return new CompositeTextureArrayResource
|
||||
{
|
||||
Name = 0,
|
||||
Handle = 0,
|
||||
Image = image,
|
||||
Slot = slot,
|
||||
Width = width,
|
||||
Height = height,
|
||||
Capacity = capacity,
|
||||
Bytes = checked((long)width * height * 4L * capacity),
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
image?.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Upload(CompositeTextureArrayResource resource, int layer, byte[] rgba) =>
|
||||
RequireImage(resource).Upload(0, layer, rgba);
|
||||
|
||||
/// <summary>
|
||||
/// On GL this makes a bindless handle non-resident after retiring its table
|
||||
/// entry. There is no residency on the RHI arm, so retiring the entry is the
|
||||
/// whole of it — and it is a slot the device defers behind its own
|
||||
/// retirement queue, exactly as the GL arm's release does.
|
||||
/// </summary>
|
||||
public void MakeNonResident(CompositeTextureArrayResource resource)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(resource);
|
||||
if (resource.Slot.IsAssigned)
|
||||
_device.ReleaseTextureSlot(resource.Slot);
|
||||
}
|
||||
|
||||
public void Delete(CompositeTextureArrayResource resource) => RequireImage(resource).Dispose();
|
||||
|
||||
private static Gpu.IGpuTexture RequireImage(CompositeTextureArrayResource resource)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(resource);
|
||||
return resource.Image
|
||||
?? throw new InvalidOperationException(
|
||||
"This composite resource was created by the GL backend and has no RHI image.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pools per-entity material composites into dimension-compatible texture
|
||||
/// arrays. Retail releases the owning CSurface reference immediately. This
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue