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