acdream/docs/research/2026-05-06-issue-47-handoff.md
Erik 8d7cad5b14 docs(research): #47 handoff prompt for next-session agent
Self-contained pickup brief for the bulky-humanoid bug. Has:
- the bug + acceptance criterion
- everything ruled out this session (with evidence)
- starting facts confirmed via diagnostics
- 4 ranked hypotheses (per-vertex normals → ambient → MSAA → frame
  composition) with concrete tests for each
- diagnostic env vars + their output shapes
- the CLAUDE.md grep-named-first workflow
- files most likely to need edits
- live test workflow (env vars, expected entities in Holtburg)
- constraints (don't break drudges / scenery / +Acdream local view)

Designed to drop straight into a fresh agent's prompt window.
2026-05-06 11:34:25 +02:00

13 KiB

Issue #47 handoff — humanoid Setup 0x02000001 renders bulky vs retail

Use this whole document as the prompt when handing off to a fresh agent. Everything they need to pick up cold is below.


The bug, in one paragraph

acdream renders any character that uses Setup 0x02000001 (Aluvian Male) with a visibly bulkier, less shape-defined silhouette than the same character viewed in retail's AC client. Specifically: shoulders look smoother / rounder where retail has pointier shoulder pads; the back lacks contour ("flat back"); arms appear puffier. The bug applies equally to players (+Acdream, +Je) and humanoid NPCs using the same setup (Woodsman, Sedor Wystan the Blacksmith, Thelnoth Cort the Healer, others). It is independent of equipment+Je stripped naked still shows the bulky shape. It is specific to Setup 0x02000001 — drudges (setup 0x020007DD) and other monster setups render identically to retail through the same pipeline. See ISSUES.md #47 for the full filing.

Acceptance criterion

Side-by-side screenshots of the same humanoid (player or NPC) viewed from the same approximate angle in acdream and retail show matching silhouette and shape definition — pointy shoulders where retail has them, contoured back, no "puffy" arms. User confirms visually. NPCs, drudges, and scenery must continue to render correctly (no regressions).

What's already been ruled out (don't redo these)

  1. 0xF625 ObjDescEvent appearance updates being dropped. Was a real bug for skin/hair colors. Fixed in commit e471527. Does NOT affect the bulky-shape issue (persists with the fix in place AND with no equipment).
  2. Position-pop on equip toggle. Side effect of the appearance fix, also resolved in e471527. Doesn't affect shape.
  3. Clothing/armor overlapping the base body (the HiddenParts hypothesis). User stripped +Je naked; bulky shape persists.
  4. ParentIndex hierarchy not walked in SetupMesh.Flatten. Setup 0x02000001 has a real hierarchy (-1, -1, 1, 2, 3, -1, 5, 6, 7, 0, 9, 10, 11, 12, 13, 14, 15, 0, ...) but a parent-walk implementation produced no visible change. Confirms AC's idle animation frames are already in setup-root coordinates, not parent-local.
  5. Equipment / wielded items. No equipment on +Je and bug persists.
  6. Player-specific data flow. Humanoid NPCs using the same setup (Woodsman et al) show the same bug.
  7. Silent GfxObj load failures or polygon drops. ACDREAM_DUMP_CLOTHING=1 confirmed every one of the 34 parts emits triangles (EMIT part=NN gfx=0xXX subMeshes=N tris=N); total ~648-700 tris per character.

What's confirmed (use this as starting facts)

  • Setup.Parts.Count = 34 for 0x02000001. flatten.Count = 34. AnimPartChanges = 34..38 depending on equipment. All match.
  • Idle animation frames place parts at sensible humanoid Z-heights (head Z=1.587, mid-body Z=0.5-1.0, ground Z=0.085).
  • All 34 per-part orientations are nearly identical: 180° around -Z axis (W≈0, Z≈-1). This is a setup-wide coordinate-flip convention. Drudges have varied per-part orientations — different layout.
  • setup.DefaultScale.Count = 0 for both humans and drudges → all parts use Vector3.One scale.
  • Same fragment shader (mesh.frag) is used for humans and drudges. Per-pixel diffuse with interpolated vWorldNormal.

Top hypotheses, ordered by likely payoff

Hypothesis A — per-face vs smoothed vertex normals

Strongest candidate. AC's dat stores ONE normal per SWVertex. If human-character GfxObjs (e.g. 0x01001212, 0x0100004B-0x01000059) were authored with per-face flat normals (each vertex's normal copied from its triangle's face normal) while monster GfxObjs were authored with smoothed normals (averaged across adjacent faces), acdream's Vector3.Normalize(sw.Normal) would produce flat shading on humans and smooth shading on monsters. The screenshots strongly support this — retail characters look smooth-shaded, acdream characters look facet-edged.

