using System; using System.Linq; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using AcDream.App.Tests.Rendering.Gpu; namespace AcDream.App.Tests.Rendering; /// /// Campaign V slice V6i-2: the composite array pool's backend-neutral arm. /// /// Plan §5.5.12 item 1 recorded that ICompositeTextureArrayBackend /// "is already a seam and takes an RHI backend directly", which is why this arm /// is four small methods rather than a port. What is worth pinning is the /// contract the cache above depends on: create returns a resource whose slot is /// live, the release order is entry-then-image, and a resource made by one /// backend is never handed to the other. /// public sealed class RhiCompositeTextureArrayBackendTests { private static byte[] Rgba(int width, int height) => new byte[width * height * 4]; [Fact] public void CreateProducesASingleLevelArrayRegisteredIntoTheTable() { using var device = new RecordingGpuDevice(); var backend = new RhiCompositeTextureArrayBackend(device); CompositeTextureArrayResource resource = backend.Create(32, 32, 8); Assert.True(resource.Slot.IsAssigned); Assert.Equal(32 * 32 * 4 * 8, resource.Bytes); // The GL identity fields are meaningless on this arm and say so. Assert.Equal(0u, resource.Name); Assert.Equal(0ul, resource.Handle); RecordingGpuTexture image = Assert.IsType(resource.Image); Assert.Equal(GpuTextureKind.Texture2DArray, image.Kind); Assert.Equal(GpuTextureFormat.Rgba8Unorm, image.Format); Assert.Equal(8, image.LayerCount); // Composites are the surfaces retail releases the moment they are built; // a mip chain would be paid for nothing. Assert.Equal(1, image.MipLevelCount); } [Fact] public void UploadWritesLevelZeroOfTheNamedLayer() { using var device = new RecordingGpuDevice(); var backend = new RhiCompositeTextureArrayBackend(device); CompositeTextureArrayResource resource = backend.Create(32, 32, 4); backend.Upload(resource, 3, Rgba(32, 32)); RecordingGpuTexture image = Assert.IsType(resource.Image); Assert.Equal([(0, 3, 32 * 32 * 4)], image.Uploads); } [Fact] public void ReleaseRetiresTheTableEntryBeforeTheImage() { using var device = new RecordingGpuDevice(); var backend = new RhiCompositeTextureArrayBackend(device); int before = device.LiveTextureSlotCount; CompositeTextureArrayResource resource = backend.Create(32, 32, 4); Assert.Equal(before + 1, device.LiveTextureSlotCount); backend.MakeNonResident(resource); Assert.Equal(before, device.LiveTextureSlotCount); Assert.False(Assert.IsType(resource.Image).IsDisposed); backend.Delete(resource); Assert.True(Assert.IsType(resource.Image).IsDisposed); } /// /// A GL-made resource reaching this backend is a composition error, not a /// runtime condition — and the message says which backend owns it rather /// than dereferencing null. /// [Fact] public void AGlResourceIsRefusedRatherThanDereferenced() { using var device = new RecordingGpuDevice(); var backend = new RhiCompositeTextureArrayBackend(device); var foreign = new CompositeTextureArrayResource { Name = 7, Handle = 0xDEAD, Slot = new GpuTextureSlot(3), Width = 32, Height = 32, Capacity = 1, Bytes = 32 * 32 * 4, }; Assert.Throws(() => backend.Upload(foreign, 0, Rgba(32, 32))); Assert.Throws(() => backend.Delete(foreign)); } /// /// The cache caps every array at 64 layers, so the layer ceiling this /// backend reports is never the binding constraint — which is what lets it /// report Vulkan's guaranteed minimum instead of a capability field the /// pinned record does not carry. /// [Fact] public void TheReportedLayerCeilingExceedsTheCachesOwnCap() { using var device = new RecordingGpuDevice(); var backend = new RhiCompositeTextureArrayBackend(device); Assert.True(backend.MaximumArrayLayers >= CompositeTextureArrayCache.MaximumLayersPerArray); } }