using System.Linq; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using AcDream.App.Tests.Rendering.Gpu; using Xunit; namespace AcDream.App.Tests.Rendering; /// /// Campaign LA gate round 2 (register AD-98 filtering fidelity, 2026-08-15): /// pins — the GPU-table half /// of the fixed-canvas jagged-text fix (TextRendererLinearTwinTests pins /// the renderer-side handle swap this method feeds). /// /// /// Uses rather than /// to get a nearest-sampled /// handle without any DAT fixture — UploadRgba8 never touches the injected /// IDatReaderWriter, so the simple TextureCache(device, dats) /// constructor can run with / /// and no live GPU, matching this suite's existing renderer-test-double idiom. /// /// public sealed class TextureCacheLinearTwinTests { private static (RecordingGpuDevice device, TextureCache cache) Build() { var device = new RecordingGpuDevice(); // UploadRgba8/GetOrCreateLinearUiTwin/Dispose never touch the dats // reference — see the class doc comment. var cache = new TextureCache(device, dats: null!); // RecordingGpuDevice's own constructor registers a 1x1 default-white // placeholder (DefaultTextureSlot) — clear that registration out of the // recorded call log so each test's assertions only see the // registrations IT caused. Clear() only wipes the call log, not the // slot/sampler state, so DefaultTextureSlot itself is untouched. device.Clear(); return (device, cache); } [Fact] public void UnknownHandle_ReturnsUnchanged() { // Chrome/background art (never registered nearest) and any handle this // cache never saw (including UiTextureTableHandle.None == 0) pass // through untouched — there is nothing to swap. (_, TextureCache cache) = Build(); Assert.Equal(0u, cache.GetOrCreateLinearUiTwin(0u)); Assert.Equal(12345u, cache.GetOrCreateLinearUiTwin(12345u)); } [Fact] public void NearestHandle_GetsADifferentTwinHandle_SampledLinear() { (RecordingGpuDevice device, TextureCache cache) = Build(); byte[] rgba = new byte[4 * 4 * 4]; uint nearestHandle = cache.UploadRgba8(rgba, 4, 4, nearest: true); uint twinHandle = cache.GetOrCreateLinearUiTwin(nearestHandle); Assert.NotEqual(0u, twinHandle); Assert.NotEqual(nearestHandle, twinHandle); // The SECOND registration recorded against the device (the first is the // original nearest upload) must carry a linear filter — the whole point // of the twin. var registrations = device.OfKind().ToList(); Assert.Equal(2, registrations.Count); Assert.Equal(GpuFilter.Nearest, registrations[0].Sampler.MinFilter); Assert.Equal(GpuFilter.Linear, registrations[1].Sampler.MinFilter); Assert.Equal(GpuFilter.Linear, registrations[1].Sampler.MagFilter); // Both registrations name the SAME underlying texture — the twin reuses // the original decoded pixels rather than re-uploading. Assert.Equal(registrations[0].TextureName, registrations[1].TextureName); } [Fact] public void NearestHandle_RepeatedRequest_ReturnsTheSameCachedTwin() { (RecordingGpuDevice device, TextureCache cache) = Build(); byte[] rgba = new byte[4 * 4 * 4]; uint nearestHandle = cache.UploadRgba8(rgba, 4, 4, nearest: true); uint first = cache.GetOrCreateLinearUiTwin(nearestHandle); uint second = cache.GetOrCreateLinearUiTwin(nearestHandle); Assert.Equal(first, second); // Exactly one twin registration — the second request must not create // another device-table slot. Assert.Equal(2, device.OfKind().Count()); } [Fact] public void NonNearestUpload_HasNoTwin() { // UploadRgba8's default (nearest: false) mirrors chrome/background art: // it already samples GpuSamplerDescription.WorldRepeat (linear), so it // never enters the nearest-source table and GetOrCreateLinearUiTwin // hands the same handle straight back. (_, TextureCache cache) = Build(); byte[] rgba = new byte[4 * 4 * 4]; uint linearHandle = cache.UploadRgba8(rgba, 4, 4, nearest: false); Assert.Equal(linearHandle, cache.GetOrCreateLinearUiTwin(linearHandle)); } [Fact] public void Dispose_ReleasesTheTwinSlot() { (RecordingGpuDevice device, TextureCache cache) = Build(); byte[] rgba = new byte[4 * 4 * 4]; uint nearestHandle = cache.UploadRgba8(rgba, 4, 4, nearest: true); uint twinHandle = cache.GetOrCreateLinearUiTwin(nearestHandle); GpuTextureSlot twinSlot = UiTextureTableHandle.ToSlot(twinHandle); cache.Dispose(); Assert.Contains( device.OfKind(), release => release.Slot == twinSlot.Index); } }