Revert "Campaign V slice V4a" - it lost world multisampling

This reverts ceec3bc4. Two independent reasons, either sufficient.

The rendering regression. The slice deleted TextRenderGlStateScope, which
saved GL_MULTISAMPLE and GL_SAMPLE_ALPHA_TO_COVERAGE on entry, disabled them
for the text pass, and restored them on exit (TextRenderGlStateScope.cs:111-112
and 153-154 at the parent commit). Its replacement bakes that state into the
text pipeline but nothing restores it, and GlGpuPassEncoder.Dispose does not
either. Every world renderer is still raw GL at this point in the campaign, so
from the first UI frame onward the world drew with multisampling disabled.

The offline pixel gate caught it: 1,791 of 563,200 compared pixels differed,
0.318% against a 0.001 threshold. The commit message attributed this to
wall-clock-driven ambient animation shifting phase, and committed through the
failure. That explanation does not survive its own control: capturing twice at
the reverted-to commit differs by 19 pixels and twice at the slice's own commit
by 8, while base-versus-head differs by 1,791 - a 224x gap that no shared-noise
source explains. An amplified difference image settles it visually: the changed
pixels are the silhouette edges of every tree, building and rock, with terrain
interiors, water and the entire UI untouched. That is the signature of losing
edge antialiasing, not of animated sprites.

This is the exact failure mode two existing memory notes already warn about -
a mid-frame renderer must set every GL state it uses rather than inherit it,
and issue #52's lesson that a rendering migration must audit per-pass GL state
before declaring itself done.

The scope. The brief was three small leaf renderers plus additive frame-
lifecycle wiring, roughly ten files. The commit changed 334 files with 3,665
insertions and 3,845 deletions, including 323 public-to-internal visibility
conversions across the App assembly, 55 test files, two retired conformance
tests, and a self-described temporary escape hatch for bridging raw-GL viewport
textures. Even without the regression, that is not separable into the part
worth keeping and the part worth dropping.

Reverting rather than patching because the good work here - the RHI frame
lifecycle wiring and a genuine render-state-cache staleness fix - is small
enough to redo cleanly against a tightened spec, while untangling it from 300+
files of unrelated churn is not.

