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.
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)
- 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). - Position-pop on equip toggle. Side effect of the appearance fix,
also resolved in
e471527. Doesn't affect shape. - Clothing/armor overlapping the base body (the HiddenParts
hypothesis). User stripped
+Jenaked; bulky shape persists. ParentIndexhierarchy not walked inSetupMesh.Flatten. Setup0x02000001has 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.- Equipment / wielded items. No equipment on
+Jeand bug persists. - Player-specific data flow. Humanoid NPCs using the same setup (Woodsman et al) show the same bug.
- Silent GfxObj load failures or polygon drops.
ACDREAM_DUMP_CLOTHING=1confirmed 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 = 34for0x02000001.flatten.Count = 34.AnimPartChanges = 34..38depending 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 = 0for both humans and drudges → all parts useVector3.Onescale.- Same fragment shader (
mesh.frag) is used for humans and drudges. Per-pixel diffuse with interpolatedvWorldNormal.
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,APCcount ParentIndex[N]: -1,-1,1,2,3,...arrayDefaultScale[N]: ...arrayIdleFrame.Frames[N]:per-partOrigin+Orientation(first 17 parts)EMIT part=NN gfx=0xXXXXXXXX subMeshes=N tris=Nper partTOTAL tris=N meshRefs=Nper 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:
- Step 0 — grep named first.
grep "Class::method" docs/research/named-retail/acclient_2013_pseudo_c.txtfor 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. - 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.csis the closest oracle. - Step 2 — pseudocode. Write the algorithm in plain language under
docs/research/*_pseudocode.mdbefore porting. - Step 3 — port faithfully. Match retail line-by-line, same variable names, same control flow. No "improvements".
- Step 4 — conformance test. Write a test using a captured wire body or known dat values as golden output.
- 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.slnxshould report 0 failures. There are 8 pre-existing motion test failures inAcDream.Core.Teststhat 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) andreferences/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 onAdjustPlanes(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).