User said "not shaders" but they may not realize per-vertex normal style is part of the shader pipeline.

Test: in src/AcDream.Core/Meshing/GfxObjMesh.cs:142, replace the direct sw.Normal read with a smooth normal computed per-load via face-adjacency accumulation:

// Pre-pass: for each polygon, compute face normal; accumulate onto each vertex.
// Post-pass: normalize.

Verify retail does this — see docs/research/deepdives/r13-dynamic-lighting.md:107 for the v.normal += t.face_normal pattern, and docs/plans/2026-04-13-rendering-rebuild.md:50 for the AdjustPlanes: face-normal accumulation + per-vertex lighting note (terrain context but same math applies to characters).

If smooth-normals fixes humans and ALSO doesn't break drudges (because drudge dat normals were already smooth, computing them again gives the same answer modulo precision), this is the bug.

Hypothesis B — cell ambient too low

Back-facing surfaces (the parts of the character not lit by the directional sun) fall to uCellAmbient in mesh.frag. If ambient is very dark, the back of any character looks uniformly black — reading as "flat" because no detail variation is visible. Retail likely has higher ambient that lets unlit surfaces still show their geometry through subtle gradients.

Test: dump uCellAmbient UBO values during a player render and compare to retail's behaviour. Try bumping ambient temporarily and see if back- detail emerges.

Hypothesis C — anti-aliasing

acdream's GL window may not have MSAA enabled. Without it, polygon edges visibly stair-step, exaggerating the faceted look at low triangle counts (~700 tris per character). Retail likely has AA on by default.

Test: check the Silk.NET window creation code for Samples/MSAA config. Try enabling Samples=4 and re-render.

Hypothesis D — orientation composition order or sign

The 180°-around-Z rotation on every part is unusual. If acdream applies it correctly but retail applies it differently (e.g. as a post-multiply or with the inverse), parts could be subtly mis-positioned in ways that read as "bulky" rather than "broken". My investigation didn't fully rule this out — parent-walk was a no-op, but a single-level orientation composition discrepancy might be invisible without comparing actual post-transform vertex positions to retail.

Test: attach cdb to retail (see CLAUDE.md "Retail debugger toolchain"), break in Frame::combine (0x518FD0) with a player guid, dump the resulting Frame for parts 0, 9, 16. Compare to acdream's per-part world matrices (add a diagnostic).

Diagnostic infrastructure already built

All env-var-gated, no runtime cost when off:

$env:ACDREAM_DUMP_CLOTHING = "1"

Prints, for every humanoid spawn (gate: setup.Parts.Count >= 10):

  • Header: setup.Parts.Count, flatten.Count, APC count
  • ParentIndex[N]: -1,-1,1,2,3,... array
  • DefaultScale[N]: ... array
  • IdleFrame.Frames[N]: per-part Origin + Orientation (first 17 parts)
  • EMIT part=NN gfx=0xXXXXXXXX subMeshes=N tris=N per part
  • TOTAL tris=N meshRefs=N per entity
$env:ACDREAM_DUMP_APPEARANCE = "1"

Prints structured decode of every 0xF625 ObjDescEvent (mid-session appearance change) — SubPalettes, TextureChanges, AnimPartChanges.

Source: src/AcDream.App/Rendering/GameWindow.cs around dumpClothing / OnLiveEntitySpawnedLocked, and src/AcDream.Core.Net/WorldSession.cs DumpAppearanceEnabled.

Workflow (per CLAUDE.md)