Post-revert: Release build clean, App suite back to 3,843 passed / 3 skipped,
offline pixel gate passing at 19 differing pixels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-27 18:27:52 +02:00
parent ceec3bc440
commit 9aaf97e785
334 changed files with 3841 additions and 3661 deletions

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Numerics;
using AcDream.App.UI;
using AcDream.Core.Items;
@ -7,7 +7,7 @@ using AcDream.Core.Selection;
namespace AcDream.App.UI.Layout;
/// <summary>
/// Controller for the action bar's selected-object strip (ids 0x1000019E–0x100001A4).
/// Controller for the action bar's selected-object strip (ids 0x1000019E0x100001A4).
/// Analogue of retail <c>gmToolbarUI::HandleSelectionChanged</c>
/// (<c>docs/research/named-retail/acclient_2013_pseudo_c.txt:198635</c>) +
/// <c>RecvNotice_UpdateObjectHealth</c> (<c>:196213</c>) +
@ -18,29 +18,29 @@ namespace AcDream.App.UI.Layout;
/// guid is provided it sets the name, flashes the selection overlay briefly, and sends
/// either <c>QueryHealth (0x01BF)</c> for health-bearing targets or
/// <c>QueryItemMana (0x0263)</c> for owned non-stack items. The Health meter
/// becomes visible only when the server actually reports health for the selected guid —
/// becomes visible only when the server actually reports health for the selected guid
/// either an <c>UpdateHealth (0x01C0)</c> arrives (retail
/// <c>RecvNotice_UpdateObjectHealth</c> → <c>SetVisible(1)</c>) or the value is already
/// <c>RecvNotice_UpdateObjectHealth</c> <c>SetVisible(1)</c>) or the value is already
/// cached. So a friendly NPC you have not assessed shows name-only (no bar), and a
/// monster's bar appears after damage / a successful assess — matching retail.
/// monster's bar appears after damage / a successful assess matching retail.
/// </para>
///
/// <para>
/// <strong>Retail element roles</strong> (PostInit, <c>:198119</c>): <c>m_pSelObjectField</c>
/// is the container <c>0x1000019E</c> whose <c>SetState(0x1000000b/0c)</c> drives a
/// 0.25s <c>Pause→Normal</c> flash that cascades to the overlay child's green frame.
/// 0.25s <c>PauseNormal</c> flash that cascades to the overlay child's green frame.
/// acdream has no state-cascade / transition-animation system, so this controller drives
/// the overlay element <c>0x100001A0</c> directly and reverts it after the same
/// <see cref="FlashSeconds"/> to reproduce the brief flash. The name element
/// <c>0x1000019F</c> is bumped to the top of the strip's z-order so it draws OVER the
/// overlay frame and the health bar (retail draws the name over the bar — see the
/// overlay frame and the health bar (retail draws the name over the bar see the
/// "Drudge Slinker" reference shot).
/// </para>
///
/// </summary>
internal sealed class SelectedObjectController : IRetainedPanelController
public sealed class SelectedObjectController : IRetainedPanelController
{
// ── Element ids (toolbar LayoutDesc 0x21000016) ─────────────────────────
// ── Element ids (toolbar LayoutDesc 0x21000016) ─────────────────────────
/// <summary>Selected-object container / field element id (retail m_pSelObjectField).</summary>
public const uint ContainerId = 0x1000019E;
/// <summary>Selected-object name element id (retail m_pSelObjectName, UIElement_Text).</summary>
@ -56,24 +56,24 @@ internal sealed class SelectedObjectController : IRetainedPanelController
/// <summary>Horizontal stack quantity slider (retail m_pStackSizeSlider).</summary>
public const uint StackSizeSliderId = 0x100001A4;
/// <summary>Selection-overlay flash duration — retail's container ObjectSelected state is a
/// Pause(0.25s)→Normal transition (toolbar dump, element 0x1000019E).</summary>
/// <summary>Selection-overlay flash duration retail's container ObjectSelected state is a
/// Pause(0.25s)Normal transition (toolbar dump, element 0x1000019E).</summary>
private const double FlashSeconds = 0.25;
/// <summary>Z-order for the name so it draws OVER the overlay frame + health bar.
/// The strip's other children sit at ReadOrder 1–4; this floats the name to the top.</summary>
/// The strip's other children sit at ReadOrder 14; this floats the name to the top.</summary>
private const int NameZOrderOnTop = 1_000_000;
/// <summary>Z-order for the selection-flash overlay — above the health meter (so the green
/// <summary>Z-order for the selection-flash overlay above the health meter (so the green
/// flash isn't hidden by the bar) but below the name (so the name stays readable).</summary>
private const int OverlayZOrder = NameZOrderOnTop - 1;
/// <summary>Height (px) of the black name band at the top of the 31px bar sprite. The name
/// label is constrained to this band (top-aligned) so the health bar shows below it —
/// label is constrained to this band (top-aligned) so the health bar shows below it
/// retail "name on the black, bar below". The bar sprite's colored region starts ~y14.</summary>
private const float NameBandHeight = 15f;
// ── Found elements (any may be null for partial/test layouts) ───────────
// ── Found elements (any may be null for partial/test layouts) ───────────
private readonly UiElement? _name;
private readonly UiDatElement? _overlay;
private readonly UiMeter? _healthMeter;
@ -81,7 +81,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
private readonly UiField? _stackSizeEntry;
private readonly UiScrollbar? _stackSizeSlider;
// ── Captured delegates ───────────────────────────────────────────────────
// ── Captured delegates ───────────────────────────────────────────────────
private readonly Func<uint, bool> _isHealthTarget;
private readonly Func<uint, bool> _isOwnedByPlayer;
private readonly Func<uint, string?> _resolveName;
@ -97,7 +97,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
private readonly Action<Action<uint, float, bool>> _unsubscribeItemManaChanged;
private readonly Action<Action<ClientObject>> _unsubscribeObjectUpdated;
// ── Live state (read by closures on the per-frame draw path) ────────────
// ── Live state (read by closures on the per-frame draw path) ────────────
private uint? _current;
private string? _currentName;
private double _flashRemaining; // > 0 while the selection overlay is flashing
@ -143,7 +143,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
_unsubscribeItemManaChanged = unsubscribeItemManaChanged;
_unsubscribeObjectUpdated = unsubscribeObjectUpdated;
// Find elements — silently skip absent ones (partial/test layouts).
// Find elements silently skip absent ones (partial/test layouts).
_name = layout.FindElement(NameId);
_overlay = layout.FindElement(OverlayId) as UiDatElement;
_healthMeter = layout.FindElement(HealthMeterId) as UiMeter;
@ -152,7 +152,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
_stackSizeSlider = layout.FindElement(StackSizeSliderId) as UiScrollbar;
// The selection-flash overlay must draw OVER the health meter (which spans the whole
// strip) — otherwise the meter hides the green flash whenever a bar is visible (i.e.
// strip) otherwise the meter hides the green flash whenever a bar is visible (i.e.
// for players/monsters). Float it just below the name so the name stays readable.
if (_overlay is not null) _overlay.ZOrder = OverlayZOrder;
@ -195,7 +195,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
//
// The bar sprite (0x0600193E/F, 146x31) carries a ~14px BLACK name band across its
// TOP with the colored bar in the lower portion (confirmed from the dat). Retail
// draws the object name in that black band with the health bar BELOW it — so the
// draws the object name in that black band with the health bar BELOW it so the
// label is TOP-aligned by constraining its height to the band, not centered over the
// whole 31px strip (which overlapped the bar's middle).
if (_name is not null)
@ -240,7 +240,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
/// <param name="layout">Imported toolbar layout (LayoutDesc 0x21000016).</param>
/// <param name="selection">The single Core selected-object owner.</param>
/// <param name="subscribeHealthChanged">Called once with <see cref="OnHealthChanged"/>
/// (typical host: <c>h =&gt; Combat.HealthChanged += h</c>) — drives meter visibility.</param>
/// (typical host: <c>h =&gt; Combat.HealthChanged += h</c>) drives meter visibility.</param>
/// <param name="isHealthTarget">Returns true for guids that may show a health meter
/// (proxy for retail's <c>IsPlayer() || pet_owner || ObjectIsAttackable()</c>).</param>
/// <param name="name">Returns retail's NAME_APPROPRIATE display name for a guid.</param>
@ -296,8 +296,8 @@ internal sealed class SelectedObjectController : IRetainedPanelController
_sendQueryItemMana(0);
}
// ── 1. Clear first (retail: SetText("") + m_pSelObjectField->SetState(0)
// + SetVisible(0) on the meters). ──────────────────────────────────────
// ── 1. Clear first (retail: SetText("") + m_pSelObjectField->SetState(0)
// + SetVisible(0) on the meters). ──────────────────────────────────────
if (selectionChanged)
{
if (_healthMeter is not null) _healthMeter.Visible = false;
@ -319,15 +319,15 @@ internal sealed class SelectedObjectController : IRetainedPanelController
uint g = guid.Value;
// ── 2. Name (displayed via the UiText child's LinesProvider reading _currentName). ──
// ── 2. Name (displayed via the UiText child's LinesProvider reading _currentName). ──
uint stackSize = _stackSize(g);
string? objectName = _resolveName(g);
_currentName = stackSize > 1u && !string.IsNullOrEmpty(objectName)
? $"{stackSize} {objectName}"
: objectName;
// ── 3. Selection overlay: brief flash (retail container ObjectSelected
// = Pause(0.25s)â†Normal). "StackedItemSelected" for stacks. ──────────────
// ── 3. Selection overlay: brief flash (retail container ObjectSelected
// = Pause(0.25s)→Normal). "StackedItemSelected" for stacks. ──────────────
SetOverlayState(stackSize > 1u
? RetailUiStateIds.StackedItemSelected
: RetailUiStateIds.ObjectSelected);
@ -344,9 +344,9 @@ internal sealed class SelectedObjectController : IRetainedPanelController
if (_stackSizeSlider is not null) _stackSizeSlider.Visible = true;
}
// ── 4. Health: query, and show the meter only if real health is already known.
// ── 4. Health: query, and show the meter only if real health is already known.
// Otherwise the meter appears when OnHealthChanged fires for this guid
// (retail RecvNotice_UpdateObjectHealth :196213). ──────────────────────────
// (retail RecvNotice_UpdateObjectHealth :196213). ──────────────────────────
if (stackSize <= 1u && _isHealthTarget(g))
{
if (selectionChanged)
@ -378,7 +378,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
if (_flashRemaining <= 0) return;
_flashRemaining -= deltaSeconds;
if (_flashRemaining <= 0)
SetOverlayState(UiStateInfo.DirectStateId); // flash done → overlay back to blank
SetOverlayState(UiStateInfo.DirectStateId); // flash done overlay back to blank
}
private void SetOverlayState(uint state)