fix(ui): Campaign LA gate round 2 — character-select scales as one authored canvas

Third iteration on the screen, completing AD-98. The previous substitution
stretched only the root BACKGROUND while the child widgets stayed at their
authored 800x600 pixel positions - and the background painting carries
visual anchors (the World/Characters captions are art), so the user gate
showed captions overlapping the listbox and every widget misaligned
against the stretched art.

Retail model (established at 71bf24fb): fixed-canvas pre-world screens
render at authored 800x600 and the whole composed frame stretches once at
presentation; the blitter has no stretch mode. Our equivalent now does the
same one stage earlier:

- UiRoot.FixedCanvasSize: while the char-select screen is active, the
  retained tree lays out in its authored canvas and Draw scopes a uniform
  scale onto TextRenderer.CanvasScale; the mouse entry points apply the
  exact inverse so MouseX/MouseY and every hit test live in canvas space.
- TextRenderer.AppendQuad is the single emission chokepoint - sprites,
  rects, AND glyphs scale together, including retail-authentic non-uniform
  aspect distortion and stretched text. World-space HUD stays native (the
  scale resets outside UiRoot.Draw).
- CharacterManagementUiController stops resizing Root to the viewport;
  activate/deactivate/dispose set and clear the host canvas.
- UiDatElement returns to retail-pure copy-or-tile; the interim
  StretchOwnBackgroundToFill flag is deleted.
- AD-98 updated to describe the completed substitution.

Tests: canvas-scale quad math, inverse input mapping (window click lands
on the canvas-space widget), degenerate-size guards, controller keeps
authored extent + sets/clears the canvas. App suite 5085/6 skips; live-DAT
char-select probes 3/3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-15 10:39:19 +02:00
parent 71bf24fb6f
commit 73041d7015
8 changed files with 277 additions and 106 deletions

View file

@ -206,11 +206,9 @@ public class UiDatElement : UiElement, IUiDatStateful
public uint? RuntimeImageTexture { get; set; }
/// <summary>
/// When true, this element's OWN active-state background media draws as ONE quad
/// stretched to exactly fill <see cref="UiElement.Width"/>/<see cref="UiElement.Height"/>
/// (UV span 0,0 .. 1,1) instead of the native-pixel TILE formula every other
/// <see cref="UiDatElement"/> uses. Default false — every ordinary dat chrome/
/// container element (corners, edges, drag bars, tab backdrops) keeps tiling.
/// Retail background-blit ground truth (Campaign LA gate round 2, register
/// AD-98). Every element draws its own media with the native-pixel TILE
/// formula below — retail has no per-element stretch, and neither do we.
///
/// <para>
/// <b>Campaign LA gate round 2 (issue found in the live client: the LA8
@ -247,21 +245,16 @@ public class UiDatElement : UiElement, IUiDatStateful
/// </para>
///
/// <para>
/// acdream has no offscreen fixed-resolution UI render target / present-time scale
/// pass — <see cref="AcDream.App.UI.Layout.CharacterManagementUiController"/> instead
/// resizes the MOUNTED ROOT ELEMENT itself to the live viewport (see its
/// constructor) so the screen still fills the window. This flag is the acknowledged
/// divergence for that substitution (register row: acdream resizes the element,
/// retail stretches the presented frame) — it makes the resized ROOT's own
/// background draw as one stretched quad so the VISUAL RESULT matches retail's
/// present-time stretch (no tiling) even though the MECHANISM differs. Set only on
/// a screen-level mounted root, never on an ordinary descendant/chrome element —
/// those keep the native tile formula, which IS what retail's own blit does for
/// content that lives inside the (in retail) fixed 800x600 canvas.
/// acdream's equivalent of that present-time stretch is
/// <see cref="AcDream.App.UI.UiRoot.FixedCanvasSize"/>: while a fixed-canvas
/// screen (char select) is active, the WHOLE retained tree — this tile draw
/// included — is scaled uniformly at the renderer's quad chokepoint, with the
/// inverse applied to mouse input. Elements therefore keep their authored
/// canvas-space sizes here, and the tile formula stays exactly retail's:
/// inside the authored canvas an element never exceeds its media's native
/// span unless retail itself tiled it.
/// </para>
/// </summary>
public bool StretchOwnBackgroundToFill { get; set; }
protected override void OnDraw(UiRenderContext ctx)
{
if (MediaVisible && RuntimeImageTexture is uint runtimeTexture)
@ -290,23 +283,14 @@ public class UiDatElement : UiElement, IUiDatStateful
var (tex, tw, th) = _resolve(file);
if (tex != 0 && tw != 0 && th != 0)
{
if (StretchOwnBackgroundToFill)
{
// One quad, UV 0..1 — see StretchOwnBackgroundToFill's doc comment
// for the retail mechanism this substitutes (a fixed-canvas screen
// stretched once at presentation).
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, 1, 1, Vector4.One);
}
else
{
// Normal → TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped
// UI texture) — retail's Graphic::Draw/Graphic::PutImage (0x00693b20/
// 0x00693a30) copy-or-tile blit; see StretchOwnBackgroundToFill's doc
// comment for the corrected citation (NOT ImgTex::TileCSI, which is
// land-surface-only). Overlay/Alphablend use the same blit (the sprite
// shader already alpha-blends). No Stretch mode exists in DrawModeType.
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
}
// TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped
// UI texture) — retail's Graphic::Draw/Graphic::PutImage
// (0x00693b20/0x00693a30) copy-or-tile blit; NOT ImgTex::TileCSI,
// which is land-surface-only (corrected citation, see the class
// doc). Overlay/Alphablend use the same blit (the sprite shader
// already alpha-blends). No Stretch mode exists in DrawModeType;
// whole-canvas stretching happens at UiRoot.FixedCanvasSize.
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
}
}