- Split the two hermetic RetailMarkupIconResolver memoization tests (and their counting fakes) out of the Lane=InstalledDat class into a new untagged RetailMarkupIconResolverMemoizationTests.cs so CI's portable filter (Lane!=InstalledDat) actually runs them. - PluginSidePanel: move the entry button's Anchors = AnchorEdges.None from the Add() call site into PluginShelfButton's own constructor (same comment carried over) so a second construction path cannot miss it. - UiRectOutlinePainterOrderTests: assert the back panel's border segment carries exactly 4 quads (24 vertices, FloatsPerVertex each) so a partial outline cannot pass the painter-order check. - RetailMarkupIconResolver: document the type as UI-thread-only (every caller is a draw-time icon source) and bound the MISS cache to 256 entries with FIFO eviction — HIT entries stay unbounded (bounded by the DAT's own surface count already). New test proves the 257th distinct miss evicts the first (re-probe count rises); verified failing first against the un-bounded code (Expected 258, Actual 257) before restoring the fix. - docs/plugin-ui-markup.md: split the icon-binding row's failure mode into Build-time (missing property only — the binder never checks CLR type) vs. draw-time (a resolved value that cannot convert to a number throws from the draw, not from Build). - docs/ISSUES.md: filed #486 (credits picture scroll frozen by the per-draw anchor pass) and #487 (radar compass tokens candidate, same mechanism, unconfirmed); corrected #461's causality — the graceful logout/reveal-cancel log lines are printed by LiveSessionController.Tick's catch -> StopAfterFailure -> StopCore AFTER the motion-update exception, then it rethrows, so the logout is a consequence of the crash, not its cause; real chain is the #462 stalled login-reveal materialization leaving PlayerMovementController in RuntimeOwnedDormant outside its SetPosition ground phase when an inbound 0xF74C arrives. - Plan doc: recorded the three fix-round commits' verdicts (all PASS) and the Smoke-plugin cleanup commit SHA in the Review ledger, plus a pointer to the two newly filed issues. Verified: dotnet build AcDream.slnx -c Release (0/0), targeted filter 85/0/0, full App suite 7364 passed / 97 skipped / 36 failed (36 pre-existing InstalledDat/Manual/Linux-only failures, unchanged by name from baseline; net +1 passed test from the new eviction test). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
125 lines
5.4 KiB
C#
125 lines
5.4 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Gpu;
|
|
using AcDream.App.Tests.Rendering.Gpu;
|
|
using AcDream.App.UI;
|
|
|
|
namespace AcDream.App.Tests.UI;
|
|
|
|
/// <summary>
|
|
/// Owner-reported symptom: with a plugin window BEHIND the inventory window,
|
|
/// the plugin button's BORDER outline drew on top of the inventory's paperdoll
|
|
/// even though the inventory window was added (and so composited) LATER. Root
|
|
/// cause — <see cref="TextRenderer"/> used to composite in three buckets per
|
|
/// layer: submission-ordered sprite segments, THEN every untextured
|
|
/// <see cref="TextRenderer.DrawRect"/> quad (regardless of when it was
|
|
/// submitted), THEN debug text. <see cref="UiRenderContext.DrawRect"/> (and so
|
|
/// <see cref="UiRenderContext.DrawRectOutline"/>, which every
|
|
/// <c>BorderColor</c> outline in the retained UI — <see cref="UiPanel"/>,
|
|
/// <see cref="UiMarkupList"/> — goes through) forwarded into that rect
|
|
/// bucket, so every outline composited above every window's sprite content
|
|
/// drawn after it, no matter the actual paint order.
|
|
///
|
|
/// <para>
|
|
/// This pins the mechanism directly against the real <see cref="UiPanel"/>
|
|
/// draw path: a back panel with a visible border, then a front sibling
|
|
/// (added AFTER — later paint order) that draws an opaque sprite over the
|
|
/// same screen rect. The back panel's border must composite UNDER the front
|
|
/// sprite, exactly like <see cref="UiRenderContext.DrawFill"/> already does
|
|
/// for panel backgrounds.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class UiRectOutlinePainterOrderTests
|
|
{
|
|
private sealed class TestElement : UiElement { }
|
|
|
|
private sealed class NullGpuFrameSource : ICurrentGpuFrameSource
|
|
{
|
|
public IGpuFrame? CurrentFrame => null;
|
|
}
|
|
|
|
private static (TextRenderer renderer, UiRenderContext ctx) MakeContext(float w, float h)
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused");
|
|
renderer.Begin(new Vector2(w, h));
|
|
var ctx = new UiRenderContext(renderer, new Vector2(w, h));
|
|
return (renderer, ctx);
|
|
}
|
|
|
|
[Fact]
|
|
public void BackPanelBorder_ComposesUnderAFrontSpriteAddedLater()
|
|
{
|
|
var root = new TestElement { Width = 200f, Height = 200f };
|
|
|
|
// Back window: added FIRST, drawn first (lower paint order). Its
|
|
// border is the ONLY thing it draws — background left transparent so
|
|
// any leaked geometry in the assertions below can only be the border.
|
|
var backPanel = new UiPanel
|
|
{
|
|
Left = 0f, Top = 0f, Width = 100f, Height = 60f,
|
|
BackgroundColor = default,
|
|
BorderColor = new Vector4(1f, 1f, 1f, 1f),
|
|
BorderThickness = 2f,
|
|
};
|
|
|
|
// Front window: added AFTER (drawn later / higher paint order) and
|
|
// covers the SAME screen rect with an opaque sprite — the inventory
|
|
// window's paperdoll, standing in for MossTank's button border.
|
|
const uint frontTexture = 55u;
|
|
var frontSprite = new UiSolidSpriteFill
|
|
{
|
|
Left = 0f, Top = 0f, Width = 100f, Height = 60f,
|
|
SpriteId = frontTexture,
|
|
SpriteResolve = id => (id, 8, 8),
|
|
};
|
|
|
|
root.AddChild(backPanel);
|
|
root.AddChild(frontSprite);
|
|
|
|
var (renderer, ctx) = MakeContext(200f, 200f);
|
|
root.DrawSelfAndChildren(ctx);
|
|
|
|
var segs = renderer.DebugSpriteSegmentVerts;
|
|
|
|
int frontIndex = -1;
|
|
for (int i = 0; i < segs.Count; i++)
|
|
{
|
|
if (segs[i].Texture == frontTexture) { frontIndex = i; break; }
|
|
}
|
|
Assert.True(frontIndex >= 0, "the front sprite must be recorded in the sprite bucket");
|
|
|
|
int outlineIndex = -1;
|
|
for (int i = 0; i < segs.Count; i++)
|
|
{
|
|
if (segs[i].Texture == UiTextureTableHandle.None) { outlineIndex = i; break; }
|
|
}
|
|
Assert.True(
|
|
outlineIndex >= 0,
|
|
"the back panel's border must route through the painter-order sprite bucket " +
|
|
"(an untextured segment, UiTextureTableHandle.None), not the separate rect bucket");
|
|
Assert.True(
|
|
outlineIndex < frontIndex,
|
|
"the back panel's border segment must be submitted BEFORE the front sprite's " +
|
|
"segment so it composites underneath it, matching the actual paint order");
|
|
|
|
// UiRenderContext.DrawRectOutline draws exactly 4 sides (top, bottom,
|
|
// left, right), each one quad (2 triangles = 6 vertices) via
|
|
// TextRenderer.AppendQuad; consecutive same-texture (None) DrawSprite
|
|
// calls batch into the ONE segment found above. A partial outline —
|
|
// e.g. a side silently dropped or clipped away — must not pass by
|
|
// merely having SOME untextured geometry in the right bucket at the
|
|
// right order; it must have all four sides' worth of vertices.
|
|
const int expectedQuads = 4;
|
|
const int expectedVertices = expectedQuads * 6;
|
|
Assert.Equal(
|
|
expectedVertices * TextRenderer.FloatsPerVertex,
|
|
segs[outlineIndex].Verts.Count);
|
|
|
|
// No outline geometry may land in TextRenderer's separate untextured
|
|
// rect bucket at all: that bucket always flushes AFTER every sprite
|
|
// segment regardless of submission order, which is exactly the bug —
|
|
// an outline drawn there would win against every window painted after it.
|
|
Assert.Equal(0, renderer.DebugRectVertexCount);
|
|
}
|
|
}
|