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:
Erik 2026-08-15 11:05:59 +02:00
parent 73041d7015
commit 308f40a3fb
6 changed files with 346 additions and 1 deletions

View file

@ -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