Skip to content

pipeline_max_coverage

Jan Boon edited this page Jul 10, 2026 · 34 revisions

title: pipeline_max coverage vs the build_gamedata Max export surface description: Gap analysis of the headless .max exporters against every Max-side export build_gamedata needs, with sized work packages for follow-up sessions published: true date: 2026-07-10T00:00:00.000Z tags: editor: markdown dateCreated: 2026-07-08T00:00:00.000Z

Written 2026-07-08 (authoring session), last updated 2026-07-10 through §10z-clod (.clod / NelExportLodCharacter headless — last surface format; 105/105 produced, 84 FLOATEQ), §10z-quinze (interface weld, mapext verification, sky-dome hemisphere, Edit-Mesh clone decode, decompAffine gold revert, Physique rigid one-link, Morpher bsList), §10z-seize (Map Extender drop-in plugin, 118/118 live Max 2010) and §10m-quater (linked-COM attach). Prior rounds: §10z-douze/treize/quatorze, §10s-*, §10k-bis, §10z-onze. Pick-up map for the next few implementation sessions. Read pipeline_max_design FIRST — especially §12 (rules for the implementing session) — and the per-format sections referenced below. The goal state: build_gamedata's 1_export.py steps can run the headless pipeline_max_* tools instead of 3ds Max, for every asset type, with corpus-proven fidelity.

How the surface was enumerated

