using System.Numerics; 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 's texture-swap seam — /// , consulted only while /// is not — the /// half of the fix that doesn't need a live GPU. The user-visible symptom this /// answers: the fixed-canvas char-select stretch (73041d70) reported JAGGED /// text, because dat-font glyph atlases upload nearest (pixel-exact at 1:1) and /// stayed nearest even while every quad was being magnified 2.4×1.8. The actual /// linear-twin CREATION lives in TextureCache.GetOrCreateLinearUiTwin /// (GPU-backed, see TextureCacheLinearTwinTests); this file proves the /// RENDERER SIDE of the seam — segment handle selection — using a fake resolver /// so the assertion doesn't depend on TextureCache's own wiring being correct. /// public sealed class TextRendererLinearTwinTests { private sealed class NullGpuFrameSource : ICurrentGpuFrameSource { public IGpuFrame? CurrentFrame => null; } private static TextRenderer BuildRenderer() { var device = new RecordingGpuDevice(); var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused"); renderer.Begin(new Vector2(800f, 600f)); return renderer; } [Fact] public void CanvasScaleOne_DrawSprite_NeverConsultsResolver_KeepsOriginalHandle() { // At the native 1:1 scale (every in-world/UI frame today) the resolver // must not even be CALLED, not just "called and return the same value" — // a resolver that throws proves the ordinary path pays zero overhead for // a feature it never activates, exactly the "lazy" design constraint the // Campaign LA round-2 follow-up was scoped to. TextRenderer renderer = BuildRenderer(); renderer.LinearTwinResolver = _ => throw new System.InvalidOperationException( "LinearTwinResolver must not be consulted while CanvasScale == One."); renderer.DrawSprite(5u, 0, 0, 10, 10, 0, 0, 1, 1, Vector4.One); var seg = Assert.Single(renderer.DebugSpriteSegments); Assert.Equal(5u, seg.Texture); } [Fact] public void CanvasScaleNotOne_DrawSprite_SwapsHandleThroughResolver() { // The fixed-canvas case: CanvasScale is set by UiRoot.Draw for the // duration of a fixed-canvas screen's tree. A nearest-registered dat-font // glyph handle (5) must draw through its linear twin (999), not itself. TextRenderer renderer = BuildRenderer(); renderer.CanvasScale = new Vector2(2.4f, 1.8f); renderer.LinearTwinResolver = handle => handle == 5u ? 999u : handle; renderer.DrawSprite(5u, 0, 0, 10, 10, 0, 0, 1, 1, Vector4.One); var seg = Assert.Single(renderer.DebugSpriteSegments); Assert.Equal(999u, seg.Texture); } [Fact] public void CanvasScaleNotOne_ResolverIsIdentityForUnknownHandles() { // Chrome/background art (never registered nearest) and // UiTextureTableHandle.None (DrawFill's untextured branch) are the // majority of scaled-canvas draws. TextureCache.GetOrCreateLinearUiTwin // returns them unchanged; this pins that TextRenderer forwards whatever // the resolver returns without a separate "was this swapped" branch. TextRenderer renderer = BuildRenderer(); renderer.CanvasScale = new Vector2(2.4f, 1.8f); renderer.LinearTwinResolver = handle => handle == 5u ? 999u : handle; renderer.DrawSprite(7u, 0, 0, 10, 10, 0, 0, 1, 1, Vector4.One); var seg = Assert.Single(renderer.DebugSpriteSegments); Assert.Equal(7u, seg.Texture); } [Fact] public void CanvasScaleNotOne_NoResolverWired_KeepsOriginalHandle_DoesNotThrow() { // A host/test that never wires LinearTwinResolver (every existing // TextRenderer construction site before this change, and any future // test double) must keep drawing — nearest stays nearest, the // pre-existing (if jagged) behavior, rather than a null-reference // failure the moment a fixed-canvas screen activates. TextRenderer renderer = BuildRenderer(); renderer.CanvasScale = new Vector2(2.4f, 1.8f); renderer.DrawSprite(5u, 0, 0, 10, 10, 0, 0, 1, 1, Vector4.One); var seg = Assert.Single(renderer.DebugSpriteSegments); Assert.Equal(5u, seg.Texture); } }