diff --git a/docs/research/2026-05-06-issue-47-handoff.md b/docs/research/2026-05-06-issue-47-handoff.md new file mode 100644 index 0000000..45da384 --- /dev/null +++ b/docs/research/2026-05-06-issue-47-handoff.md @@ -0,0 +1,287 @@ +# 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: + +```csharp +// 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: + +```powershell +$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 + +```powershell +$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) + +```powershell +# 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).