fix(ui): Campaign LA gate round 2 — fixed-canvas stretch filters bilinearly like retail's presentation blit
AD-98's fixed-canvas stretch (73041d70) scales every retained-UI quad at
TextRenderer.AppendQuad, but the live gate reported it JAGGED — text
especially. Cause: dat-font glyph atlases and IconComposer's composited
icons upload nearest (TextureCache.UploadUiTexture's UiNearestRepeat
sampler) — correct at the native 1:1 scale (pixel-exact retail art), but
aliased once magnified 2.4x1.8. Chrome/background art was already fine:
it uploads through GpuSamplerDescription.WorldRepeat (linear) by default.
Retail's own fixed-canvas presentation is a single bilinear-filtered
frame blit, never a per-texture stretch — this closes that gap one step
earlier, at the source texture, without adding RHI surface area.
- TextureCache.GetOrCreateLinearUiTwin: lazily registers a SECOND table
slot for a nearest handle's IGpuTexture, sampled WorldRepeat (linear)
instead of nearest — no re-decode, no re-upload, no extra memory-ledger
bytes. Returns the handle unchanged for anything never registered
nearest (chrome, UiTextureTableHandle.None), so it's a cheap
unconditional probe. Twin slots are released in Dispose without
double-disposing the shared texture.
- TextRenderer.LinearTwinResolver + the DrawSprite chokepoint: swaps a
sprite's texture handle through the resolver only while
CanvasScale != One. At CanvasScale == One the resolver is never even
called — zero overhead on the ordinary in-world/UI path.
- InteractionRetainedUiComposition wires the resolver to TextureCache
right after every UiHost acquisition (the lease can hand back a host
from a prior session against a fresh TextureCache).
- AD-98's register row gets one added sentence recording the fix.
Tests: TextRendererLinearTwinTests pins the renderer-side handle-swap
seam GPU-free (segment handle selection); TextureCacheLinearTwinTests
pins twin creation/reuse/dispose against RecordingGpuDevice. App suite
5097/3 skips (Release, ACDREAM_PROBE_LIVE_MOUNT=1 live-DAT probes
included). Full solution builds clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
73041d7015
commit
308f40a3fb
6 changed files with 346 additions and 1 deletions
|
|
@ -484,6 +484,12 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
d.DebugFont,
|
||||
d.HostQuiescence));
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.UiHostAcquired);
|
||||
// AD-98 filtering fidelity: re-wired unconditionally on every
|
||||
// composition, same as the UiLocked assignment below — the lease can
|
||||
// hand back a HOST from a previous session while d.TextureCache is a
|
||||
// fresh instance for this one, so a stale resolver would keep
|
||||
// resolving twins against a disposed TextureCache.
|
||||
host.TextRenderer.LinearTwinResolver = d.TextureCache.GetOrCreateLinearUiTwin;
|
||||
inputCapture = d.RetainedInputCapture.Bind(host.Root);
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.InputCaptureBound);
|
||||
// D7 Group-C re-point (Campaign OP OP4, 2026-08-11): server
|
||||
|
|
|
|||
|
|
@ -218,6 +218,21 @@ public sealed class TextRenderer : IDisposable
|
|||
/// </summary>
|
||||
internal Vector2 CanvasScale = Vector2.One;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA gate round 2 (register AD-98 filtering fidelity): resolves a
|
||||
/// UI texture handle to its linear-sampled twin
|
||||
/// (<see cref="TextureCache.GetOrCreateLinearUiTwin"/>), consulted by
|
||||
/// <see cref="DrawSprite"/> only while <see cref="CanvasScale"/> is not One.
|
||||
/// Wired once by the composition root right after <c>TextureCache</c> exists;
|
||||
/// left null by any test/host that never sets it, in which case a scaled
|
||||
/// draw keeps sampling its original slot — nearest stays nearest, exactly
|
||||
/// today's (jagged) behavior, rather than throwing. Nearest-sampled dat-font
|
||||
/// glyphs and composited icons are the only handles this ever changes —
|
||||
/// see the resolver's own doc comment for why chrome/background art passes
|
||||
/// through unchanged.
|
||||
/// </summary>
|
||||
internal Func<uint, uint>? LinearTwinResolver { get; set; }
|
||||
|
||||
/// <summary>Begin a HUD pass. Call once per frame before any Draw* calls.</summary>
|
||||
public void Begin(Vector2 screenSize)
|
||||
{
|
||||
|
|
@ -365,6 +380,15 @@ public sealed class TextRenderer : IDisposable
|
|||
public void DrawSprite(uint texture, float x, float y, float w, float h,
|
||||
float u0, float v0, float u1, float v1, Vector4 tint)
|
||||
{
|
||||
// AD-98 filtering fidelity: while a fixed-canvas screen is stretching
|
||||
// every quad (CanvasScale != One), sample nearest-registered handles
|
||||
// through their linear twin instead — see LinearTwinResolver's doc
|
||||
// comment. The resolver itself is the identity for any handle that
|
||||
// isn't a nearest-sampled UI texture, so this is safe to call
|
||||
// unconditionally rather than needing its own "is this nearest" check.
|
||||
if (CanvasScale != Vector2.One && LinearTwinResolver is { } resolve)
|
||||
texture = resolve(texture);
|
||||
|
||||
SpriteSeg seg = OverlayMode
|
||||
? NextSpriteSeg(_overlaySpriteSegs, ref _overlaySegUsed, texture)
|
||||
: NextSpriteSeg(_spriteSegs, ref _segUsed, texture);
|
||||
|
|
|
|||
|
|
@ -53,6 +53,20 @@ public sealed class TextureCache
|
|||
// GPU texture objects/slots until process exit.
|
||||
private readonly List<GpuUiTextureEntry> _adhocGpuTextures = new();
|
||||
|
||||
// Campaign LA gate round 2 (AD-98 filtering fidelity): the ORIGINAL IGpuTexture
|
||||
// behind every handle UploadUiTexture registered nearest (dat-font glyph
|
||||
// atlases, IconComposer's composited icons). Populated at upload time so
|
||||
// GetOrCreateLinearUiTwin never has to search either keyed family above to
|
||||
// find the pixels a twin should reuse. Chrome/background art (nearest: false)
|
||||
// never enters this table — it already samples GpuSamplerDescription.WorldRepeat
|
||||
// (linear) and has no twin to create.
|
||||
private readonly Dictionary<uint, IGpuTexture> _nearestUiTextureSources = new();
|
||||
|
||||
// The LINEAR-sampled twin handle for a nearest handle, created lazily by
|
||||
// GetOrCreateLinearUiTwin on its first request and reused after. Empty for
|
||||
// the lifetime of a session that never activates a fixed-canvas screen.
|
||||
private readonly Dictionary<uint, uint> _linearUiTwinHandles = new();
|
||||
|
||||
private readonly CompositeTextureArrayCache? _compositeTextures;
|
||||
private bool _destinationRevealUploadPriority;
|
||||
|
||||
|
|
@ -359,6 +373,14 @@ public sealed class TextureCache
|
|||
|
||||
IGpuSampler sampler = _device.CreateSampler(nearest ? UiNearestRepeat : GpuSamplerDescription.WorldRepeat);
|
||||
GpuTextureSlot slot = _device.RegisterTexture(texture, sampler);
|
||||
uint handle = UiTextureTableHandle.FromSlot(slot);
|
||||
if (nearest)
|
||||
{
|
||||
// AD-98 filtering fidelity: remember the source texture under its
|
||||
// handle so a fixed-canvas screen can request a linear twin of it
|
||||
// later without re-decoding. See GetOrCreateLinearUiTwin.
|
||||
_nearestUiTextureSources[handle] = texture;
|
||||
}
|
||||
return new GpuUiTextureEntry(texture, slot, glName, decoded.Width, decoded.Height);
|
||||
}
|
||||
catch
|
||||
|
|
@ -368,6 +390,60 @@ public sealed class TextureCache
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA gate round 2 (register AD-98): the LINEAR-sampled twin of a
|
||||
/// nearest-sampled UI texture handle, created and table-registered the first
|
||||
/// time it is requested and reused after.
|
||||
///
|
||||
/// <para>
|
||||
/// Nearest is correct at the UI's native 1:1 scale — it is what makes
|
||||
/// dat-font glyphs and composited item icons pixel-exact retail art. Retail's
|
||||
/// own fixed-canvas pre-world screens never stretch a source texture at all:
|
||||
/// they compose at authored size and the WHOLE FRAME goes through a single
|
||||
/// bilinear-filtered presentation blit (see
|
||||
/// <see cref="AcDream.App.UI.UiRoot.FixedCanvasSize"/>'s doc comment for the
|
||||
/// retail citation). acdream has no present-time frame stretch to hang that
|
||||
/// on, so the equivalent has to live one step earlier, at the source texture:
|
||||
/// while <see cref="TextRenderer.CanvasScale"/> is scaling the composed quads
|
||||
/// themselves, this method gives a nearest handle a same-pixels twin sampled
|
||||
/// LINEAR instead, so the stretch softens the way retail's frame blit did
|
||||
/// rather than aliasing.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Returns <paramref name="handle"/> UNCHANGED for anything this cache never
|
||||
/// registered nearest — chrome/background art already samples
|
||||
/// <see cref="GpuSamplerDescription.WorldRepeat"/> (linear) and has nothing to
|
||||
/// swap, and <see cref="UiTextureTableHandle.None"/> (DrawFill's untextured
|
||||
/// branch) is not a texture at all. Callers do not need to know which case
|
||||
/// they're in: this is a cheap dictionary probe either way, so
|
||||
/// <see cref="TextRenderer.DrawSprite"/> can call it unconditionally whenever
|
||||
/// the canvas is scaled.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The twin reuses the ORIGINAL <see cref="IGpuTexture"/> — no re-decode, no
|
||||
/// second upload, no additional bytes tracked in the memory ledger — and
|
||||
/// occupies one more device texture-table slot, exactly the shape
|
||||
/// <see cref="RegisterWorldSurface"/>'s (surface, wrap) keying already uses to
|
||||
/// register one texture under two samplers. Lazy: a session that never
|
||||
/// activates a fixed-canvas screen never creates one.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal uint GetOrCreateLinearUiTwin(uint handle)
|
||||
{
|
||||
if (!_nearestUiTextureSources.TryGetValue(handle, out IGpuTexture? texture))
|
||||
return handle;
|
||||
if (_linearUiTwinHandles.TryGetValue(handle, out uint twin))
|
||||
return twin;
|
||||
|
||||
IGpuSampler linearSampler = _device.CreateSampler(GpuSamplerDescription.WorldRepeat);
|
||||
GpuTextureSlot twinSlot = _device.RegisterTexture(texture, linearSampler);
|
||||
uint twinHandle = UiTextureTableHandle.FromSlot(twinSlot);
|
||||
_linearUiTwinHandles[handle] = twinHandle;
|
||||
return twinHandle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The identity a UI upload is accounted under. There is no GL name on the
|
||||
/// Vulkan-only backend, so a descending synthetic counter supplies one; the
|
||||
|
|
@ -994,6 +1070,15 @@ public sealed class TextureCache
|
|||
|
||||
_paletteIndexedByTexture.Clear();
|
||||
|
||||
// Campaign LA gate round 2 (AD-98): linear twin slots. Each one is a
|
||||
// SECOND table registration of a texture another family below owns and
|
||||
// disposes — release the slot here, before that texture goes away, and
|
||||
// never touch the texture itself (that would double-dispose it).
|
||||
foreach (uint twinHandle in _linearUiTwinHandles.Values)
|
||||
_device.ReleaseTextureSlot(UiTextureTableHandle.ToSlot(twinHandle));
|
||||
_linearUiTwinHandles.Clear();
|
||||
_nearestUiTextureSources.Clear();
|
||||
|
||||
// RenderSurface (UI sprite) textures — Campaign V slice V4a: each
|
||||
// entry's IGpuTexture.Dispose() releases the underlying GL name
|
||||
// through the device's own retirement queue, so only the memory-
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue