using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Wb;
using Chorizite.Core.Render.Enums;
namespace AcDream.App.Rendering;
///
/// Campaign V slice V6i-2: one shared world texture array of each format family
/// and one composite array, created through the RHI at startup on a backend
/// that has no GL context.
///
/// Why this exists. The slice's whole claim is that world texture
/// CREATION now reaches . A creation path nothing
/// constructs is a claim, not a fact — and plan §5.5.12 recorded the cost of
/// exactly that shape twice over: UniformSkyParams and the terrain clip
/// block were both wrong for months because no Vulkan pipeline had ever been
/// built from them. The parked TerrainAtlas draft this slice reuses was
/// itself reverted for the same reason — "built then reverted because nothing
/// exercised it".
///
/// So the Vulkan composition host builds these at startup. They are never
/// drawn — no world renderer exists on that arm until the slice that ports the
/// dispatchers — but they ARE created, uploaded, mip-chained and registered into
/// the device's texture table, which puts every one of those calls under the
/// validation layer and inside the ownership ledger that teardown converges.
/// That is the difference between a gated path and an untested one.
///
/// What the two arrays cover between them. RGBA8 exercises
/// — the device-side blit — and BC1
/// exercises the CPU chain, because Vulkan cannot blit into a compressed image
/// and is what stands in for that.
/// Those are the two upload shapes the world's shared atlases actually take.
///
internal sealed class BackendNeutralWorldTextures : IDisposable
{
private readonly IWorldTextureArray _rgba;
private readonly IWorldTextureArray _compressed;
private readonly ICompositeTextureArrayBackend _compositeBackend;
private readonly CompositeTextureArrayResource _composite;
private bool _disposed;
///
/// The size class the exercise uses. Small enough to cost nothing at
/// startup, large enough that both arrays have a real multi-level mip chain
/// (64×64 is 7 levels) rather than the degenerate single-level case.
///
private const int ArrayExtent = 64;
/// Composite surfaces are the retail 32×32 item art size class.
private const int CompositeExtent = 32;
private const int CompositeLayers = 8;
internal static BackendNeutralWorldTextures Create(IGpuDevice device, Action log)
{
ArgumentNullException.ThrowIfNull(device);
ArgumentNullException.ThrowIfNull(log);
return new BackendNeutralWorldTextures(device, log);
}
///
/// Creates the bundle and releases it in the same statement.
///
/// Nothing is retained on purpose. The terrain atlas — which the same
/// slice moved onto — is the retained case: two
/// real arrays registered in the device table for the whole run and torn
/// down at shutdown. What this covers instead is the shared-atlas and
/// composite CREATION shapes, and creating-then-releasing exercises one more
/// thing a retained bundle would not: that both slot pairs come back and the
/// images route through the retirement queue, so the ownership ledger this
/// gate reads converges by construction rather than by assertion.
///
internal static void Exercise(IGpuDevice device, Action log)
{
using BackendNeutralWorldTextures textures = Create(device, log);
}
private BackendNeutralWorldTextures(IGpuDevice device, Action log)
{
var arrays = new RhiWorldTextureArrayFactory(device);
// Capacity comes from the same target-bytes policy the shared atlases
// use on GL, so the exercise allocates what production would.
int rgbaLayers = TextureAtlasManager.CalculateInitialCapacity(
ArrayExtent,
ArrayExtent,
TextureFormat.RGBA8);
int compressedLayers = TextureAtlasManager.CalculateInitialCapacity(
ArrayExtent,
ArrayExtent,
TextureFormat.DXT1);
IWorldTextureArray? rgba = null;
IWorldTextureArray? compressed = null;
ICompositeTextureArrayBackend? compositeBackend = null;
CompositeTextureArrayResource? composite = null;
try
{
rgba = arrays.CreateClampedArray(TextureFormat.RGBA8, ArrayExtent, ArrayExtent, rgbaLayers);
rgba.UpdateLayer(0, OpaqueRgba(ArrayExtent, ArrayExtent), null, null);
long rgbaMipBytes = rgba.ProcessDirtyUpdates();
compressed = arrays.CreateClampedArray(TextureFormat.DXT1, ArrayExtent, ArrayExtent, compressedLayers);
compressed.UpdateLayer(0, OpaqueBc1(ArrayExtent, ArrayExtent), null, null);
long compressedMipBytes = compressed.ProcessDirtyUpdates();
compositeBackend = new RhiCompositeTextureArrayBackend(device);
composite = compositeBackend.Create(CompositeExtent, CompositeExtent, CompositeLayers);
compositeBackend.Upload(composite, 0, OpaqueRgba(CompositeExtent, CompositeExtent));
_rgba = rgba;
_compressed = compressed;
_compositeBackend = compositeBackend;
_composite = composite;
log(
"[V6i-2] world texture creation on the RHI: "
+ $"RGBA8 {ArrayExtent}x{ArrayExtent}x{rgbaLayers} "
+ $"(wrap {rgba.ResolveSlot(true)}, clamp {rgba.ResolveSlot(false)}, "
+ $"{rgbaMipBytes} mip bytes blitted); "
+ $"BC1 {ArrayExtent}x{ArrayExtent}x{compressedLayers} "
+ $"(wrap {compressed.ResolveSlot(true)}, clamp {compressed.ResolveSlot(false)}, "
+ $"{compressedMipBytes} mip bytes encoded); "
+ $"composite {CompositeExtent}x{CompositeExtent}x{CompositeLayers} ({composite.Slot}).");
}
catch
{
if (composite is not null)
{
compositeBackend!.MakeNonResident(composite);
compositeBackend.Delete(composite);
}
compressed?.Dispose();
rgba?.Dispose();
throw;
}
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_compositeBackend.MakeNonResident(_composite);
_compositeBackend.Delete(_composite);
_compressed.Dispose();
_rgba.Dispose();
}
private static byte[] OpaqueRgba(int width, int height)
{
var pixels = new byte[width * height * 4];
Array.Fill(pixels, (byte)0xFF);
return pixels;
}
///
/// A BC1 block whose two endpoints are white and whose selectors are all
/// zero, repeated across the level — a legal, fully-opaque payload of
/// exactly the size the codec expects. The point is the upload and the
/// chain, not the pixels.
///
private static byte[] OpaqueBc1(int width, int height)
{
int blocks = Math.Max(1, (width + 3) / 4) * Math.Max(1, (height + 3) / 4);
var data = new byte[blocks * 8];
for (int block = 0; block < blocks; block++)
{
int at = block * 8;
// Two RGB565 endpoints, both white (0xFFFF), then four selector
// bytes of zero: every texel takes endpoint 0.
data[at + 0] = 0xFF;
data[at + 1] = 0xFF;
data[at + 2] = 0xFF;
data[at + 3] = 0xFF;
}
return data;
}
}