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; } }