This is AC-specific behavior. Follow the mandatory workflow:

  1. Step 0 — grep named first. grep "Class::method" docs/research/named-retail/acclient_2013_pseudo_c.txt for any function name from a hypothesis. The Sept 2013 PDB is named — most things have real names. Don't decompile fresh until you've grep'd.
  2. Step 1 — cross-reference. Check ACE / ACME / ACViewer / Chorizite / AC2D / holtburger as appropriate. The reference hierarchy in CLAUDE.md spells out which ref is authoritative for each domain. For rendering: ACME WorldBuilder.Tests/ClientReference.cs is the closest oracle.
  3. Step 2 — pseudocode. Write the algorithm in plain language under docs/research/*_pseudocode.md before porting.
  4. Step 3 — port faithfully. Match retail line-by-line, same variable names, same control flow. No "improvements".
  5. Step 4 — conformance test. Write a test using a captured wire body or known dat values as golden output.
  6. Step 5 — visual verification. User confirms in-client.

If grep-named-first turns up nothing relevant, the function is likely in the unnamed minority. Fall back to Ghidra chunks under docs/research/decompiled/ keyed by address.

Files most likely to need edits

  • src/AcDream.Core/Meshing/GfxObjMesh.cs — polygon emission, vertex normal handling at line 142. Hypothesis A lives here.
  • src/AcDream.Core/Meshing/SetupMesh.cs — per-part transform composition. The parent-walk experiment lives here (and was reverted — see git history if you want to revisit it).
  • src/AcDream.App/Rendering/Shaders/mesh.frag — per-pixel lighting equation. Hypothesis B lives here if ambient is the cause.
  • src/AcDream.App/Rendering/Shaders/mesh.vert — normal transform (mat3(uModel) * aNormal). Watch for non-uniform scale issues if any future change touches scale.
  • The Silk.NET window setup code (search for IWindow.Create / WindowOptions) — Hypothesis C lives here if MSAA needs enabling.

Test workflow (live verification)

# Always: kill stale + 3-5s wait (ACE session lingers briefly).
Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 4

$env:ACDREAM_DUMP_CLOTHING = "1"  # verbose per-spawn diagnostics
$env:ACDREAM_DAT_DIR   = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE      = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"
$env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"
$env:ACDREAM_TEST_PASS = "testpassword"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
    Tee-Object -FilePath "launch_issue47.log"

The user's +Acdream character logs in at Holtburg. Nearby:

  • Other player(s) like +Je (driven from a parallel retail acclient)
  • Humanoid NPCs (Woodsman, Sedor Wystan the Blacksmith, Thelnoth Cort the Healer, Sontella Dagroff the Bowyer, Archmage Cindrue, Monyra the Jeweler, Ecutha the Tailor) — most use Setup 0x02000001
  • Drudges and slinkers nearby (different setup, render correctly — use as control)
  • The Foundry's "Nullified Statue of a Drudge" — different setup, used for prior diagnostic work

User has the retail acclient open in parallel for side-by-side comparison. Visual verification is the acceptance test — there's no automated way to say "this looks like retail."

Constraints / don't-break

  • Do not break drudge / monster rendering. They're correct now.
  • Do not break scenery (terrain, buildings, statues). They're correct.
  • Do not break the local +Acdream (own-character) view either — same pipeline so any rendering change applies to it.
  • Tests must stay green: dotnet test AcDream.slnx should report 0 failures. There are 8 pre-existing motion test failures in AcDream.Core.Tests that are NOT yours — don't try to fix them in this work.
  • Build must stay green: dotnet build AcDream.slnx -c Debug.

When to stop and ask

Per CLAUDE.md, ask only for:

  • Visual verification (user looking at the client)
  • Genuine architectural disagreements with the roadmap
  • Hard-to-reverse destructive actions

Otherwise act. Don't ask "should I continue?".

References to consult

  • references/ACME/WorldBuilder/Editors/Landscape/StaticObjectManager.cs — ACME's mesh hydration; same stack (Silk.NET) as us, used as our closest "should look right" oracle.
  • references/ACViewer/ACViewer/Physics/PartArray.cs:614 (UpdateParts) and references/ACViewer/ACViewer/Physics/Animation/AFrame.cs:43 (Combine) — frame composition math.
  • references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Networking.cs:48 (SerializeUpdateModelData) and :978 (AddBaseModelData) — what ACE puts on the wire for character appearance.
  • docs/research/named-retail/acclient_2013_pseudo_c.txt — Sept 2013 PDB with 18,366 named functions. Grep this FIRST.
  • docs/plans/2026-04-13-rendering-rebuild.md — earlier rendering rebuild plan with notes on AdjustPlanes (terrain, but the face-normal accumulation pattern is the smooth-normal pattern).
  • docs/research/deepdives/r13-dynamic-lighting.md — has the smooth- normal accumulation pseudocode at line 107.

Final note

This is a rendering-fidelity issue, not a wire-protocol one. The network data is correct (the previous session's 0xF625 work confirmed it). The bug is in how acdream interprets that data into pixels.

The smoothest path is probably Hypothesis A (smooth normals), one contained change in GfxObjMesh.Build, gated behind a feature flag for A/B testing, then verified live by the user. If that doesn't fix it, move to B (ambient), then C (MSAA), then D (frame composition with cdb trace).