# 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).