nel/tools/build_gamedata/processes/*/1_export.py + their maxscript/*.ms scripts. Every Max-side export goes through one of these NelExport* MAXScript entry points (implemented in nel/tools/3d/plugin_max/nel_export/nel_export_script.cppnel_mesh_lib/): NelExportShapeEx, NelExportAnimation (used by BOTH the anim and shape processes), NelExportSkeleton, NelExportSkeletonWeight, NelExportInstanceGroup (ig + ligo), NelExportCollision (rbank + ligo), NelExportPACSPrimitives, NelExportVegetable, NelExportLodCharacter, plus ExportRykolZone (zone) and LigoExportZone (ligo). The ps process is a plain file copy (no Max involvement) despite its log line. Everything else under processes/ (rbank build, ig_light, zone_light, banks, maps, sheets, …) is post-export C++ tooling that already runs headless.

Reference outputs live in ~/pipeline_export (continents/ecosystems/common trees, mirroring the workspace config under ~/ryzomcore_leveldesign/workspace); sources in ~/ryzomcore_graphics.

Coverage table

Entry point Process(es) Output Refs in ~/pipeline_export Headless tool Status
ExportRykolZone zone .zone 6306 (incl. ligo) pipeline_max_export_zone GREEN — corpus 1068 exact + 130 x87-tier + 3 budget (§10h)
LigoExportZone ligo .zone/.ligozone bricks (in above) pipeline_max_export_zone --ligo GREEN — .ligozone 100% byte-identical; .zone same tiers as ExportRykolZone above (§10h)
NelExportSkeleton skel .skel 203 pipeline_max_export_skel GREEN — legacy 99.4% drot, era classes documented (§10, §10e)
NelExportSkeletonWeight swt .swt 1 (+ corpus) pipeline_max_export_swt GREEN (§10f)
NelExportInstanceGroup (standalone ig) ig .ig 54 direct (→ 18497 final, downstream via land_exporter, out of scope) pipeline_max_export_ig GREEN — T1/T2 116/116; 54/54 direct-ref field-exact under uninit-mask (--max-direct-diff budget retired to 0, gate strict); processed-tier 40/40 match. 0 truly byte-identical remains a legitimate tolerance class (reference exporter's SunContribution/Light[]/STL-light-tie-order uninit heap). §10w closed the last open diff via the Edit Mesh 0x0130 created-verts decode
NelExportInstanceGroup (ligo bricks) ligo .ig 537 direct pipeline_max_export_ig --ligo MOSTLY (near green) — T1/T2 1201/1201; 529/537 (98.5%) match under uninit-mask (71 byte-identical + 458 uninit-only under x64/SSE; under VS2008/x87 shifts to 130 byte-identical + 399 uninit-only — same 8 open, x87 codegen zeros the uninit heap x64 leaves random). 8 open, budgeted DIFF_BUDGET=8 (§10w, was 89 → 28 → 8 in one session): ~5–6 PS-instance clusterize AABBox borderline containment (extra cluster links on rotated FX bboxes, LOW real-content risk — extra links harden culling rather than drop content) + 2–3 unclassified singleton files (taverne palme_type_C3 Scale ≈3e-5, jungle-2 ju_water_jungle_coude Pos ≈15cm, brandon fortress_int_03/04 Clusters + one Scale)
NelExportAnimation anim, shape .anim 9146 pipeline_max_export_anim (+ shape --anim-out) MOSTLY — whole corpus DIRECT-gated since the §10m-ter audit (2026-07-09): all 4524 anim sources have a fresh direct reference (fauna/sky anim_export tiers wired — fauna was informational-optimizer-only before); non-biped 25 byte-identical + 46 floateq / 71 (ULP scale-value class, codegen-independent); biped 7 byte-identical + 4445 structural-exact, 0 structural fails; the audit closed Bezier Scale outTan [14..16], nested-COM re-entry (mektoub rider subtree) and the linked-COM rigid figure attach (kitin Bip02 5.5 m class → 2e-4); shape-process anims 110/110 byte-identical with not-produced accounting; in-between IK bounded (chars median ~0.068, §10r/§10s; arm-pin default ON §10s-quat/cinq/sept; pole [22..24] §10s-six); §10m-quater decoded the linked-COM attach EXACTLY (L = 0x0112 × the corrected current-position frame — position AND rotation float-exact; the stun-trilogy "era class" was our decode error, withdrawn; mektoub pair 1.449 → ~1.32, its residual is the mount's 4-link HorseLink/Toe IK tier); remaining: FK-floor plants, in-plant foot rot, blend ramps
NelExportShapeEx shape .shape 11251 pipeline_max_export_shape MOSTLY (coverage 99.97% — 3589 exported, 1 not produced) — the §10z-douze/treize/quinze rounds closed every skip class except skinned-maxverts=1 (the gauntlet authoring case, defective_max_files §1.7): spline caps + UVW planar + PB2 prims (§10z-douze), skinned WORLD-space VBs + Physique link-parent rule → weight-exact skinning (§10z-treize), interface border weld + Morpher bsList + Physique rigid one-link (§10z-quinze). Corpus (2026-07-10 session-final, x64): 141 byte-identical + 1330 float-eq + 736 differ + 113 mapext-verified + 3 mapext-diff + 983 lightmap-verified + 282 lightmap-diff + 1 no-ref; VS2008/x87 same binaries: 158 + 1354 + 695 (reference codegen closes a tier of borderline files, §10z-treize pattern). Map Extender fully closed: cache decode §10z-quatorze, hemisphere fix → 118/118 applies, --compare-mapext-mask verification tier, per-node validation matrix in defective_max_files §4.4 (2 byte-identical + 8 FLOATEQ-full + 105 verified + 3 diff), and a drop-in plugin (§10z-seize) validated 118/118 in live Max 2010. Remaining open: interface-weld normals residual (~75 files, 0.002–0.2 border corners), ~550-file CMeshMRMSkinned MRM-structural class (float-noise input divergence, informational tier), CMeshMultiLod slave-material merge, UVW non-planar / Unwrap UVW / FFD tail
NelExportCollision (standalone/rbank direct) rbank .cmb 16 direct (+ 429 placed-instance copies land_exporter — a separate, already-headless C++ tool — writes into RBankCmbExportDirectory from the ligo-brick Max exports below, same mechanism as its .ig placement copy; porting that copy step is out of scope for pipeline_max, but it is real and load-bearing: it's what rbank's "Build rbank indoor" step reads to build the shipped .rbank/.gr/.lr) pipeline_max_export_cmb MOSTLY (near green) — direct tier (gated): 16/16 files export, 12/16 byte-identical + 3/16 x87-tier + 1/16 differ (DIRECT_DIFF_BUDGET=1, was 3): §10z-quinze's Edit-Mesh created-vert CLONE decode (0x0130 srcTag != -1 = clone of input vertex + offset from its PRE-move position) made FY_hall_reunion and Zo_bt_hall_Reunion_vitrine byte-identical; the single remaining differ is Zo_bt_Hall_Conseil's rotation-about-Y class (open on its own terms since §10v, precision ruled out).
NelExportCollision (ligo bricks) ligo .cmb 221 measured igs (subset of the 537 ligo bricks that contain collision geometry) pipeline_max_export_cmb --ligo MOSTLY — genuine Max export (nel_ligo_export.ms calls NelExportCollision per brick exactly like NelExportInstanceGroup, landing brick-level .cmb in LigoEcosystemCmbExportDirectory which is land_exporter's RefCMBDir input for the placement copy in the row above) and is now GATED in the corpus test (LIGO_DIFF_BUDGET=95 since §10z-quinze, was 105 at §10z-ter's gating; --gate-t3 fails on regression past that count — same shape as ligo_ig_corpus.py's DIFF_BUDGET). §10z-ter closed the majority of §10x's "base object is not a GeomObject" gap via EditablePoly + parametric-primitive extraction paths on cmb (previously shape-only): ~230 skipped node warnings dropped to 47. Ligo T3 (2026-07-10): 34 byte-identical + 95 float-eq + 92 differ + 0 export failures across 221 measured igs (LIGO_DIFF_BUDGET tightened 105 → 95; the clone decode moved several bricks) — up from 11 + 15 + 22 + 6 = 54 (§10v baseline).
NelExportPACSPrimitives pacs_prim .pacs_prim 493 pipeline_max_export_pacs_prim GREEN — 493/493 byte-identical (§10v)
NelExportVegetable veget .veget 126 pipeline_max_export_veget MOSTLY — T1/T2 126/126 clean; T3 (corrected §10z-douze — the earlier "505 exported" record was a -j shared-outdir race artifact, fixed with per-file outdirs + a pipeline_max_veget_corpus ctest gate): 126 exported: 25 byte-identical + 97 size-eq (float-noise byte-diff) + 4 size-diff (Ju_DebrisFeuillesA class, unclassified), 0 export failures, 0 references not produced. Shared code with the shape exporter. §10z-onze/§10z-douze.
NelExportLodCharacter clodbank .clod 105 pipeline_max_export_clod MOSTLY105/105 refs produced, 0 export failures; T1/T2 106/106; T3 84 float-eq + 21 residual (ConvertToRigid→Bip01 bone-set class ~14, MRM VB-dedup splits ~3, normals ~4 verts across 4 files, 1 UV phytopsy). Shared PHYSIQUESKIN + CMeshMRM NLods=1/Divisor=1 → CLodCharacterShapeBuild. Deep OSM peel fix in scene_lib closed cococlaw. Gate: pipeline_max_clod_corpus (§10z-clod)

Cross-cutting infrastructure that is DONE and reusable: the storage/scene layer with full T1/T2 byte-identity over every corpus (max files parse→build byte-identical), the OLE backend (§2a), the VS2008/Wine x87 reference build (§2b), Edit Mesh / Editable Poly / Edit Patch geometry decode (ig/zone/shape sessions), the shared pipeline_max_export_common library — EDITMESH (Edit Mesh decode+apply, §10x, now driving ig+cmb+shape), XREFRESOLVE (§10x, ig+cmb), PRIMMESH (Box/Cylinder/Sphere/Plane topology + Box/Plane UVs, §10y/§10z-sept), PHYSIQUESKIN (Physique vertex weights → CMeshBuild skinning, §10z-sept — reused by .clod §10z-clod), SPLINESHAPE (BezierShape/Spline3D compact knot decode, §10z-dix — remanence shipping; tessellation pending for cmb XRef / mesh-eval), TRACKBUILD (shared Max keyframer → NL3D ITrack builders incl. typeColor, §10k-bis), MAPEXT (Map Extender UV cache, §10z-quatorze), SPLINEMESH (spline tessellation + the corpus-reversed morph-cap rule, §10z-douze — the spline-cap shapes), UVWMAP (planar projection default-ON §10z-douze; non-planar gated), IFACEBUILD (interface-mesh border weld, §10z-quinze — shape-side), max_scene/max_math/OLDPBLOCK/db_path/appdata_util — materials + ParamBlock2 (§10j), typed Bezier Point3 (0x200A) / Bezier Color (0x2011) / TCB Point3 (0x442314) controllers, the biped system incl. typed keytrack editing (§10t/§10u), AppData flag reading, and the corpus-test harness patterns (*_corpus.py, fork-per-file, tolerance tiers, --compare-lightmap-mask / --compare-mapext-mask). Plus, outside the headless tools proper: the Map Extender drop-in Max plugin (plugin_max/map_extendermapext198m3.dlm, §10z-seize — in the quick_start redist set for Max 9..2023, validated 118/118 in live Max 2010) and the resource-compiled VC90 SxS manifest mechanism (NL_EMBED_SXS_MANIFEST_RC, nel.cmake — mt.exe page-faults under Wine; without it, Wine-built plugins fail on real Windows with LoadLibrary error 126). Standing warning (§10z-quinze): max_math's decompAffine adaptation is corpus-arbitrated — a "gold alignment" against an offline Max 2010 probe was landed and REVERTED after it broke the ig gates structurally (the probe validated reassembly, which is convention-invariant); never change pipeline_max_export_common math without re-gating ig/ligo-ig/shape/skel/zone together.

The gaps, sized

A. The remaining small formats (breadth completion)

.pacs_prim and .cmb landed 2026-07-08 (§10v) — .veget and .clod close the rest of the SURFACE.

  1. .cmb — NelExportCollision DONE, mostly (§10v landed, §10x closed most follow-ups, §10z-ter closed the face-vertex remap + ligo extraction gaps). pipeline_max_export_cmb. §10z-ter landings: (a) the face-index remap chunk the §10x handoff flagged was 0x0210 itself — misclassified in §L.2 as "created map-1 texture-vertex faces", corpus-validated across 445 files / 113 chunks / 2881 entries as the modern equivalent of legacy TOPO_FACEMAP_CHUNK 0x2780 (per-face per-corner index rewrites, (uint32 faceIdx, uint32 applyMask, uint32 v[3])), applied in the shared EDITMESH::applyEdits between moves and face-deletes; FY_hall_reunion face-index match 247/248 → 248/248, Zo_bt_hall_Reunion_vitrine face diffs 221 → 0 (25 vertex offsets remain); (b) EditablePoly + parametric-primitive (Box/Cylinder/Sphere/Plane) extraction paths added to cmb (shape had them, cmb didn't) — ligo brick warnings ~230 → 47, and the 16 ligo bricks that produced zero output all now succeed (0 export failures). (c) Ligo tier is now GATED (LIGO_DIFF_BUDGET=105) instead of informational — nel_ligo_export.ms calls NelExportCollision per brick exactly like NelExportInstanceGroup, so its fidelity matters exactly as much as the standalone tier's. FY_hall_reunion 16 created-vert positions + Zo_bt_hall_Reunion_vitrine 25 vert offsets CLOSED §10z-quinze — both byte-identical: the 0x0130 created-vert record with srcTag != -1 is a CLONE of input vertex srcTag with the Point3 as an OFFSET from the source's PRE-move position (the modern merge of legacy TOPO_CVERTS_CHUNK + the clone's move; "srcTag ignored" in the earlier decode was wrong). Remaining open: Zo_bt_Hall_Conseil vertex offset (still open on its own separate terms since §10v — rotation-about-Y suspected, precision ruled out); 35 XRef sources resolving to Max SplineShape (superclass 0x40 — SPLINEMESH tessellation now exists for the shape-cap path, wiring it into cmb is the follow-up); 12 Boolean compound objects on Matis buildings (class (0x51db4f2f, 0x1c596b1a), union/intersection/subtraction of two operands — no quick path without SDK).
  2. .pacs_prim — NelExportPACSPrimitives DONE (§10v) — pipeline_max_export_pacs_prim, 493/493 byte-identical.
  3. .veget — NelExportVegetable DONE, mostly (§10z-onze, counts corrected + gated §10z-douze) — pipeline_max_export_veget, 126 exported: 25 byte-identical + 97 size-eq + 4 size-diff (Ju_DebrisFeuillesA class open).
  4. .clod — NelExportLodCharacter DONE, mostly (§10z-clod) — pipeline_max_export_clod, 105/105 refs produced, 84 FLOATEQ + 21 residual. Reuses PHYSIQUESKIN + shape mesh path. Residual classes documented in design §10z-clod (ConvertToRigid root-fallback Bip01, MRM dedup, 1 UV, few normals).

B. Shape depth (the flagship remaining work — Opus-sized)

NelExportShapeEx beyond plain CMesh, in rough dependency order (post-§10z state):

  1. Skinned + MRM meshes (CMeshMRM / CMeshMRMSkinned) COVERAGE DONE §10z-sept (653-file skinned-physique skip class closed; quality still DIFF). Shared PHYSIQUESKIN in pipeline_max_export_common: bone table = modifier refs, boneRef=~index, top-4+normalize into SkinWeights, skeleton walk for BonesNames, root-weight fallback for ConvertToRigid residues. Remaining quality work: full ConvertToRigid replay (differential dataset), MRM era tolerance tiers, interface border-weld. Skin modifier still unimplemented (0 corpus hits). §10z-quat/six history (detection, Part M characterization, bone table location) remains the decode foundation — see design §10z-quat/six/sept. Part M open items reduced to cross-link boneRefs + ConvertToRigid fidelity: (i) boneRef residual positive values (~4 %) — deformable cross-links, needs a differential dataset authoring TWO variants of a minimal skinned mesh with one vertex's bone reassigned to pin), (ii) weight-to-.shape normalization (Physique's stored rigidity blend vs the .shape's top-4 normalized weights — likely needs the reference's own SDK rigidity→final-weight math replayed on the stored bytes; if not derivable from stored payload alone, byte-identity is closed and placeholder root-bone weights become the pragmatic fallback for coverage), (iii) Skin modifier fully uncharacterized (same PMB_SKIN_DUMP diagnostic applies when a Skin-carrying corpus file appears — none in the Max 3-era corpus, but Max 4+ authored assets would), (iv) bone-name array source (per-modifier bone list location on the Physique scene class not yet pinned; may live on getReference() slots or in a container chunk on the modifier itself, separate from the mod-app). The Session-3 plan below is unchanged; §10z-quat gives it a written baseline instead of a cold start. Update 2026-07-09 (§10z-six): two of Part M's four blockers closed WITHOUT a differential dataset — the per-modifier bone table is the modifier's OWN reference array (87 INode refs on fy_hof_armor01_pantabottes, named, 16 NULL slots counted), and the primary boneRef is ~index (one's-complement) into it, validated end-to-end (boneRef −77→idx 76→Bip01 L Thigh etc., anatomically exact). The bone-name array source is therefore solved too. Remaining: ~4 % deformable cross-link boneRefs (small positive, not ~index) and the Bip01 root-link reassignment from ConvertToRigid(TRUE) — both still need the differential dataset. PMB_SKIN_DUMP now also dumps the modifier's references; --dump-skin <shape> validates a decode against a built reference shape. Update 2026-07-10 (§10z-douze/treize/quinze) — the Physique decode is COMPLETE, all without the differential dataset: positive cross-link boneRefs are 1-BASED link indices (neg k=~stored, pos k=stored−1; every 0x0989 record is uniformly 4+20·numBones bytes — the "different record structure" hypothesis was wrong); the deforming bone is the INode PARENT of ref[k+1] (equals ref[k] mid-chain, differs at chain boundaries); skinned meshes export WORLD-space VBs (GetObjectTM — why no skinned vertex ever position-matched before); a single-link record is RIGID (weight 1.0 regardless of the stored float, which is Physique's rigidity factor — the c03/monster family stores 0.0 there and 620+ verts/mesh silently fell to root). Result: position-matched vertices are weight-exact against the references and whole CMeshMRMSkinned files compare byte-identical (~122+ corpus-wide). Remaining quality is the ~550-file MRM-STRUCTURAL class (the MRM builder takes different simplification decisions on float-noise-divergent inputs — the byte-identical files prove exactness on bit-equal inputs; informational tier, same shape as the anim optimizer tier) plus the interface-weld normals residual below.
  2. CMeshMultiLod / coarse meshes DONE §10y — LOD_NAME_COUNT-driven build with per-slot CMeshGeom or CMeshMRMGeom, per-slot DistMax/BlendLength/BlendIn/BlendOut/CoarseMesh flags from slave appdata; 418 files added to the exported set (+63 float-eq, +274 lightmap-verified, +79 differ). One residual class: multi-lod slaves whose materials aren't all in the parent's list produce ref-larger material counts (e.g. common/construction/gen_mo_charognemammal 'materials: 3 vs 4') — likely the reference plugin merges slave materials into the base list; not yet root-caused. Coarse-mesh routing (with-coarse-mesh output dir) was already handled by M1's NEL3D_APPDATA_LOD_COARSE_MESH scan.
  3. Water shapes (CWaterShape) (221 files) DONE §10z, promoted to FLOATEQ §10z-bisWATERBUILD::buildWaterShape replicates CExportNel::buildWaterShape: mesh eval + node-local transform + Z=0 projection + convex hull → CPolygon2D::setShape, PB2 reads for pool id / wave height / slot enables + texmaps, textures via a lean BitmapTex + NeL-Multi-Bitmap loader; default transformation from decompMatrix(getLocalMatrix). Same-session byte-identity rounds 1+2 landed on top of the coverage path: (r1) CTextureBlend day/night envmap pairing when slot 2's TEXMAP is present + CWaterShape stream v7→v4 downgrade (patch version byte + truncate trailing 14 bytes) → file size matches reference exactly (698 vs 698 on tr_water_00), byte diffs 111 → 35; (r2) correct v14-water-material NLB_MAIN param IDs pinned empirically via PMB_WATER_DUMPfBumpU/VScale/Speed=0x09..0x0c, fDisplaceU/VScale/Speed=0x0d..0x10, fWaterHeightFactor=0x11, iWaterPoolID=0x12 — plus tail bool defaults (splash=true from ctor, UsesSceneWaterEnvMap[0/1]=false explicit) → byte diffs 35 → 3. 220 of 221 water files close (the 1 residual is a genuine missing-required-map case matching reference behavior). §10z-bis added a compareWaterShape field-walk in compareShapesFields (dispatched off dynamic_cast alongside the CMeshBase branch; walks the default transform double-cover-aware + water-specific fields + textures via a new compareTexture helper handling CTextureFile/CTextureMultiFile/CTextureBlend recursively) that promotes the ~219 water-differ files from DIFF to FLOATEQ. Open items on the tail: the 3-byte DefaultRotQuat MSB residual (ref has sign bit set on x/y/z; ours has 0.0 — likely the same double-cover boundary class as CMesh's rot-quat sign flip §10i already documents); the water script's bEnableWaterSplash + bWaterUseSceneEnvMap{Above,Under} name-to-id mapping still unmapped in-code (initial guesses at 0x19..0x1b turned out to be the standard material's iBlendSrcFunc/iBlendDestFunc); setColorMapMat (2x3 affine, only fires when diffuse slot 7 is enabled — most corpus files don't use it).
  4. CFlareShape (13 files) DONE §10z-bisFLAREBUILD::buildFlareShape replicates CExportNel::buildFlare: nel_flare scripted-plugin PB2 (ClassId (0x4e913532, 0x3c2f2307), extends Sphere), 57 params declared without explicit id: tags → IDs equal MAXScript declaration order (same convention as pacs_prim / max_geometry_formats §K.1), corpus-verified via python PB2 dump against zo_cn_mairie.max. Color conversion follows the reference verbatim ((uint)(255.f*col.x), plain truncation — NOT the material_build path's rounded +0.5 convertColor). DefaultPos from decompMatrix(getLocalMatrix) translation row; rotation/scale not exposed on CFlareShape. Also caught in the same landing: main.cpp was calling shape->setDistMax(LOD_DIST_MAX appdata) UNIFORMLY on every returned shape, but the reference gates it inside the mesh branch AFTER the special-shape early returns (buildFlare/buildWaterShape/buildRemanence/buildWaveMakerShape/buildParticleSystem all return before line 466 of export_mesh.cpp) — flares/water/remanence keep the ctor default. Moved into the mesh/multi-lod branch of buildShapeForNode. Byte-identical on flare_preoder_item, zo_flare, zo_bt_mon_flare_couloir, gen_autel_flare04. Full sweep: 12/13 byte-identical; the 13th (gen_autel_flare04) is a reference-era class — byte-identical against the newer ecosystems/desert reference, differs by 1 byte from the older common/construction reference; not a decode bug.
  5. CSegRemanence (82) DONE §10z-dix — shared SPLINESHAPE + REMANENCEBUILD; 82/82 produced (trail subset 78/78 FLOATEQ-or-better). Interface border-weld LANDED §10z-quinze (IFACEBUILD::applyInterfaceToMeshBuild — MRM bookkeeping + packed-vertex align + both normal-correction variants, scene-normals dominant): byte-identical 122 → 139 at its landing; a ~75-file normals residual remains (~0.002–0.2 on border corners; suspected biped-box candidate-set gap in the scene-normals sweep).
  6. Waterfall texture-matrix material anims DONE §10k — landed pre-shape-session; 642 byte-identical .anim outputs in the M2 shape sweep.
  7. Lightmap-diff tail (282 files) — the calc-lm-mask compare says these have structural divergence beyond just the lightmap-added fields. Candidates: multi-lod material merging (above), missing UV generation on parametric prims, Unwrap UVW modifier effects (§10y's mod:02df2e3a, 25 uses). Triage after (1)–(3) close.
  8. Modifier tailUVW Map (planar DONE §10z-douze; non-planar gated), Unwrap UVW (36 uses), FFD(box) (20 uses), Nel VertexTreePaint DONE §10z-quat, and Map Extender (mapext198m3.dlm, 118 export nodes) FULLY CLOSED §10z-quatorze/quinze/seize — cache decode (shared MAPEXT library), the 4 sky-dome "stale cache" refusals were our Sphere-hemisphere defect (fixed → 118/118 applies), --compare-mapext-mask verification tier splits the bucket (113 verified + 3 diff; per-node matrix in defective_max_files §4.4 — 8 references even carry VALID UVs matching the cache), and the drop-in Max plugin (§10z-seize) makes legacy scenes view/collapse/export correctly in Max itself, validated 118/118 in live Max 2010. Also landed §10z-quinze: Morpher bsList (visage class-mismatch closed — non-empty bsList forces the CMeshMRM branch like the reference). Unwrap UVW / FFD(box) / UVW non-planar still open.
  9. UV mapping-coord generation for parametric primitives DONE §10z-sept (Box/Plane) + §10z-onze (Cylinder + Sphere). All four primitive UVs now emit per-corner UV verts through PRIMMESH::buildParametricMesh's uvVerts output. Corpus-validated against ~/shape_export_dataset primuv_{box,cyl,sphere,plane}. Cylinder/Sphere seam handling uses a within-tri shift rule: consecutive corner u values are kept within 0.5 by adding/subtracting 1 (reproduces Max's u=1.0 or u=-0.125 duplicate UV verts at cap-fan seam-crossing tris without special-casing). Cylinder uses u = fract(0.75 + k/N) (0.75 offset places seam at +Y direction). Sphere uses u = k/segs, v = 1 - i/rows (INDEX-based, not by latitude z — Max maps UV rows uniformly regardless of the actual cos-spaced z coord).

C. Anim depth

Shape-process anim surface closed §10k-bis (2026-07-09). Prior state only emitted texture-matrix tracks; a full NelExportAnimation for AUTOMATIC_ANIMATION nodes is now a separate pass over all scene objects (matching shape_export.ms for node in objects), covering:

  1. Animated light groups — Omni lights with NEL3D_APPDATA_LM_ANIMATED + LM_ANIMATED_LIGHT name → LightmapController.<name> BezierRGBA color tracks. Source: stuff/animated_light/fyros_city_animated_lights.max. 11/11 byte-identical vs ~/pipeline_export/common/objects/shape_anim and core4_data/objects.
  2. Node transform tracks (scale/rotquat/pos) — e.g. conerotor.anim / rotor.anim from taverne constructions; byte-identical.
  3. Material texture-matrix tracks — already §10k; re-validated 18/18 waterfall sample.
  4. Morph tracks — path wired (same as anim exporter); corpus signal is 1 shape_anim file.

Biped IK: legs default (§10r); arm-pin default ON (§10s-quat small-D + §10s-cinq large-path dual-fold + §10s-sept moderate-miss FK hold when wrist miss < 6.5 cm — coup_fort_03 residual plant 0.51→0.23; static 2-knot / 3-knot large / course stay FK). §10s-six: limb pole [22..24] channel built (planted-only); apply behind PMB_BIPED_IK_POLE=1 after default-on regressions; offline GetIKActive confirms both-keys plant gate. Parked: FK-floor residual on some plants, foot-worst a1m coups, in-plant foot rotation (PODA hold rejected), blend-ramp — structural-exact with bounded in-between error is the shipping bar.

D. Ig export — open correctness items

Standalone (§10g) is GREEN as of §10w (2026-07-08): budget retired to 0, 54/54 direct-ref field-exact under uninit-mask. Ligo (§10g-bis) is MOSTLY near-green with 8 open diffs (DIFF_BUDGET=8, down from 89 → 28 → 8 in one session); the two large open classes that occupied this section (cluster-containment defect + selection-order divergence) both closed via §10w. The remaining 8 are documented as a punch list for the next session — small and mostly precision-tier, not structural. Historical §10g/§10g-bis budgets and their un-root-caused states are kept in the design doc; this section is now the current open list.

  1. Cluster-membership / vertex-in-volume containment. CLOSED §10w — the shared defect that surfaced as the standalone's TR_hall_reu_vitrine diff AND ~65 of the ligo path's 89 was the Edit Mesh 0x0130 created-verts chunk never being read (16-byte srcTag+Point3, mis-decoded as 0x0210 in the §10g decode note); any node whose base mesh got fully deleted and rebuilt in Edit Mesh evaluated to an empty world mesh, missing all cluster containment. Both cases green after the fix. The gen_decors_probe.ms Max 9 dump the user contributed (~/decors_probe/manifest.txt) also refuted the earlier "Mirror plane two stories down" hypothesis — the mirror offset=-24.9164, axis=Z, gizmo tz=1.78 produce exactly our current world-Z range.
  2. Ligo selection-order divergence. CLOSED §10w — closed by two orthogonal fixes: (a) depth-first scene-tree walk for the per-category iteration (buildTreeOrder from ssc->scene()->rootNode(), matches MaxScript's $geometry/$lights/$helpers), which closed the sibling-parent-interleave cases (desert nb01..nb05 etc.); (b) the ligo XRef pre-pass now filters exactly like the maxscript — single-step source resolution (resolveXRefObject() recurses, so peel one step and stop at node->getReference(1)), reject nested-XRef sources, gate on {Geom, Helper, Light} superclass — which closed the 23 village-bundle-file class (fy_module_village_nb_01..05, zo_imm_village_nb_01..05, tr_village_nb_01..05, jungle foret-18..21_village_a..d, etc.). Kaetemi's Max 9 gen_ig_selorder_probe.ms run pinned the maxscript rule that made the fix possible.
  3. Byte-identity backlog. Under x64/SSE the standalone remains 0/54 truly byte-identical — the reference exporter's SunContribution/Light[] uninit-heap + STL-light-tie-order class is a legitimate tolerance not a bug; ligo is 71/537. Under VS2008/x87 the same code produces 130/537 byte-identical (59 files gain full byte-identity, x87 codegen zeros the uninit heap x64 leaves random). Standalone doesn't shift because its uninit fields are per-instance (every ig has some), whereas ligo has many small igs where the uninit fields don't get exercised. Byte-identity is a stretch goal, not the gating bar (matches the zone/skel precedents).
  4. Remaining 8 ligo diffs (the current open list).
    • ~5-6 files, PS-instance clusterize AABBox borderline containment (fy_sheriff inst 23/26, fy_warschool inst 48/49/79, street inst 103/113/117, oasis_kami_auberge, ma_serre_village_a): the pss->getAABBox() corners re-boxed in world space (via CAABBox::transformAABBox — the reference plugin's own behavior, corpus-validated) land within CLUSTERPRECISION = 5mm of a cluster plane on rotated FX-instance bboxes; ours attach the PS to 2/3 clusters where reference attaches to 1/2. Confirmed VS2008/x87 build produces the SAME diffs → this isn't x87 codegen precision, it's a genuine borderline case. LOW real-content risk (extra links harden visibility culling rather than drop content), acceptable tolerance-tier candidate. Would need a Max-side dump of the shape's getAABBox() bytes to fully pin whether the reference used a smaller/different bbox or the CLUSTERPRECISION test itself.
    • 3 singleton unclassified files: taverne.ig inst 53 palme_type_C3_taverne Scale ≈3e-5 (may be a tighter epsilon needed on Scale — same magnitude class as the standalone POS_EPS); jungle-2.ig inst 0 ju_water_jungle_coude Pos differs by ≈15cm in X, ≈1.4cm in Y (real content difference — likely a modifier/controller decode gap for this water node); fy_cn_fortress_a_brandon.ig inst 38 Scale ≈1e-5 + inst 97/99 Clusters (1 vs 3) / (0 vs 2) — this one is the opposite direction from the PS borderline class (we find FEWER clusters, not more), suggesting a mesh evaluation issue on FY_CN_fortress_int_03/04 specifically. Not yet triaged individually.

Whoever picks this up: the punch list is small enough now to close each item on its own. Retire by fixing diffs and re-tightening the budget — don't raise. When it hits 0, tighten to 0 in ligo_ig_corpus.py and re-grade the table row from MOSTLY to GREEN.

Suggested session plans

Session 1 (Sonnet-scale): .pacs_prim + .cmb. DONE 2026-07-08 (§10v/§10x) — pipeline_max_export_pacs_prim (493/493 byte-identical) and pipeline_max_export_cmb (16/16 direct files T1/T2, 13/16 T3 exact-or-tolerance, 3/16 documented open gaps). Extended 2026-07-09 (§10z-ter): 0x0210 correctly decoded as face-vertex remap (was misclassified as map-1 texture-vertex faces), EditablePoly + parametric-primitive extraction paths added to cmb, ligo tier gated (LIGO_DIFF_BUDGET=105, was informational — same "load-bearing Max export step" as its ig counterpart, feeding land_exporter → the shipped rbank). pacs_prim_corpus.py/cmb_corpus.py wired into ctest. Ligo T3 state: 32 byte-identical + 84 float-eq + 105 differ + 0 export failures across 221 measured igs. Open follow-ups listed in item A.1 above.

Session 2 (Opus-scale): shape M2 coverage frontier. DONE 2026-07-08 (§10y) — the M2 breadth landing that added CMeshMultiLod (+418 files), parametric-prim shapes (+37), Edit Mesh shared-library migration (defect fix + code consolidation), and --compare-lightmap-mask mode (1263 lightmap-bucketed → 982 verified + 281 diff). Corpus: exported 2141 → 2596 (+21% coverage). See §10y for the full session summary + open items handoff.

Session 3 (Opus-scale): skinned/MRM shapes. DONE across §10z-sept → §10z-quinze — coverage landed §10z-sept (653-file class closed), and the decode reached weight-exact + whole-file byte-identity through §10z-douze (1-based cross-links) / §10z-treize (world-space VBs + link-parent rule) / §10z-quinze (rigid one-link) — the planned differential dataset was never needed; corpus data + the reference shapes pinned every rule. The success metric was met: every skinned outcome is in an explained tier (byte-identical / FLOATEQ / MRM-structural — the last being the measured 2004-vs-now MRM builder divergence on float-noise inputs, ~550 files, informational).

Session 3b (Sonnet-scale, after Session 3): the M2 tail — CWaterShape CFlareShape CSegRemanence, interface meshes. Water DONE 2026-07-09 (§10z, FLOATEQ §10z-bis). Flare DONE 2026-07-09 (§10z-bis). Remanence DONE 2026-07-09 (§10z-dix) — shared SPLINESHAPE (max_geometry_formats Part N) + REMANENCEBUILD; 82/82 produced, trail subset 78/78 FLOATEQ-or-better. Interface border-weld DONE §10z-quinze (residual: the ~75-file border-normals class). Residual 14 mesh-eval CLOSED §10z-douze — the "reference yields no mesh .shape for plain splines" assumption was wrong: real references existed for all of them (open splines → EMPTY CMesh, closed splines → the corpus-reversed min-interior-angle XY ear-clip cap, shared SPLINEMESH, MGF §N.5); shape not-produced is down to 1 (the gauntlet). Full spline tessellation for cmb XRef Shape sources (~35) remains the shared follow-up.

Session 4 (Sonnet-scale, anytime): shape-process modifier tail — UVW Map non-planar, Unwrap UVW (36), FFD(box) (20). Nel VertexTreePaint DONE §10z-quat; Map Extender DONE §10z-quatorze (cache decode); UVW Map planar DONE §10z-douze. Remaining: non-planar UVW types (gated), Unwrap UVW, FFD(box).

Session 5 (Sonnet-scale): .veget DONE (§10z-onze/douze), .clod DONE (§10z-clod). Whole Max-side export surface now has a headless tool. .clod residual quality (21 files) is the ConvertToRigid/MRM/normals/UV tail — not coverage.

Waterfall texture-matrix anims DONE §10k. Shape-process light-group + transform anims DONE §10k-bis (11/11 light + transform samples byte-identical).

Non-negotiables for every session (from §12, restated)

  • Load the session baseline first: all pipeline_max sources + the design doc (see the pipeline-max-session-baseline memory note / §12).
  • Corpus-gate EVERYTHING: T1/T2 byte-identity must stay green over the full .max corpus for any parser change; new exporters get their own T3 vs ~/pipeline_export references with explicit tolerance tiers; per-file forked runs (parse→clean→build→disown; COFile, not CMemStream).
  • The .ms scripts + workspace config (~/ryzomcore_leveldesign/workspace) are the ground truth for WHAT gets exported from each source file and with which options — read them before the C++.
  • Emission-form preservation, typed-chunk order preservation, and the §11 hazard list apply unchanged.
  • Don't rebuild binaries mid-sweep; don't trust per-file probes over full-corpus A/B (§10r method notes).

Clone this wiki locally