Build and exact matching
September 2, 2026 · View on GitHub
Toolchain
The inherited build reproduces the upstream Visual Studio .NET 2002 (VC7) and
DirectX 8 environment, generates build.ninja, and invokes the Windows tools
through scripts/th08run.bat. On Linux/macOS the wrapper runs them through
Wine.
Host requirements are:
- Python 3.11 or newer for the reconstruction ledgers and analysis helpers
(
tomllibis used); the inherited compiler build itself remains compatible with Python 3.4; msiextractand Wine on Linux/macOS;aria2coptionally, for torrent-backed dependency acquisition;- the initialized Detours Git submodule for optional DLL builds.
Create the environment on Linux/macOS with:
git submodule update --init --recursive
./scripts/create_th08_prefix
The helper downloads historical toolchain inputs into ignored local paths and
uses ~/.wineth08 as its default Wine prefix. Set WINE before invocation if
a compatible alternative runner is required. On Windows:
python scripts/create_devenv.py scripts/dls scripts/prefix
Do not commit downloaded compilers, SDKs, prefixes, or original game files.
Builds
The canonical normal build is:
python3 ./scripts/build.py
It regenerates build.ninja and builds build/th08.exe. Other inherited build
modes are selected explicitly:
python3 ./scripts/build.py --build-type bugfix
python3 ./scripts/build.py --build-type diffbuild
python3 ./scripts/build.py --build-type dllbuild
python3 ./scripts/build.py --build-type objdiffbuild
These modes serve different runtime and comparison purposes. Success in a bugfix, DLL, or object build does not establish that the normal executable matches the original.
Target detection
Place the privately supplied exact target at resources/th08.exe and verify
its identity before comparison:
sha256sum resources/th08.exe
stat -c '%s' resources/th08.exe
reccmp-project detect --search-path resources/
The required SHA-256 is
330fbdbf58a710829d65277b4f312cfbb38d5448b3df523e79350b879213d924
and the required size is 840704 bytes. reccmp-project.yml sets
allow_hash_mismatch: false; do not weaken that gate.
Executable comparison
After the normal build, register the reconstructed executable from build/
and generate a report there:
cd build
reccmp-project detect --what recompiled
reccmp-reccmp --target th08 --html report.html
The report consumes the mappings in config/reccmp-functions.csv,
config/reccmp-globals.csv, config/reccmp-floats.csv, and
config/reccmp-strings.csv. A mapped symbol is not necessarily implemented or
exact. Preserve the report as local evidence under build/; publish numerical
progress only through a reproducible generation path.
The progress generator reports source presence and exact matching separately:
python3 scripts/progress.py --check
Exact figures count only accepted entries in config/matches.csv; source
presence remains a separate inventory. See docs/PROGRESS.md for the generated
interpretation and counts.
Object comparison
objdiff.json maps reconstructed objects under build/objdiff/reimpl/ to
original/delinked objects under build/objdiff/orig/. Build all reconstructed
comparison objects with:
python3 ./scripts/build.py --build-type objdiffbuild
The build wrapper can also request one object through --object-name. For a
configured function, build only its translation unit and run the strict COFF
comparator against the verified executable:
python3 scripts/build.py --build-type=objdiffbuild --object-name ItemManager.obj
python3 scripts/compare-function.py item-auto-collect --json
The comparator requires the symbol, target address, size, and every COFF
relocation to agree with config/match-units.toml. Missing or extra
relocations fail closed. It reports exactness only for that configured function
range; it does not imply an object- or executable-wide match.
VC7 may include compiler-owned switch tables in a function's COFF auxiliary
extent. Such units keep size as authored code coverage and use compare_size
for the complete code-plus-table range. Exact acceptance still compares every
associated byte and relocation; table bytes do not increase authored progress.
VC7 source-shape notes
The following /Od behaviors are confirmed by small VC7 probes and by strict
TH08 matches; use them as diagnostics, not as permission to force bytes:
#pragma var_order(a, b, c)assigns listed function-scope locals from the least-negative stack slot downward in list order. Nested block locals are not reliably controlled by a function-level list. On large/Odfunctions, getting this order right can change total function size substantially because locals inside-0x80..-0x1use short EBP displacements while deeper slots require long displacements on every access.- A block-scope
#pragma var_ordercan control locals declared in that block, including a direct-initialized class local that receives a hidden return buffer. Do not put the pragma directly after a label: VC7 can mis-handle name visibility there. Open a normal{ ... }block after the label first. - Unlisted scoped locals can otherwise occupy earlier stack slots than expected. When a large function's frame size is right but every named local is shifted, identify the actual owners of the leading slots before changing semantics.
- Under
/Od, lexicalcasebody order affects emitted switch layout even when the numeric case values and jump table are unchanged. Likewise, ordinarybreakstatements can compile directly to the switch merge while explicitgotostatements may introduce short trampoline chains. Preserve the target's source-level control-flow shape where the bytes distinguish them. - Placement construction and tiny wrapper classes are not neutral stack-layout
tools here. In the tested VC7
/EHscconfiguration they introduced extra constructor/placement-newmachinery, so prefer ordinary source constructs and compiler-native temporaries. - A user-defined empty default constructor can still materialize as a call under
/Od. If the target instead shows a plain aggregate copy (for example, one base load followed by three dword moves), keep the public/ABI type intact and use a function-local trivial aggregate with the same fields for the temporary. This can express the observed copy semantics without constructor machinery; accept it only when the strict comparator proves the full function. - Value context matters for x87 comparisons. A floating comparison used directly
as a ternary condition can branch on the status word without materializing a
source value, while
static_cast<ZunBool>(comparison)makes VC7/Odstore a 32-bit0/1temporary and test it before selecting the ternary arm. When the target contains that materialized boolean slot, reproduce the value conversion in C++ rather than inventing a named stack local. - Boolean grouping can change temporary ownership even when the truth table is
identical. Under VC7
/Od,gate ? (a && b) : falsecan materialize the inner conjunction and then copy it into a second ternary-result slot; flattening the same logic togate && a && bremoves that outer slot. Preserve the target's expression grouping when stack shape shows both temporaries. - Ternary comparison direction and integer signedness can change the exact
branchless select sequence even when the result is algebraically identical.
For example, VC7 may emit
setlplus a signed difference mask forvalue >= limit ? negative_a : negative_b, while the inverted<form or unsigned hex constants selectsetgeand a different mask/add sequence. Preserve the target's comparison direction and signed value context before trying to tune registers around an equivalent expression. - A one-bit bitfield assignment has its own read-modify-write shape: VC7 can evaluate and mask/shift the RHS first, then load the containing word, clear the destination bit, OR the shifted value into that word, and store it. An equivalent hand-written integer mask/OR expression can choose the opposite OR destination register. When the target shows the bitfield pattern, a typed bitfield view is a more faithful C++ expression than algebraic register tuning.
- Recovered typed fields can also fix evaluation order that raw byte-pointer
arithmetic gets wrong. A raw
*(u32 *)(p + dst) = *(u32 *)(p + src)may make VC7 prefetch the left-hand base before finishing the RHS; expressing the same operation as a real field assignment such asvm->color1Initial = vm->color1can restore the target's RHS-first register sequence without inventing a stack temporary. Prefer the existing ABI type when the offsets are already proven. - Signedness of byte fields is visible in exact codegen: plain
charmay producemovsx, while the target'smovzxis evidence for an unsigned value context. Cast the read tou8(or recover the field type) instead of masking the result after sign extension. - Keep nested condition ownership intact when an
else ifbelongs only to the outer condition. Flatteningif (gate) { if (a && b) body; } else if (c)intoif (gate && a && b) body; else if (c)changes behavior whengateis true butaorbis false, and VC7 can expose the mistake as a near conditional jump where the target has a short jump to an outer merge trampoline. - For class-valued arithmetic, algebraic commutativity does not imply identical
VC7 hidden-return-buffer codegen. In the matched spell-effect interpolation,
delta / scale + baselets the division temporary stay inthisfor the finaloperator+whilebaseis pre-pushed as the RHS; spelling the equivalentbase + delta / scalechanges temporary creation and call setup. Preserve the target's operand order even for mathematically commutative operators. - An empty SDK/class default constructor can be target-visible under
/Od. A plain declaration such asD3DXVECTOR3 position;emitted the target's call to the empty constructor, while copy-initializing the same local from an existing vector elided that call and emitted three dword copies. Distinguish declaration from copy initialization when the target shows constructor timing explicitly.
For a function whose authored body is followed by compiler-owned tables, first
prove the authored extent independently, then set compare_size to the COFF
auxiliary extent and replay every table relocation as well. Local $L... COFF
labels are normalized to $L* by the comparator because VC7 renumbers them when
earlier code in the translation unit changes; their relocation offsets and
resolved target addresses remain the evidence.
When a shared header change appears to break unrelated strict units after a fresh rebuild, establish causality before editing manifests: save the current header, restore the committed header only for the failing object's rebuild, rerun that unit, then restore the current header and rebuild again. If the same size or relocation failure persists under the committed header, treat it as baseline staleness rather than attributing it to the new declaration. Only update a compiler-local relocation name when the A/B build proves that the current header caused the renumbering while machine code and resolved target addresses remain unchanged.
For stack, register-home, direct-call, absolute-reference, and return-cleanup facts, install Python Capstone and generate a read-only target packet:
python3 -m pip install capstone
python3 scripts/typed-re.py 0x004413E0 --compare --json \
> build/typed-re-004413E0.json
There is no VC7 library scanner yet. Do not borrow TH07 archives or infer library matches from names. A future scanner must start with SHA-pinned TH08-specific archives, relocation policy, and canonical comparator replay.
-
If a target stack layout matches a known ternary with compiler-generated boolean/result slots, do not promote those slots into named locals. In
Item::CollectPoint, two explicit decompiler locals displacedthis; restoring the samestatic_cast<ZunBool>(comparison) ? a : bshape already proven byCollectPointSmallrecreated the anonymous-0x10/-0x14temporaries naturally. -
A side-effecting loop condition can explain a target loop head that calls a helper, tests state, branches directly to the exit, and jumps back to the helper after the body.
while ((UpdateThreshold(), value >= threshold))matched that VC7 shape; spelling it as an infinite loop plusif (...) breakintroduced a two-byte branch trampoline even though the behavior was equivalent. -
When a switch-bearing function emits compiler-owned jump tables in the same COFF section, keep
sizeequal to the authored function extent but setcompare_sizeto the full source-emitted COFF section. This attests jump-table entries and padding without inflating authored-byte progress.Spellcard::Initis a concrete example: 0xCA5 authored bytes plus 0x63 bytes of three VC7 switch tables compare as a 0xD08 unit. -
Do not merge branch-local resource checks merely because the failure action is identical. In
Spellcard::Init, eachPreloadAnmbranch performs its own immediate null check and return path. Hoisting those checks to a common merge preserved successful-case semantics but changed branch topology and shortened the target by dozens of bytes. -
For a dense VC7 switch, compare the object jump-table entry targets with the target table before rewriting the whole dispatcher. If every later case start is displaced by the same constant, that constant is often exactly one missing case body. In
GuiImpl::RunMsg, cases 8 through 0x16 were all shifted by0xEC; reconstructing the missing 236-byte case 7 made every one of the 23 case starts land on its canonical address and also caused VC7 to emit the target function-levelpush esinaturally. -
Do not replace a target's repeated calls with a loop merely because the operands form a contiguous array.
GuiImpl::RunMsgexecutes its eight message VMs as eight lexicalExecuteScriptcalls; spelling them as4 + 2 + 2loops added exactly 26 bytes of loop machinery under/Os. -
Pointer-update spelling can determine VC7 register ownership. The adjacent-engine source shape
currentInstr = (Instr *)((i32)¤tInstr->args + currentInstr->argSize)preserved the base pointer in EAX and loadedargSizethrough ECX, exactly matching TH08. Algebraically rewriting it ascurrentInstr + argSize + 4reversed those roles even though the computed pointer was identical. -
A later case body can change the prologue of the entire function. Before GUI opcode 7 was restored,
RunMsghad the correct frame but no callee-saved ESI save; the real case's register pressure made VC7 emitpush esi/pop esiautomatically. Do not force such prologue bytes locally—restore the missing source body first. -
The type of an apparently neutral constant can control where VC7 converts an integer expression to floating point. In
Gui::DrawGameScene,GetPower() + 488 + 0.0femits the targetadd eax, 488; fild; fadd 0.0, whileGetPower() + 488.0fconvertsGetPower()first and emits a different x87 sequence. Preserve integer subexpressions and even a trailing+ 0.0fwhen the target shows an integer ALU operation immediately beforefild. -
VC7 can match a target stack slot only when the source also reuses one local across phases. In
Gui::FUN_0043741d, the target slot at[ebp-0x20]first holds the boss-life count and later the capped spell timer. Keeping separatebossLivesandcappedSpellcardSecondsRemaininglocals enlarged the frame by four bytes; one reusablebossValuerestored the target frame without padding. -
#pragma var_orderis lexical-scope sensitive in this codebase. A function-level pragma did not order locals declared inside the boss-HUD block. Moving the pragma into that block, while keeping declarations at their target execution points, restoreddark=-0x8,bright=-0xc,rect=-0x1c..-0x10,bossValue=-0x20,segmentIndex=-0x24,segmentStop=-0x28,timerColor=-0x2c,segmentWidth=-0x30, andtextPos=-0x3c..-0x34. -
A constructor can be target-visible even when the initialized value is overwritten before its apparent first use. The TH08 boss-HUD code constructs
Float3 textPos(48.0f, 16.0f, 0.0f)immediately before the first health-bar draw and later assigns(384,16,0). Omitting or moving that seemingly dead construction changed both size and control-flow offsets. -
Prefer correcting typed field order over compensating in expressions.
Gui::bossLifeBarMaxSizewas actually at+0x34while the previous header named the+0x30float as the max value. Swapping the two field names removed repeated target/object displacement mismatches throughout the health-bar code and preserves the proven segment arrays at+0x3c/+0x5c/+0x7c. -
For VC7 x87 comparisons,
a > bandb < aare not interchangeable source shapes. InGui::FUN_00435900, the target gauge testfld size; fcomp max; test ah,0x41; jnecame frombossLifeBarSize > bossLifeBarMaxSize; spelling it asbossLifeBarMaxSize < bossLifeBarSizeemitted the opposite operand/test-mask sequence despite identical C++ semantics. -
Preserve the target's outer condition direction when it controls a large lexical branch. The clock-display tail matches as
if (timer >= 60) { if (current < target) animate; else timer++; } else { timer++; }. Rewriting it asif (timer < 60) ... else if (...)selected a short conditional jump plus an extra branch and made the function two bytes short. -
Equivalent boolean regions can have very different floating-point branch layouts. The GUI portrait-alpha target is naturally
if (x >= 64 && y < 128) { fade down } else { fade up }; the De Morgan formx < 64 || y >= 128reversed both x87 test masks. Prefer the target fallthrough region over a logically equivalent negated predicate. -
A local array of a type with a non-trivial default constructor can explain an otherwise mysterious
eh_vector_constructor_iteratorcall.GuiImpl::DrawDialoguedeclaresVertexDiffuseXyzrhw vertices[4]; leaving it as a real local array naturally emits target helper0x406850with element ctorVertexDiffuseXyzrhw::VertexDiffuseXyzrhw @ 0x40B580. -
Preserve aggregate-copy spelling when the target copies a constructor temporary with string instructions. In
GuiImpl::DrawDialogue, the adjacent source shapememcpy(&vertices[i].pos, &Float3(...), sizeof(Float3))emitsFloat3::Float3followed by threemovsd/rep-style dword copies exactly like TH08. Replacing that with an apparently cleaner typed assignment can change register ownership and copy lowering. -
Do not merge adjacent label/value text calls just because one formatted string could display the same text.
Gui::FUN_0043826bhas a standaloneAddFormatText("Night Bonus"), advances Y by 16, then a secondAddFormatText(" %8d0", value). Combining them into"Night Bonus = %8d0"made the function exactly 34 bytes short and changed the static call-site layout. -
A compile-time string length can be the right way to prevent over-folding of a floating expression. In
Gui::FUN_00438a89,(384.0f - (f32)strlen("Spell Card Bonus!") * 14.0f) / 2.0f + 32.0fmakes VC7 fold the literalstrlento17.0fbut still emit the target five-step x87 multiply/subtract/divide/add sequence. Replacing it with the seemingly equivalent literal17.0flet VC7 fold the entire x coordinate to one constant and made the function 24 bytes short.
Acceptance rules
-
Verify the target hash before every new comparison environment.
-
Start at function/object scope and resolve calls, globals, strings, floats, imports, and relocation differences explicitly.
-
Preserve VC7 calling conventions, structure layout, compiler flags, source order, and translation-unit effects.
-
Do not use copied target byte arrays, naked assembly dumps, fake types, arbitrary padding, or behaviorally empty bodies to manufacture equality.
-
A successful compile or link means only
compiles. Claimmatchingonly for the exact scope demonstrated by the accepted report and command. -
Do not infer a repository-wide percentage from source coverage, mapping rows, or adjacent-version similarity.
-
Cache-vs-direct-object access can be target-visible under
/Os. In the GUI message initializer, caching&this->msgVmin a local pointer movedthisfrom-0x10to-0x14and shortened every later large-offset access. The target repeatedly formsthis + 0x21814; a local typed overlay is still useful as a type, but do not store a pointer to it unless the target has that stack slot. -
Preserve block placement, not just condition truth. The target's route setup spells replay handling as
if (!IsReplay()) { clear-history tree } else { replay tree }; moving the replay tree before the non-replay checks kept semantics but changed a large forward branch region. -
Recover table row types when address generation disagrees. A flat
u32[]indexed byshotType * 4generatedshl 2plus a scale-4 SIB; the target usedshl 4and a plain base+index load. Typing the table as 16-byte rows (u32 colors[4]) restored the exact addressing mode and removed one byte from each of four loads. -
In a small VC7
/Oscleanup loop, caching a repeated indexed field address in an explicit pointer local can make the function shorter than the target.Enemy::ReleaseChildEclBlocksis 96 bytes only when the threethis + i * 4 + 0x3384accesses remain lexically explicit; avoid **slotcache compiled to 88 bytes. Treat repeated address formation as source-shape evidence rather than automatically introducing a convenience local. -
Effect-specific storage can overlap semantically even when a shared struct currently gives the slots one effect's names.
ScreenEffect::CalcShakereads its interpolation endpoints from+0x18/+0x1C, while fade drawing also uses those slots for color-related state. Preserve the target-observed offsets untilRegisterChainproves a universal field meaning; do not force a misleading shared field name into another effect mode. -
Splitting a floating expression across assignments can be necessary to preserve VC7
/Odx87 spills. InScreenEffect::CalcShake, one combined interpolation expression emittedfimul/fidiv/fiaddin-register and was 12 bytes short. Reusing onef32 shakeAmountacross multiply, divide, and add assignments recreated the target's threefstpspill points plus interveningfild/fdivr/faddsequence exactly. -
A dense switch can reveal the original lexical case order through physical case-body placement even when the jump table is numerically indexed.
ScreenEffect::RegisterChainhas numeric cases 0..7, but the target body order is0,1,2,4,3,5,6,7; preserving that source order made VC7 emit the exact 0x274-byte body and 0x20-byte table. -
When a function's public struct fields are effect-specific aliases, the constructor/registration path can be stronger layout evidence than any single consumer.
ScreenEffect::RegisterChainproves+0x18/+0x1C/+0x20are generic param3/param4/param5 storage selected by effect type; consumers should keep target-observed offsets until a universally valid field model is recovered. -
Sibling formatted-text helpers need not share buffer size.
AnmManager::DrawTextRightuses a 128-byte local buffer, butDrawTextCenteredis exact only withchar buf[72]; that gives the target 0x70 frame and placesbuf/fontWidthat-0x50/-0x54. Infer local-array extent from frame/offset evidence, not neighboring source. -
For VC7 x87 range tests, explicit negation can be the exact source shape. In
Player::CheckBulletCancelCollision, nested!(min > point)and!(max < point)preserve the target operand order while selectingtest ah,0x41; jeandtest ah,0x05; jnp; flattening tomin <= point && max >= pointor De Morgan forms changes the status masks. The same function also needs a shared branch-localnextmerge for circle/rotated/axis misses rather than replacing every miss withcontinue. -
In
BulletManager::OnDraw, four expressions spelled ascosine * length + positioneach made VC7 spill the product before callingFloat3::operator float*, adding exactly 6 bytes and one compiler temporary per expression. Reordering the same arithmetic toposition + cosine * lengthremoved all four spills at once, shrinking the function by exactly 24 bytes and restoring the target 0x2C frame. For class/accessor-valued operands, preserve target evaluation order even when scalar addition is commutative. -
A raw dword copy can be the correct source shape when the target prefetches the destination base before evaluating the RHS. In
BulletManager::OnDraw, typedlaser->vm1.color1 = laser->vm0.color1evaluated the RHS base first; spelling the observed*(u32 *)(laser+0x494) = *(u32 *)(laser+0x1F0)produced the targetdst-base / src-base / value / storeregister order. Use this only when target offsets and widths are independently proven. -
Bullet::DrawSingleBulletconfirmsAnmVm+0x1FCis the signedtypefield, notpendingInterrupt(+0x1FE), and its angle update is source-shaped asZUN_PI / 2 + bulletAngle; reversing the add operands changes the x87 load/add sequence. -
A function's COFF auxiliary extent can contain multiple sparse-switch lookup schemes, not just a plain pointer table.
BulletManager::AddedCallbackhas a 0x67E authored body followed by 0xFF bytes containing a six-entry jump table + 111-byte selector table and a three-entry jump table + 108-byte selector table. Keep authoredsizeseparate fromcompare_size, and attest both pointer entries and selector bytes without counting compiler tables as authored progress. -
Sparse switch decompilation can hide target-visible dead writes. In
BulletManager::AddedCallback, script id 5 writes size class 3 and intentionally falls through into the script-106 body, which immediately overwrites it with class 4. Preserving that source fallthrough is required for exact code even though the first assignment is semantically dead. The target's physical case-body map was recovered from the jump-pointer and selector tables rather than trusting decompiler case grouping. -
BulletManager::AddedCallbackalso reinforces that a convenient row pointer is not neutral: caching&bulletTypeSprites[i]made the function 290 bytes short, while direct indexed expressions recreated the target's repeatedimul i,0xD44address formation. -
VC7 can choose the opposite setcc for semantically identical ternaries depending on which comparison is written explicitly. In
Item::CollectTimeOrb,time >= threshold ? A : Bemittedsetl, while the equivalenttime < threshold ? B : Aemitted the targetsetgeand the exactdec / and 0x107f / add 0xdfffef80select sequence. When a branchless ternary is otherwise exact, invert both predicate and arms before trying register-level workarounds. -
AABB tests can be exact only when the boolean region owns the same shared false block as the target.
Player::CalcItemBoxCollisionmatches as one OR-chain of failure predicates (minX > maxX || maxX < minX || ...) followed byreturn 0; return 1;. Nested negatedifstatements preserve finite-value semantics but change the final x87 branch/fallthrough and miss the target. -
ItemManager::OnUpdateshows that two visually identical vector resets may have different source shapes in adjacent states. Its state-2 reset uses aFloat3(0,0,0)temporary, but state-3/state-5 death resets are three direct component stores. Writing all three as aggregate assignments created two extra 12-byte compiler temporaries and enlarged the frame from target0x88to0xA0. -
Large state-machine block order can be recovered from temporary offset drift.
ItemManager::OnUpdateis exact only when autocollect is the positive body of a structuredif (state == AUTOCOLLECT || condition) { ... } else { normal fall }; a semantically equivalentnormalFallblock followed bygoto autoCollectdisplaced the homing block by 64 bytes even though the shared move label later realigned. -
A target function's auxiliary extent may be mistaken for authored code if a jump table follows the return immediately.
ItemManager::OnUpdatehas exactly0x7C5authored bytes and a 9-entry/36-byte compiler table, so its strict unit usessize=1989andcompare_size=2025. -
A relocation address match does not prove that the reconstructed literal has the target value.
ItemManager::OnUpdateoriginally appeared exact with source symbol__real@0000000000000000mapped to0x004B5B30, but the target bytes there are00 00 00 00 00 00 60 40, or double128.0. Because relocation replay replaced only the pointer field, the old comparison hid the gameplay-changing0.0/128.0difference. The corrected source emits__real@4060000000000000; its manifest row binds directly to0x004B5B30with zero addend and recordsdata_hex = "0000000000006040".compare-function.pyandvalidate-tracking.py --require-targetnow decode every 32-bit or 64-bit__real@...symbol and require its little-endian value to agree with the target bytes. An optionaldata_hexentry makes a reviewed high-risk literal explicit and must agree with both. Function instruction bytes alone are insufficient evidence for a relocated literal's semantics. -
Auditing all existing
__realrelocations after that fix found 1,548 floating-literal uses and exposed 12 stale entries across five already accepted units. Target data provesReplayManager::SaveReplay @ 0x004531F0scales the humanity ratio by10000.0f(0x004B42A0),BulletManager::RemoveAllBullets @ 0x00430830expands the laser item radius by32.0f(0x004B42CC),Player::UpdateRespawnAnimation @ 0x0044D180divides its animation timer by30.0f(0x004B4534), andInitializeDirectionalOffset @ 0x004270C0adds1.0f(0x004B4338).WarpBulletsAcrossNarrowBarrier @ 0x00423A60uses135.7645111f / 67.8822556ffor the zone-0 axis and the inverse ratio for the other axis; all eight repeated ratio relocations must retain that branch ownership. These are target observations. Their gameplay effects—score formatting, laser-item spread, respawn pacing, effect displacement, and mirror-barrier geometry—are semantic interpretations supported by the surrounding exact instructions. -
Tiny class helpers that already exist as natural C++ may still lack a standalone production owner. The Rng seed accessors were originally class-inline definitions; moving their unchanged bodies to
Global.cppproduced the target standalone VC7 functions (SetSeed,ResetGenerationCount,GetSeed) without changing semantics. Treat such moves as shared-header changes: fresh-rebuild callers and rerun their strict units before accepting the new owner. -
Existing COMDAT-emitted helpers can sometimes be attested without any source change at all.
Float3::operator+=andAnmManager::ResetFrameDebugInfowere already emitted by production objects and matched their target extents directly; source presence should still be separated from exact ledger acceptance until the canonical comparator is run.
VC7 dispatcher locals and lexical branch placement
Two recent strict closures expose source-shape rules that are useful for large VC7 dispatchers:
GameManager::GetClockTimeIncrement(0x43C35F) only reproduces its 0x38-byte frame when each of the six repeated stage cases has its own pair of integer locals. Calling equivalent getters or reusing one pair shrinks the frame and changes every case body. The target also requires explicitif/elsereturns; a semantically equivalent ternary lowers tosetl/incinstead. The authored body is 0x134 bytes followed by a compiler-owned 0x20-byte jump table.- In
EffectManager::OnUpdate(0x427BF0), an equivalentelse if (mode != 0)placed its body immediately after the test and produced a short branch, leaving the function at 0x309 bytes. Structuring the same logic asif (mode == 0) { nested cases } else { nonzero body }moves that body after the nested subtree, causing VC7 to emit the target long branch and the exact 0x30A-byte body. For near-exact large functions, inspect where physical branch bodies live before changing data or adding padding.
These are compiler-owned layout effects, not semantic differences. Prefer source restructuring and strict recompile over manual byte compensation.
VC7 conditional-result temporaries and collision gate layout
The Player collision cluster around 0x44A230..0x44A930 adds two reusable /Od source-shape rules:
Player::AwardGrazeneeds a nested conditional expression,extreme ? 3 : (moderate ? 2 : 1). VC7 materializes the outer conditional result in a hidden stack slot and then copies it to the named local. Writing(moderate != 0) + 1is semantically identical but lowers throughneg/sbb/neg/addand removes the target hidden result temporary. When a decompiler shows an anonymous value copied into a named local immediately after a conditional, preserve the conditional-expression ownership rather than algebraically simplifying it.Player::CalcLaserHitboxuses four nested negated overlap gates for its first rectangle test. A failure exits the nested region and naturally falls into the graze path; only four successful gates execute the explicitgototo the lethal path. Flattening the same test into a positive&&or a failure||changes the final x87 branch sense and introduces/removes compiler trampolines. The same!(min > max)/!(max < min)spelling also preserves the targettest ah,0x41andtest ah,0x05masks.
The laser helper also demonstrates that a seemingly interchangeable argument can be target-visible: the successful graze callback passes &this->position, not the incoming laser position. That changes the call setup by nine bytes even though both positions may be close in gameplay semantics.
VC7 cold-path placement and arithmetic spelling
Player::Die (0x44AB40) shows two more source-shape details worth preserving in large control-flow functions:
- The target keeps the normal
bombs >= 1deathbomb path lexically before the rare Miss path. This makes VC7 emit a six-byte forwardjlinto the cold tail block. Writing the equivalentif (bombs < 1) { Miss } else { main }places the cold block first and shrinks the branch to a short inverse jump, shifting the remainder of the function. - Doubling the pre-death counter must be written as
counter += counter;counter *= 2lowers toshland removes the target's repeated receiver/load/add/store sequence under/Od.
When a near-exact function is short by only a handful of bytes, inspect cold-path lexical placement and apparently trivial arithmetic rewrites before adding locals or touching data layout.
Scope-owned temporaries, probe aliases, and duplicated source bodies
The death-flow closures at 0x42ADB0, 0x42BEA0, 0x44C650, and 0x44CBA0 add several reusable VC7 rules:
- Constructor placement follows lexical scope under
/Od. InEnemy::DetachEnemyChain, the firstFloat3must be declared only after the parent-chain test succeeds, while the attached-enemyFloat3is declared only inside the attached tail path. Declaring both at function entry moves constructors and changes every later stack slot. - When two independent block locals refuse to occupy the target slots, a trivial local aggregate can express the original ownership without padding. The exact ADB0 shape uses a two-int local struct so
itemCountand the loop index occupy one contiguous eight-byte allocation while the separateitemTyperemains in the shallow slot. This is source structure, not manual stack padding. - Algebraic equivalence is not instruction equivalence on x87.
(f32)itemCount * 2.0flowers to targetfild; fadd st,st; adding two separately cast copies can lower to a longer integer-memory add. Likewise, random coordinate updates inEnemy::DropItemsmust use compound+=so the lvalue pointer returned byFloat3::operator float*()survives the RNG call in the target compiler temporary. - Do not deduplicate repeated source bodies just because they are semantically identical.
Player::UpdateBombStatecontains two copies of the “consume all remaining Bombs” path under the forced/non-forced deathbomb branches. Combining them withisForced || bombs < 2removes 39 target-authored bytes. - A probe alias is not automatically a production global. The analysis name
g_EclEnemyTableF54CC0resolves tog_EnemyManager + 0x9DCDA0in the shipped image. Production code should reference the realEnemyManagerstorage and let the COFF relocation carry the field addend instead of creating a second global at the same address. - Bitfield-to-bitfield assignment can be target-visible.
Spellcard::InvalidateCaptureAndEnableBombDamageonly reproduces VC7's redundant-looking mask sequence when bit 7 is assigned from bit 0 through a one-bit overlay; simplifying it to whole-word arithmetic changes register ownership and bytes.
If a header change is correct but VC7 reports a newly declared member as absent, verify the precompiled header timestamp. This repository's object-only path can reuse a stale build/th_pch.pch; forcing a PCH rebuild is preferable to changing valid declarations to satisfy stale compiler state.
VC7 structure-copy order, signed bit extraction, and compiler-owned dispatch data
The BulletManager closure around 0x42A410..0x43216A adds several source-shape rules that recur in large /Od state machines:
- A fixed-size copy that ends in
rep movsddoes not implymemcpysource.CopyBulletAnmVmCoreis exact only as a typedAnmVmstructure assignment: VC7 then emitsESI = src; ECX = 0xA9; EDI = dst; rep movsd. The intrinsicmemcpy(dst, src, 0x2A4)emits the same copied bytes but loads the count beforeESI, missing eight authored bytes. Infer the source operation from register-evaluation order, not only the final string instruction. - Masking a signed field does not automatically make its later right shift unsigned. In
Bullet::AdvanceTransformProgram,(record->int0 & mask) >> nproducedsar; the target usesshr. Cast the stored signed dword tou32before mask/shift when the target extracts packed unsigned subfields. - Avoid a result-valued floating ternary when the target writes each branch directly to its destination. The
0x400/0x800transform case is exact asif (value >= 0) field = value; else field = currentSpeed;. A ternary introduced one hidden dword at-0x22C, enlarged the frame from0x228to0x22C, and added six bytes. - One authored function can own more than one contiguous compiler-generated dispatch structure.
Bullet::AdvanceTransformProgramhas a0x81Bauthored body followed by a 20-byte pointer table and a 64-byte selector map (compare_size = 0x86F).BulletManager::SpawnSingleBullethas a0x85Aauthored body plus a 36-byte nine-entry jump table, whileBulletManager::OnUpdatehas a0xF16authored body plus a 20-byte five-entry table. Count only the authored body in progress, but strict-compare every compiler-owned byte when the COFF auxiliary extent includes it. - VC7 can renumber named compiler/local-label symbols when earlier functions in the same translation unit change even though the relocation offset and resolved target are unchanged. The strict comparator therefore normalizes only the trailing numeric suffix of labels shaped like
$name\$12345(and existing$L...labels), while still requiring the semantic label stem, relocation type/offset, resolved target, and raw bytes to match. Do not treat this normalization as permission to ignore relocation targets.
The same cluster reinforces that adjacent-version source is scaffolding only: TH06 helped identify the broad bullet/laser state machine, but TH08 moved transform behavior into a separate 18-record interpreter. The TH08 shipped image, target relocations, and fresh VC7 objects remained the acceptance evidence for every recovered branch and field.
Fastcall parameter homes, slot reuse, and large Player state-machine layout
The Player closure around 0x44AEC0, 0x44D650, and 0x451640 adds several useful VC7 /Od rules:
- Do not promote obvious assignment temporaries to source locals merely because the decompiler names them. In
Player::AddedCallback, the two floats at-0x18/-0x1Care compiler temps created while assigningg_PlayerPlayfieldWidth / 2andg_ItemPlayfieldBottom - 64throughFloat3::operator float *(). Declaring them explicitly pushed the fastcallPlayer *home from target-0x14to-0x1Cand enlarged/shifted the entry sequence. Writing the expressions directly restored the target home slots automatically. - One physical stack slot can represent source variables with different signedness in different lexical loops.
AddedCallbackreuses-0x4: the 384-entryPlayerCollisionRegionreset loop lowers as unsigned (jae), while the later 128-entry shot-slot loop lowers as signed (jge). Keeping oneu32source slot and spelling the second condition as(i32)i < 0x80recovers the target without inventing another local. - Equivalent flat indexing can change address-generation ownership. The option callback tables are exact as rows of four pointers indexed
table[route].callbacks[slot], which makes VC7 formroute << 4first and then applyslot * 4. Flattening totable[route * 4 + slot]is semantically identical but changes the register/evaluation sequence. - A large switch's lexical case order is observable independently of the numeric case values. Both movement-speed switches in
Player::UpdateMovementAndOptionsare exact with physical case order4, 3, 1, 2, 5, 7, 6, 8; two compiler-owned eight-entry tables map numeric cases back to those blocks. Strict comparison usessize = 0x12A1andcompare_size = 0x12E1, counting only authored bytes while still verifying both tables. - When IDA pseudo-code and shipped control-flow disagree, trust the shipped image. The team-route animation block at
0x44AEC0is one example: the physical route/odd-even script selection was recovered from target branches and strict comparison rather than the initial decompiler arm labels.
Shipped-vs-analysis-database target safety
- Preserve shipped executable semantics when the analysis database contains a research patch. At
0x44D0F9, the shipped v1.00d bytes arepush -1beforeGameManager::AddLives; the IDA database had been intentionally patched topush 0. Reconstruction followsresources/th08.exe, not the patched pseudo-code.
Canonical relocation owners and local-array extent
- Raw bytes are not enough to justify a field owner.
Supervisor::CalculateFpsinitially matched with source expressions that referenced the wrong members while a generated relocation manifest compensated by shifting the global base. Always verify each recovered relocation base againstconfig/reccmp-globals.csv; the exact owners here areg_GameManager + 0x2D,g_Supervisor + 0x300,+0x178, and+0x33C. Exact source should make both bytes and relocation ownership canonical. - An IDA stack-gap inference can overestimate a local array.
Supervisor::CheckFpslooked likefloat samples[31]from the decompiled frame, but VC7 only reproduced the target0xA4frame and[ebp+index*4-0x88]addressing withfloat samples[29]. Preserve target-visible dead locals (the elapsed-seconds calculation), then use fresh compiler stack allocation as stronger evidence than decompiler array guesses. - For
/Oscontrol flow with a shared success epilogue, lexical ownership matters.CheckFpsmatches asif (!disableVsync) { diagnostics; if (average >= 65) { ... return -2; } } return 0;; spelling the two success paths as separate early returns adds a short inverse branch plus a trampoline. - Commutative integer addition can determine register ownership.
CalculateFpsrequirescurrentQpc >= lastQpc + (frequency >> 1)in that operand order to emiteax=frequency/2; ecx=last; add ecx,eax; cmp current,ecx; jb. Equivalent orderings folded one operand into memory or reversed the compare.
D3DX projection owners and x87 comparison width
- Call arity plus the shipped push sequence can recover imported D3DX helpers without guessing library bodies. In
AnmManager::Project3DQuad,0x477178isD3DXVec3Project(out, input, viewport, projection, view, world),0x477612isD3DXMatrixMultiply, and0x477F42/0x477FC2/0x478043areD3DXMatrixRotationX/Y/Z. Use the standard D3DX API so production source remains ordinary C++ and the import thunks stay compiler/linker-owned. - A floating zero literal's type is target-visible. The three rotation gates in
Project3DQuadare exact only asrotation.axis != 0.0(double literal);0.0femits single-precision x87 compares and leaves exactly one opcode byte wrong at each of the X/Y/Z gates. - A contiguous 64-byte
rep movsdtarget copy is stronger layout evidence than an old field guess.AnmManager+0x1C24..+0x1C63is a cachedD3DXMATRIX, notFloat3followed by padding. Recovering the aggregate type lets VC7 emit the target matrix copy naturally. - Do not route new reconstruction through the repository's
sincosmacro when inline assembly is disallowed. VC7sin/cosintrinsics do not fuse intofsincos; functions whose target specifically requires that instruction should remain unclaimed until a non-inline-assembly source mechanism is found.
VC7 block-local slot order and exact float literals
- Two class-valued locals can have the correct constructor order but the wrong physical stack slots.
UpdateOptionHomingToPlayerandUpdateOptionHomingToTargetboth constructtargetthendelta; the target stores them attarget=-0x18anddelta=-0x0c. Keeping lexical construction order while changing#pragma var_orderfrom(target, delta, ...)to(delta, target, ...)fixed all 15 byte differences in each function without changing control flow. - Do not treat decimal float literals as approximate documentation.
DrawPlayerOptionwrites the exact single-precision value produced by0.49f(0x3efae148). Spelling it as0.495fproduced the same instruction shape but a different three-byte tail in the immediate. Recover the source literal/constant that rounds to the shipped IEEE value, then let the compiler emit it.
Constructor lifetime placement and recovering absolute-address owners
#pragma var_ordercontrols physical slots but does not move constructor calls. InAnmManager::ProjectCameraFacingQuad/ProjectCameraFacingQuadWithCallback, declaringD3DXMATRIXand fourFloat3locals at function entry emitted their constructors before the target'srotation -> fsincossequence even though every slot was correct. Keeping scalar declarations first, executingfsincos, and only then declaring the class-valued locals moved the five constructors to the exact target offsets without changing the 0xA0 frame.- Before creating a new production global for a target absolute address, subtract nearby known object bases. The projection reference at
0x004EA3F4is exactlyg_Background + 0x63C4, i.e.g_Background.unk6394.vectors[4]. Referencing the real aggregate owner preserves the correctg_Backgroundrelocation plus field addend and avoids overlapping storage aliases.
Byte-lane writes versus packed-color RMW
- A source write to one byte of a packed color is not necessarily equivalent to the target's dword operation.
EffectOrbitUpdate @ 0x426030keeps the existing RGB low 24 bits, converts the computed alpha with__ftol2, shifts it by 24, ORs it into the preserved dword, and writes the full color back. Spelling the operation aseffectBytes[0x1f3] = alpharemoved the target's ESI save/mask/or sequence and made the function 29 bytes short. Express the observed whole-word read-modify-write in ordinary C++ when the target proves it; an extra(u8)cast before the shift also inserts a target-absentmovzx.
Compiler-owned RHS temporaries and shared return blocks
- In
Player::UpdateDeathAndRespawn, explicitspawnX/spawnYsource locals forced VC7 to reserve their stack slots before the member-functionthishome. The shipped code instead comes naturally from direct assignments such asposition.operator float *()[0] = expression: because the conversion call would clobber the x87 value, VC7 creates compiler-owned RHS temporaries at the point of use. This restoredvalue=-0x4,this=-0x8, an earlier ternary temp at-0xC, and the two late RHS temps at-0x10/-0x14without padding. - Repeated
return 0statements are not source-shape neutral under VC7/Od. Two explicit zero returns inPlayer::UpdateDeathAndRespawneach emitted a localxor eax,eaxbefore jumping to the epilogue. Spelling the same control flow as ordinaryif/elsearms let both paths jump to the single shared final zero-return block, removing exactly two bytes per path and matching the target extent.
Large draw loops: aggregate copies, operand order, and x87 loop ownership
EnemyManager::OnDrawImpl @ 0x42e140recovered its targetsub esp, 0xA4frame without padding by keeping the observed lexical local order and letting sixFloat3return temporaries remain compiler-owned. Source-level locals for these temporaries would change the frame layout.- Adjacent scalar member copies can be source aggregate copies. Saving/restoring
AnmVm::scaleas aFloat2generated the target paired loads/stores and removed otherwise repeated owner-pointer reloads. Separate.x/.yassignments were semantically equivalent but byte-different. - Integer multiplication operand order matters at
/Od:savedAlpha * kproduced the targetmovzx eax,alpha; imul eax,[k], whilek * savedAlphaforced an extra register load and was two bytes longer at each fade site. - A threshold in a
forcondition is not byte-equivalent to a body-localbreak/continue. For the-990.0ftrail sentinel, spellingif (sample < -990.0f) break/continue;restored the target x87test ah,5plus shortjp/jmptrampoline. Algebraically negated conditions changed parity branches and jump ownership.
Large varargs report builders: loop ownership and allocation temporaries
ResultScreen::LogScoreDataToFile @ 0x454298recovered a 0x488-byte VC7 frame by preserving the target lexical stack order, including a 222-entry spell-name pointer array and a 64-byte temporary name buffer.#pragma var_orderis useful here only after the real source-visible locals are distinguished from compiler-owned homes.- A semantically redundant allocation temporary can be target-visible at
/Od. The target stores the 0x10004-byte block allocation first inblockCursor, then copies it into the persistentblocklocal. Writingblock = Alloc(...)directly made the function exactly 14 bytes short. do { if (!node) break; ... } while (limit)was not byte-equivalent to the target score-list traversal. The target comes fromwhile (node != NULL) { ...; if (entry >= 10) break; }, producing a direct nearjeat loop entry instead of a short inverse branch plus a near trampoline. Restoring that ownership fixed the final two-byte extent/direction difference.- CP932 target text can be reconstructed portably with the same
\xNNbyte-literal convention used by generatedi18n.hpp; this avoids host/source-codepage dependence while still letting VC7 pool ordinary string literals and emit canonical relocations.
Constructor lowering and optimized integer scaling
- An array of non-trivial elements is not source-shape equivalent to adjacent named fields under VC7.
Effect::Effect @ 0x4287e0needs nine distinctFloat3members:Float3 vectors[9]collapses the nine target-visible constructor calls intoeh_vector_constructor_iterator, while nine adjacent fields emit the exact individual call chain before the trailingZunTimerconstructor. - Optimization pragmas are part of the source-shape contract.
GameManager::ScaleIntBasedOnRank @ 0x421ba0only emits the target signed divide-by-32 correction (cdq; and; add; sar) inside the surrounding#pragma optimize("t", on)region;/Odemitsidivinstead. - Operand order remains target-visible even after strength reduction: spelling the final expression as
quotient + upperlets VC7 useadd eax,[upper], whileupper + quotientintroduces an extra register move and changes the extent.
Constructor families can recover object and global ABI together
Enemy::Enemy @ 0x42a280shows how constructor lowering exposes the real aggregate shape. OneAnmVmfollowed by anAnmVm[2], one 0x228 context followed by a 16-element context array, distinctFloat3fields, twoBulletSpawnDescriptormembers, a 96-row 0x1c trail array, a 194-row 0x1c textured-vertex array, and trailing timers naturally reproduce the target's individual calls andeh_vector_constructor_iteratorsites. Flattening these into byte blobs loses the constructor call graph.- A compiler-generated implicit constructor can be target-visible even without an explicit source declaration.
VertexTex1DiffuseXyzrhwhas a compiler-owned constructor because itsFloat3member is non-trivial; the existing AnmManager exact manifest already proves both the 0x14 untextured and 0x1c textured vertex constructor symbols fold to target address 0x40b580. The Enemy vertex array therefore needs the real 0x1c textured type, not a stride shim. - Reconstructing
EnemyManageras oneEnemyplusEnemy[481], a 0x30 target-observed region, sixteen 0x10 timeline rows, and the manager timer restores the target global size0x9DCF10. This also makes the following chain globals land at the correct relative offsets in the COFF data section, providing an independent ABI check beyond function-byte equality.
Manager constructor arrays can reveal sentinel ownership
EffectManager::EffectManager @ 0x428740proves the manager containsEffect effects[654]beginning at+0x1C, followed by five distinctEffectsentinel members. VC7 therefore emits oneeh_vector_constructor_iteratorfor 654 rows and five individual constructor calls; one 659-element array is byte-different.- The layout reaches
+0x8B03C; its 0x20-byte tail gives the same0x8B05Cextent independently observed inResetEffects, so constructor lowering and reset extent cross-check the ABI.
Collision predicates: preserve branch ownership and x87 operand order
Player::CalcDamageToEnemy @ 0x451670shows that!(a <= b && c <= d && ...)is not a byte-equivalent replacement for the target's direct separating-axisa > b || c > d || ...test. VC7 changes the x87 status mask and parity-branch direction even though the predicates are mathematically equivalent. Preserve the target comparison orientation instead of normalizing boolean algebra.- Two independent early exits should remain two source
ifstatements when the target has two distinct short trampolines. Combiningif (!active) continue; if (frame % interval != 0) continue;into oneif (!active || frame % interval != 0)made this function exactly two bytes short; splitting them restored the target 0x66E extent. - Rotated AABB and circle tests are sensitive to comparison operand order. Writing the target's bound on the left (
-halfWidth > projectedRight,halfWidth < projectedLeft,radius * radius < distanceSquared) restored the target x87 load order andtest ah/jp/jnpsequence. Reversing the comparison while preserving semantics produced a different instruction stream.
Large aggregate constructors can correct earlier field aliases
Player::Player @ 0x449ca0proves the complete constructor-visible Player aggregate:AnmVm @ +0x10, two leadingFloat3s,Float3[16], ten adjacentFloat3s,PlayerOptionState[4] @ +0x40c,PlayerBombState @ +0xfdc, two 192-row collision pools,PlayerShot[128] @ +0xbe838, three 0x10 timeline rows, two lateFloat3s, and sixZunTimers. This layout naturally emits the target 0x1a6-byte constructor with 30 relocations/26 calls.- Constructor evidence can invalidate an earlier standalone field name without changing the observed offset. The old
Player::stateColor @ +0x200alias lies inside the provenAnmVm @ +0x10; its real owner ismainVm.color1. Likewise the oldPlayer::frameStop @ +0xfdcalias is the first dword of the provenPlayerBombState. Move callers to the real aggregate owner rather than creating overlapping fields. - Cross-TU regressions are especially important after such a header lift. Keep object size and offsets fixed with
offsetofassertions, then fresh-check consumers; a nested member expression should still lower to the same base relocation plus addend when the ABI is truly unchanged.
Player bomb color/fade helpers
- VC7 keeps
ZunColorbyte-channel arithmetic source-visible at/Od. InSetBombBackgroundTint @ 0x40BC60, computing each RGB channel as0x80 - (0x80 - source.channel) * timer / 60preserves the targetmovzx/integer-division sequence; replacing it with packed-color arithmetic or a helper changes register ownership and code shape. ZunTimercomparison/operator spelling matters even when every expression is mathematically an integer comparison. The target bomb fade helpers use the overloaded timer operators for<,>=, subtraction, and integer conversion, which produces distinct target-visible calls to0x4066A0,0x40B8E0, and0x40D3B0. Cachingtimer.currentas ani32removes those calls and cannot match.- Two visually identical screen-fade callbacks can differ only in literal channel initialization.
DrawSlashOfPresentWorldBombandDrawSlashOfFutureEternityDeathbombshare the same control-flow/relocation skeleton and 0xCF extent; preserving separate lexical bodies lets VC7 keep their only semantic difference (white versus redZunColor) without introducing an abstraction call not present in the target.
Player bomb callback family: local ownership and repeated draw lanes
- For
#pragma var_order, the first listed scalar/pointer receives the shallowest EBP slot in these VC7/Odcallbacks.DrawFantasyOrbBombinitially emittedworkItem=-4 / i=-8 / vm=-C; changing the pragma from(workItem, i, vm)to(vm, i, workItem)restored the targetvm=-4 / i=-8 / workItem=-Cwithout changing behavior. - Signedness of an otherwise identical loop index is target-visible. The 16-entry work-item renderer at
0x40C820uses signedi32andjge, while the 128-entry sibling at0x40D010usesu32andjae; preserving only the bound but not the source type changes the branch opcode. - An apparently unused local can be required source shape.
DrawReturnInanimatenessDeathbombstores&player->bombState.workItems[0]in a stack local that is never consumed by the later C++ logic; removing that assignment changes the 0x38-byte frame/slot layout. Keep target-visible/Odlocals until strict comparison proves they are compiler-owned noise. - Float algebra is not freely interchangeable under VC7 x87 lowering. In
DrawMasterSparkBomb,motionStep + motionStepemits a 3-byte memoryfadd, making the function one byte too large;motionStep * 2.0freuses the loaded x87 value and emits the target 2-bytefadd st,st. - Repeated Player bomb draw callbacks often differ only by loop count, mix color, or whether the VM position is
positionversusposition + pos2. Preserve separate lexical functions instead of abstracting them behind a shared helper: the target callback table points directly at these bodies, and an abstraction call would be target-visible.
Player bomb callback family: shared initializer ownership
- A two-scalar copy can be six bytes larger than the target even when both fields are adjacent. In
Player::SpawnBombStateEffect, assigning the two scale components separately reloads the effect pointer twice; assigningFloat2as one aggregate emits the target paired loads/stores and shrinks the function from 0x10E to the exact 0x108. - Reusing a cached subobject pointer is not always source-equivalent.
UpdateMasterSparkBombuses cachedbombfor its first timer checks, but the later modulo-four condition reloadsplayer->bombState.timerthrough the full+0xFF4displacement twice. Reusingbomb->timermade the function exactly six bytes short (two short-displacement instructions instead of two long-displacement instructions). - Equivalent member addresses can still encode different ownership. The first five script setup calls in
UpdateMasterSparkBombare emitted frombomb + {0x204,0x4A8,...}, not from cachedworkItem + {0x1B8,0x45C,...}. Preserve the source base object selected by the target even when the final address is identical. - Target-visible unused locals can carry semantic reconstruction clues. The same callback stores
-PIin a local that is not consumed in that body; retaining it is necessary for the target frame/slot layout and may reflect a shared source template with sibling bomb callbacks.
Player bomb burst callbacks: constructor timing and float-constant precision
- A non-trivial local's declaration point is code-generation-visible even when its final stack slot is already correct. In
UpdateQuadrupleBarrierBomb, declaring eachFloat3 velocity(...)at the top of its branch emitted the constructor before the target's common-init/collision calls. Moving the declaration to the exact point where the target constructs the vector fixed the first-difference without changing the 0x5C frame. - Block-local pointer declaration order can invert shallow/deep stack ownership around a non-trivial local. The 10/20/30-frame burst blocks require the spawned-effect pointer at the shallower slot and the collision pointer below it; matching that required preserving the target lexical declaration order rather than assigning both through a shared temporary.
- Float macro algebra can lose a target ULP at compile time.
ZUN_PIis explicitly narrowed tof32, soZUN_PI * 5.0f / 8.0ffolds to0x3FFB53D1; the shipped target stores0x3FFB53D2, the correctly rounded high-precision 5π/8 value. Where the target proves the stored float, use a float literal that rounds to that exact mathematical value rather than forcing arithmetic through an already-narrowed macro. - A parameter can be target-visible purely through calling convention.
EffectManager::SpawnEffectInFixedSlotWithVelocitynever reads its fifth source parameter, but retaining it is required for the target six-stack-argumentret 0x18; deleting the apparently unused parameter would change the ABI even if the body stayed equivalent.
Effect-strip vertices and bomb effect callback families
whileandforare not interchangeable at VC7/Odeven when their update expressions are identical.AnmManager::InitializeVerticalTextureStripwas four bytes short as twowhileloops; spelling the two odd/even vertex walks asforloops restored one two-byte entry trampoline per loop. The loop preheader order is also visible: target emitsi = N; currentY = y; jmp condition, notcurrentY = y; for (i = N; ...).- A block-scope
#pragma var_order(position, radius)can place a later-declared class local and an earlier scalar into the target slots without moving the class constructor.UpdateBarrierRadialEffectandUpdateRotatingBarrierRadialEffectrequireFloat3 positionimmediately after the radius calculation while physically assigning the three-vector above the scalar on the stack; function-scope ordering cannot express that shape. - Copy-initialized trivial
Float3locals can be target-visible without a default-constructor call. The 139-byte effect initializer wrappers copyeffect+0x2A4andeffect+0x2B0into two 12-byte locals;#pragma var_order(velocity, position)reverses their physical slots while preserving the target's lexical copy order. - Do not cache repeated
effect+0x338timer access merely to shorten source. InUpdateBarrierRadialEffect, aZunTimer *timercache made the function 18 bytes too short. The target reloads the full displacement for each comparison/conversion, and that repeated address formation is part of the source-shape evidence. - A ternary feeding one helper call can intentionally create a compiler-owned float stack temp.
UpdateRotatingBarrierRadialEffectneedsAddNormalizeAngle(angle, (index & 1) ? +delta : -delta)so VC7 materializes the selected delta once and then issues one call; duplicating the call inif/elsebranches changes both frame ownership and control flow.
Player bomb slash callback: field-width and branch-local state
- Target-observed member width can explain an entire extent mismatch.
UpdateSlashOfPresentWorldBombwas 21 bytes short because sevenslot+0x38stores were initially interpreted as bytemodewrites at+0x3D; the target uses dwordcollisionIntervalwrites at+0x38. Eachmov bytewas three bytes shorter than the targetmov dword, giving exactly7 * 3 = 21bytes. - Mutually exclusive branch-local
Float3 position = player->positiondeclarations can intentionally occupy distinct stack slots under VC7/Od. The 70/80/90/100-frame branches inUpdateSlashOfPresentWorldBombnaturally allocate four separate 12-byte locals, which in turn leaves the following six compiler-ownedFloat3expression temporaries at the target offsets. - For chained
Float3interpolation, preserve the exact operand direction. The outbound move is(workItem->motion - workItem->position) * t + workItem->position; the return move is(workItem->position - workItem->motion) * t + workItem->motion. Algebraically rearranging either expression changes hidden return-buffer ownership and the call sequence ofoperator-,operator*, andoperator+.
Large Player orbit/slash callback templates
- x87 branch ownership must match the target arm direction, not merely the predicate. In
UpdateGhastlyDreamBomb, spelling the outer radius test asif (radius < 500) { update } else { deactivate }emittedtest ah,5 / jp; the target comes fromif (radius >= 500) { deactivate } else { update }, which emitstest ah,1and the target short branch. - A
continuecan be target-visible even when falling through the end of anif/elsereaches the same loop update. The radius-expired arm inUpdateGhastlyDreamBombmust explicitlycontinue; otherwise VC7 jumps to the lexical loop tail first, changing only the four-byte branch destination while keeping the function extent identical. - Recovering a few fields can unlock multiple multi-kilobyte bodies. Naming
PlayerBombWorkItem::motionStep @ +0x8andeffectVm @ +0x16D8letUpdateGhastlyDreamBombandUpdateEternalSleepInDreamlandDeathbombshare the target's 128-row orbit update source shape instead of relying on raw byte offsets. ZunTimer::IsPeriodicis the periodic-event predicatecurrent != previous && current % interval == 0. Keeping it as the original member call preserves the target call boundary inUpdateEternalSleepInDreamlandDeathbomb; open-coding the modulo in each consumer would change both caller extent and timer access ownership.- A
breakbefore aforupdate can intentionally preserve the current iterator pointer for code after the loop.UpdateEternalSleepInDreamlandDeathbombfills at most 16 free work items and then passesworkItem->position.xto the positioned-sound call; rewriting this as a counter-controlled loop that advances the pointer before exit changes the target pointer value and the emitted control flow. - Sibling callback bodies can legitimately duplicate several kilobytes.
UpdateGhastlyDreamBombandUpdateEternalSleepInDreamlandDeathbombshare the same four 16-entry initialization lanes and 128-entry runtime lane, but the latter adds a periodic 16-slot burst. Keeping the repeated source lexical structure matches the target; factoring it into a helper would introduce calls absent from the shipped binary.
Effect interpolation setters and compiler-owned scalar temporaries
- A target stack slot that receives a ternary integer only to feed
fildneed not correspond to a source local. InAnmVm::FUN_0040EB50, declaring an explicitoffsetforced the scalar into[ebp-4]and displaced the compiler-ownedthishome to[ebp-8]. Inlining(timerChanged & 1) ? 8 : 0into the float expression lets VC7 keepthis=[ebp-4]and materialize the integer conversion temp at[ebp-8], exactly matching the target. - The explicit
& 1before a boolean ternary is machine-visible. Without it VC7 emitsneg; sbb; and 8; the target includesand eax,1first. Preserve low-bit masking when the target proves it even if the helper currently returns only zero/one. - Small interpolation setter families are best recovered as real
AnmVmmembers rather than open-coded raw writes in each consumer.FUN_0040EC30/ECA0/ED50/EDA0share the same two-timer-plus-mode-byte pattern but write different aggregate/color payloads; keeping four separate member bodies preserves their target call boundaries and argument cleanup (ret 0x10). - RGB unpack order is target-visible.
FUN_0040ECA0writes red, green, blue as independent byte stores extracted from the same dword, in the exact order target uses; replacing that with a packed color assignment would change both shifts and store order.
Effect aggregate layout and fixed-slot address generation
- A semantically plausible platform type can still be the wrong source owner.
Effect +0x304is emitted as a ninth ordinaryFloat3constructor followed by a scalar at+0x310; declaring oneD3DXQUATERNIONremoves the targetFloat3relocation even though both layouts occupy 16 bytes. Preserve the constructor family proven by the object, not just size and alignment. - Five adjacent manager sentinels are likewise not source-equivalent to an
array.
EffectManager::EffectManageremits five independent constructor calls after the 654-row vector constructor. Keep the sentinels as separate named members so the target constructor lowering remains intact. - In
EffectManager::SpawnEffectInFixedSlotandSpawnEffectInFixedSlotWithVelocity, naturaleffects[slotIndex + 0x280]indexing is two bytes shorter than the target under/Od. The exact ordinary-C++ source shape forms the byte address fromthis,(slotIndex + 0x280) * sizeof(Effect), andoffsetof(EffectManager, effects). This expression is intentional semantic pointer arithmetic, not unresolved layout debt. - Signed byte fields are target-visible through load extension. The Effect
release request/timer, alternate draw group, vertex-dirty flag, and
update-during-freeze fields must remain
i8; changing them tou8replaces targetmovsxconsumers withmovzxeven though stored values are currently small.
Branch-local vector lifetime and indexed-owner recovery in Player bomb callbacks
UpdateFantasyOrbBombshows that mutually exclusive class-valued locals must retain branch-local lifetime under VC7/Od. Hoisting the<40previousPositionand the latertargetPositionto function scope enlarged the frame from target0x5Cto0x74and inserted two entry constructors. Keeping them inside their respective branches restores the target slots (previousPositionat-0x1C,targetPositionat-0x34) and allows the compiler to reuse the frame.- Equivalent pointer ownership remains codegen-visible. In the initialization loop,
bomb->workItems[i].angle = angleemits the targetimul i,0x16F0indexed path; spelling the same store through cachedworkItem->angleremoves ten bytes. Preserve the target's source owner even when both expressions address the same member. - A direct target
sqrtfresult store toworkItem + 0x0Cidentified that location as persistent state (PlayerBombWorkItem::speed), not a disposable local. Promoting the field in the real row type fixed both later source shape and cross-callback ABI without changingsizeof(PlayerBombWorkItem). - Positive/negative branch ownership still matters after semantics are known:
if (!workItem->state) continue;emits the target short inverse branch plus short loop jump, whereas wrapping the large body inif (workItem->state)forces a six-byte near branch. Likewise, target-equivalent finite comparisons (tail.x > -100.0fvs an inverted<=form, andspeed > 10 ? 10 : speedvs the algebraically equivalent inverse ternary) produce different x87 masks/arms.
Function-scope non-trivial locals and duplicated compound-assignment arms
UpdateFantasySealBlinkDeathbombdemonstrates that a class-valued local declared later in a function can still take a shallower physical stack slot than earlier scalar locals unless it is included in the function-level#pragma var_order. The first exact-sized build putpreviousPositionat-0x0Cand shiftedi/bomb/workItem/angledown by 12 bytes. AddingpreviousPositionafter those four names restores targeti=-4,bomb=-8,workItem=-0x0C,angle=-0x10, andpreviousPosition=-0x1Cwithout moving its constructor call.- Do not merge branch-owned compound assignments into a ternary merely because both arms update the same lvalue. In the same callback, target
if (i & 1) motionStep += 1.2f; else motionStep += 2.4f;keeps two independent load/add/store sequences.motionStep += (i & 1) ? 1.2f : 2.4fshares the destination update and makes VC7/Odseven bytes short. Restoring the lexical arms changes the emitted extent from0x6F7to the exact0x6FE. - A small target data table referenced only by one callback is still a real relocation owner. The seven packed Dream Seal colors at
0x004C6100are represented asg_PlayerDreamSealColors[7]and recorded inreccmp-globals.csv; do not compensate its relocation by inventing a nearby spell-card-table addend.
Sibling timer-state callbacks and block-local class ordering
UpdateArtfulSacrificeBombandUpdateReturnInanimatenessDeathbombshare one source-shaped 60-frame quadratic interpolation: a branch-localFloat3 target(192,224,0),interp = (float)timer / 60,interp *= interp, then(target - savedAnchor) * interp + savedAnchor. Keeping the class local inside the<60branch naturally leaves the threeFloat3operator return buffers compiler-owned and produces the target0x58/0x64frames.- Mutually exclusive event arms can intentionally allocate distinct otherwise-unused effect pointers. The 100/110/120/130 and 130/140/150/160 lanes store
SpawnEffectreturns to separate stack slots even though the values are never consumed. Preserve those branch-local declarations instead of deleting dead-looking locals or sharing one pointer across the event chain. - When a block has both scalar pointers and a later-constructed class local, list all of them in the block
#pragma var_orderif target slots require it. In the 120-frameUpdateReturnInanimatenessDeathbombburst,(effect, damageSlot, burstPosition)yieldseffect=-0x1C,damageSlot=-0x20,burstPosition=-0x2C..-0x24while theFloat3constructor still executes only after the first three effect spawns. Listing only the pointers made the later class local steal the shallow slots. PlayerOptionState +0x2C8is a target-observed state dword: both sibling callbacks set it to the initializing state at their terminal event before assigning the row timer to zero. The later semantic pass confirms the same inactive/initializing/active/exiting protocol across every option callback and names itlifecycleState; its exact offset/width and the complete state transitions are both proven.
Fixed-size zeroing and mixed block-local aggregate ordering
UpdateFinalSparkDeathbombdistinguishes three explicit zero component assignments frommemseteven for a 12-byteFloat3. VC7/Oilowersmemset(&v, 0, 12)toxor eax,eaxplus three stores, while the shipped target has three independent immediate-zero stores. Writingv.x = v.y = v.z = 0.0fas separate statements restores exactly ten authored bytes without padding.- One block can require physical stack order different from declaration/constructor order across mixed aggregate types. The periodic effect block is exact with
#pragma var_order(effect, position1, position0, scale1, scale0): the pointer occupies-0x10, the second-declaredFloat3occupies-0x1C, the first-declaredFloat3occupies-0x28, then twoFloat2s occupy-0x30/-0x38, while VC7 still executes the twoFloat3constructors in lexical declaration order. - The collision tail in the same function independently requires
#pragma var_order(slot, position), giving the scalar collision pointer-0x3Cand the later class-valued position-0x48. Block-local ordering is preferable to moving declarations when constructor timing already matches the target.
Symmetric multi-option bomb templates and lexical sibling differences
UpdateRedNightlessCastleBombandUpdateScarletDevilDeathbombare two 0x73D-byte four-option callbacks whose 0xE4 frames come naturally from four lexicalFloat3interpolation expressions, not a loop. Each expression owns three compiler return buffers; replacing the four source copies with an indexed loop would destroy both the frame and call-site layout.- In the
<60interpolation block, the source-visible scalar must own the shallow slot even though a non-trivialFloat3is declared after it.#pragma var_order(interp, position)gives targetinterp=-0x0Candposition=-0x18..-0x10while preserving the constructor after the timer calculation. - Equal-size sibling functions can still differ in target-visible lexical placement. The normal Remilia Bomb fills all four option targets before creating its player-centered cancel circle; the Last Spell declares the second-phase
Float3, creates the cancel circle, and only then copies the player position into that local. Preserve these separate bodies instead of factoring a shared helper. - The two siblings also preserve gameplay constants directly in codegen: movement scale
2.0fversus3.0f, periodic interpolation end-scale64.0fversus128.0f, and terminal events at frames239versus279. Treat a matched template as a source-shape scaffold, not permission to normalize sibling constants or statement order.
Parent-vs-subobject ownership and event-loop branch topology
UpdateKillingDollBombshows that the samePlayerBombStatedata may deliberately use different source owners in adjacent phases. Initialization formsworkItem = bomb->workItemswith an 8-bit+0x4C, while the knife-spawn phase formsworkItem = player->bombState.workItemswith target 32-bit+0x1028. Its per-knife event test likewise reloadsplayer->bombState.timerthrough+0xFF4instead of the cachedbomb + 0x18. Preserve the target owner even when both expressions name the same address.- A sparse event loop whose body is large is target-shaped as
if (!justReached) continue; if (active) return; body. VC7 emits a short conditional plus short jump to the loop update. Wrapping the body inif (justReached) { ... }emits a six-byte near branch over the large body and changes the function extent. - The knife movement phase is physically laid out as the movement path first and the pure-rotation path later. Writing
if (timer < 30 || timer >= 70) { movement } else { rotation-only }reproduces target branch placement; the equivalent positive conjunction30 <= timer && timer < 70places the rotation body first and changes near/short branch ownership. - Sibling knife callbacks preserve source-owner differences as well as constants. The normal Bomb hit effect intentionally reindexes
bomb->workItems[i].position, producing animul i,0x16F0, while the Last Spell uses cachedworkItem->position. Do not normalize equivalent member addresses across sibling bodies.
Small effect easing callbacks: preserve staged x87 updates
- The Player-owned effect callbacks at
0x40E040,0x40E120,0x40E200, and0x40E2D0share a source-shaped easing sequenceinterp = 1 - timer/40; interp *= interp; interp = 1 - interp. Keeping those assignments separate reproduces the target'sfst/fmul/fstpsequence and the two-dword frame (interp=-4, fastcall receiver home-8). UpdateExpandingOrthogonalRadialTrailextends the same rule to a quartic curve: after normalizing(timer - 30) / 30, two lexicalinterp *= interpstatements intentionally producet^2and thent^4, with a target-visible store between them. Replacing this withpowf,interp*interp*interp*interp, or algebraic folding changes the x87 spill pattern.- A valid shipped function boundary does not require IDA to have created a function object. The four callbacks at
0x40E040..0x40E2D0each have independent prologue/return pairs and CC padding in the canonical executable even though the current IDA database fails to decompile0x40E040. Boundary acceptance comes from shipped bytes plus exact COFF comparison, not database function metadata alone.
Player option-state ABI and compiler-owned switch temporaries
- The option-update family proves the
PlayerOptionStatetail layout:lifecycleState @ +0x2C8, homingbehaviorState @ +0x2CC,optionIndex @ +0x2D0,orbitAngle @ +0x2D8,timer @ +0x2E0, update callback+0x2EC, and render callback+0x2F0.Player +0xE2ABCis the currentEnemy*option homing target; the already-exact homing helpers independently dereference itsEnemy +0x2D88position and gate it withshotTimer. - Do not introduce named locals for a switch expression merely because the decompiler names one.
UpdateHomingOption @ 0x0044E3A0is exact only when both switches readoption->lifecycleState/option->behaviorStatedirectly. VC7 then materializes its own-0x0C/-0x10switch temporaries. Namedstateandsubstatelocals caused a second copy of each, enlarged the frame from0x10to0x18, and made the authored body 12 bytes longer before other differences. - A nested switch default that reaches the shared function epilogue should remain
break, notreturn 0. InFUN_0044E3A0,default: return 0emitted a localxor eax,eaxplus jump;default: breaklets all paths share the final zero return and completes the exact 17-byte correction. FUN_0044E3A0has a 0x3B5 authored body followed by one 16-byte four-entry compiler jump table (compare_size=0x3C5).FUN_0044EB70has a 0x2DB authored body followed by two independent 16-byte tables (compare_size=0x2FB) because the original source contains two lexicalswitch(optionIndex)statements. Verify all table relocations but count only authored code.- Fallthrough is target-visible in option state machines.
FUN_0044EA40state 1 initializes script/state and intentionally falls through into state 2 positioning;FUN_0044EB70state 1 initializes its per-slot target/angle and falls through into the state 2 orbit update. Avoid duplicating the shared body or inserting a helper call.
Effect trail geometry: transposed UV strips and constructor timing
- The effect-trail family at
0x4272e0..0x427b50gives a consistent target-observed tail layout for the 0x360-byteEffect:+0x314/+0x318/+0x320feed the primary radius/angle/strip-width geometry,+0x324is the segment count,+0x32c/+0x330/+0x334select and parameterize ellipse/phase modes,+0x34cis the custom draw callback,+0x356is the geometry-dirty byte, and+0x358owns the allocated textured-vertex buffer. These names describe the proven trail mode; do not assume every effect type gives the overlapping storage the same semantics. AnmManager::InitializeHorizontalTextureStripandInitializeVerticalTextureStripare source-shaped siblings. Both use two lexical odd/evenforwalks over 0x1c-byte textured vertices;0x4649a0decrements U while holding V at the sprite's start/end edges, whereas0x464b00decrements V while holding U at the two edges. Preserve theforspelling: in this VC7/Odfamily, replacing the walks with equivalentwhileloops changes the entry trampoline bytes.#pragma var_orderfixes physical slots but not the lifetime point of a non-trivial local. InDrawRadialTrail, the dead branch-localFloat3belongs at-0x4c, but the target constructs it only after storing the two phase angles and computing the phase step. Declaring it with the scalar locals kept the same frame and slot yet moved the constructor call earlier; moving only the lexical declaration point removed the final 49 byte differences.
Effect camera-relative initializers and canonical aggregate owners
- Target absolute references inside Effect callbacks should be reconciled against nearby aggregate bases before inventing globals. The camera vectors at
0x4EA3C4,0x4EA3D0, and0x4EA3E8are exactlyg_Background + 0x6394/+0x63a0/+0x63b8, i.e.g_Background.unk6394.vectors[0/1/3]. Expressing them through the realBackgroundaggregate makes the COFF relocation targetg_Backgroundwith the corresponding addend and reproduces the shipped bytes in the0x426280,0x426720, and0x426e70random initializers. - The small timer-driven Effect updates at
0x426bb0,0x426c90, and0x4271a0naturally produce their target 0x20/0x2c frames from class-valued arithmetic temporaries. Keep expressions such asvector6 * alpha + vector5andvector6 * alpha * 128.0f + vector5intact instead of naming the intermediateFloat3s; explicit source temporaries move the compiler-owned hidden return buffers. InitializeDirectionalOffsetconfirms that a trailing addend can be target-visible even in random scalar setup: the shipped instruction references0x004B4338, whose bytes are single-precision1.0f, so the exact expression isGetRandomF32InRange(1.5f) + 1.0fbeforeFloat3::operator*=. Comparing only the relocated instruction field had previously hidden the stale+ 0.0fsource. Validate the destination data as well as the relocation shape before calling a floating expression exact.
Tiny accessors: avoid convenience pointer homes
PrepareSpellcardForTimerCallback @ 0x42bc50is exact only when its three stores operate directly from the typedSpellcard *fastcall receiver. Caching a separate byte-pointer alias in a local enlarges the target 0x32-byte helper to 0x3a by adding an extra stack home. For tiny/Odaccessors and bit-manipulation helpers, start from repeated direct receiver expressions and introduce a pointer local only if the shipped frame proves one exists.EclManager::GetTimelineCount/GetTimeline @ 0x42dfb0/0x42dfd0independently prove the ECL header word at+0x06is the timeline count and the relocated timeline pointer table begins at+0x08. Keep the underlying header layout stable for claimed ECL interpreter work; a semantic accessor can expose the proven meaning without forcing an immediate shared-field rename.
Enemy contact and motion branch ownership
Enemy::CheckPlayerCollision @ 0x42c290uses one source-visibleFloat3 collisionSizeplus two compiler-owned return buffers forsize / 0.7fandsize / 1.5f. The route/attachment gate is not byte-equivalent as one OR expression: the target callsHasAttachedEnemy()only for route ids 0/4 and uses a shortjeinto the collision body followed by a near jump to the epilogue when an attached enemy exists. Preserve that nested early-return ownership.EnemyManager::UpdateSubrankpreserves the arithmetic groupingGetLives() * 4 * 60, which VC7 lowers toshl eax,2; imul eax,eax,0x3c; replacing it with* 240changes the target instruction shape. Its dialog gate likewise matches as one outerif (!IsDialogPresent())block rather than an explicit early return.Enemy::IntegrateVelocityplaces the normal X integration block before the mirrored-X block: spell bit 18 is tested asif (bit == 0) add; else subtract. Reversing the lexical arms leaves semantics unchanged but swaps the targetjneand the physicalfadd/fsubropcodes.
Enemy phase transitions: shared return blocks and tiny-member TU ownership
Enemy::HandleTimerCallback @ 0x42b930only reproduces its 0x31c extent when the timeout test owns the whole success body:if (timer >= timeout) { ... return 1; } return 0;. An equivalent explicit earlyreturn 0generated a local zero-return/trampoline and made the function one byte short. The shipped function uses one six-byte nearjefrom the timer gate directly to the shared final zero-return block.- A tiny member's translation-unit owner can be visible in its epilogue.
Gui::FUN_0042f340 @ 0x42f340compiled in the/OsGui TU as a 0x14-byte function ending inleave; ret 4; the shipped 0x16-byte body ends inmov esp,ebp; pop ebp; ret 4. Defining the same ordinary C++ member in the neighboring/OdEnemyManager TU naturally emitted the exact body. Use address neighborhood and compiler epilogue shape as TU evidence before trying padding or assembly. g_EnemyManagerUpdatePlayerTimer @ 0x018B89ECis a standard productionZunTimer. Phase-change code inHandleLifeCallback/0042b930writes it directly when forcing the player transition; promote the storage from probe-only knowledge to the production EnemyManager owner and record the global in the canonical ledger rather than hard-coding the address.
Replay callbacks: hidden new temporaries, legacy arithmetic, and caller-proven ABI
ReplayManager::BeginRecordingStage @ 0x452830shows that an explicit source local aroundnew Tcan duplicate VC7's compiler-owned new-expression temporary. Writingg_ZunMemory.AddToRegistry(new ReplayData, sizeof(ReplayData), "ReplayDataInf")directly gives the target0x24frame; cachingnew ReplayDatain a named local adds onemov local,local, expands the frame to0x28, and makes the function six bytes long. Preserve the direct allocation expression when the target already exhibits the hidden temporary.ReplayManager::BeginPlaybackStage @ 0x452D60preserves an adjacent-generation replay leftover rather than simplified arithmetic: the replay shot byte is assigned through/ 1,% 1, and then overwritten by the original byte. Under VC7/Od, signed% 1deliberately emits the targetand 0x80000000 / dec / or -1 / incsequence. Algebraically removing the first two stores destroys target-visible source history. TH06/TH07 can identify this family, but TH08 bytes remain the acceptance authority.- A caller can prove a wider ABI than the callee body alone.
GameManager::SetClockTime @ 0x453C60stores only the low byte, so its 25-byte body is identical for a narrow ori32parameter. The exact replay caller at0x452FDBfirst sign-extends the storedi8clock byte withmovsxand pushes the resultingint; declaringSetClockTime(i32)makes both caller and callee exact. Prefer caller+callee evidence over inferring parameter width from the callee's final store. - The extended replay stream at
ReplayManager +0x78is independently constrained as a six-byte record:BeginRecordingStagealiases it to the input stream, writes words at+0/+2/+4, and playback advances it by six bytes. Keep this layout typed as a three-word record even when a remaining callback has a register-allocation mismatch. A one-byte difference caused solely byand r32,imm32versus accumulator-formand eax,imm32is not permission to add padding or assembly; preserve the semantic reconstruction and continue compiler-shape inversion. ReplayManager::PlaybackExtendedInputAndFps @ 0x004526C0is the bounded counterexample to treating a one-byte extent miss as a boundary error. The target is 0x169 bytes and uses the register phaseEAX/ECX/EDXafter the six-byte stream advance; the natural/Odobject is 0x16A bytes and usesEDX/EAX/ECX. That phase makes the target% 8lowering use the five-byte accumulatorand eax,0x80000007, while the object uses the six-byte genericand edx,0x80000007. Separate versus combined postincrement, struct versus raw-word versus pointer-to-three-word-array ownership,++versus+= 1, byte arithmetic, index syntax, C versus C++ casts, local type/declaration placement, and#pragma var_order(unused)all reproduce the same 0x16A object. Do not repeat that syntax matrix; require a new source-shape or TU hypothesis that explains the allocator phase before probing again.Gui::FUN_00437dc7 @ 0x437DC7provesGuiMsgVm +0x1568is a one-byte state consumed by replay frame throttling. Promote the byte inside the realGuiMsgVmaggregate instead of hard-codingg_Gui.impl + 0x1568; the tiny helper is exact in the/OsGui TU and gives later replay code a canonical owner.
Gui added-callback: preserve table rank, branch-local validation, and real receiver owners
Gui::ActualAddedCallback @ 0x4390EEproves three resource tables as real relocation owners: the 12-entry loading portrait table at0x004C72C4, the 9-entry stage-text ANM table at0x004C745C, and the 9x12 message-path table at0x004C74C0. Preserve the message table as a true two-dimensional array. Flattening[stage][shot]intostage * 12 + shotchanges VC7/Osfrom the targetimul stage,0x30plus scaled column index into animul stage,0x0c/add sequence even though the selected pointer is identical.- Do not hoist identical validation out of mutually exclusive resource-load branches when the target duplicates it. The stage-text normal and spell-practice paths each assign
stageTextAnm, immediately test that branch's result for NULL, and returnZUN_ERRORlocally. Sharing one NULL check after the two assignments made the function 17 bytes short; restoring the two lexical validation blocks reproduced the target branch extents. - Receiver ownership is target-visible even for the same object. The common stage-background disable helper is called as
g_Gui.FUN_004390d6(), which emits the target five-byte immediate ECX load;this->FUN_004390d6()uses the existing stack home and makes the caller two bytes short. Likewise thevm34d4setup belongs totimesAnm @ Gui+0x14, notstageTextAnm @ +0x10; the two exact call sites distinguish those semantic owners. - Recover anonymous
AnmVmstate through already-exact accessors before naming it in a large loop.AnmVm::GetIntVarprovescounterVar0 @ +0x120; using that field in the 14x12vm5728grid reproduces target writes atGuiImpl + 0x5848.intVar0 @ +0x100is address-equivalent only to a different member and produces the wrong offset. - In this
/OsGui function,#pragma var_order(i, j, k)yields the target physical homesi=-0x4,j=-0x8,k=-0xC. The grid parity is lexical bit arithmetic((i + j) & 1) + 3;% 2introduces signed division machinery and changes the body despite identical non-negative loop values.
Enemy timeline/spawn: split non-trivial lifetime from scalar slots
EclTimeline::Run @ 0x0042A8A0is a useful VC7/Odexample where constructor timing and physical local ownership must be solved separately. The fiveD3DXVECTOR3positions are branch-local non-trivial objects: declaring them at function scope hoists all five default constructors into the prologue, while the target constructs each vector only inside the opcode arm that uses it. Keep those objects branch-local even when related scalar locals need longer lifetimes.#pragma var_orderis not a reliable way to place names whose lifetime exists only inside a case block. Natural block lifetime, nested scopes, andfor-initializer locals controlled the target homes more reliably. The random-play arm needed the argument cursor outside a nested vector scope, which producedargs=-0x4Cand the vector at-0x58without padding.- When two related branch locals need a fixed adjacent layout, a real POD work aggregate can be the source shape rather than two independent locals. The special-spawn arm only matched when
{ Enemy *spawned; i32 *args; }was one 8-byte local aggregate, giving target homesspawned=-0x2C,args=-0x28while the vector remained branch-local. - Lexically equivalent loop conditions can change the authored body boundary. Timeline opcode 14 needs
if (slot >= 0) continue; slot = value;; rewriting it asif (slot < 0) slot = value;removes a short branch and makes the body two bytes short. In this function those two bytes are exactly what place the 17-entry switch table at target0x0042AD60. - Treat a compiler switch table as associated data when the target authored function ends before it.
EclTimeline::Runhas a0x4C0authored body and an immediately following0x44table with 17 relocations. Canonical validation usessize = 0x4C0pluscompare_size = 0x504: all table bytes/relocations are verified, but the table is excluded from authored-byte progress.
Enemy spawn: typed aggregate copies and true bitfield RMW
EnemyManager::SpawnEnemy1/SpawnEnemy2 @ 0x0042A4E0/0x0042A680prove that largememcpycalls can have the right semantics but the wrong VC7 register ownership. Assigning a typed aggregate ofsizeof(Enemy)reproduces the targetmov edi(dst); mov esi(src); mov ecx,count; rep movsd. The0x78ECL-context copy in SpawnEnemy2 is a second shape: typed aggregate assignment produces targetmov esi(src); mov edi(dst); add edi,0x810; mov ecx,0x1E; rep movsd.- Enemy
+0x3324writes are genuine bitfield read/modify/write operations. Modeling the active bit and spawn-variant bit as one-bit fields reproduces target accumulator/register ownership; hand-written masks and ORs were address/semantic equivalent but non-exact. - Do not force a reconstructed dependency into the production link before its owner closure is ready. Spawn/timeline are canonical in
build/probes/EnemyTimeline.objwhileEclManager::RunEclstill has probe-only dependencies. The normal executable remains link-clean; the exact probe holds the real source implementation, not a forwarding shim. Promote the dependency group together once the remaining ECL owners are reconstructed.
Exact inventory: overloaded authored names are range identities
reccmp-functions.csvcan legitimately contain more than one authored function with the same logical name. TH08 currently has bothFloat3::Float3 @ 0x00404720andFloat3::Float3 @ 0x0040B460. Exact evidence is an address/extent claim, not a globally unique-name claim. Progress/tracking code must therefore bind an exact row to the authored inventory by target address (and size), then validate the logical name at that address; a dictionary keyed only bynamesilently picks one overload and rejects the other.- Small
__fastcallECL adapters are also source-shape sensitive to argument caching.StartEnemySpell @ 0x00421280has only the two incoming ECX/EDX homes. Cachinginstructionin a third local expands the frame from target 8 to 12 and adds exactly 7 bytes. Re-read the fastcall argument directly when the target repeatedly materializes the same parameter home.
ECL tail motion/shoot helpers: lexical lifetime and guard-return topology
Enemy::UpdateMovementproves that two observable default constructors do not imply overlapping class-local lifetimes. VC7/Odassigns the target slots only when the ctor-only legacyFloat3lives in its own completed block and the used polar-velocityFloat3is declared afterward. Hoisting both into one scope swaps their stack slots; wrapping them in a synthetic aggregate creates a new constructor and is not source-equivalent. Prefer sequential non-overlapping lexical scopes over padding or fake fields when the target proves both ctor calls.- When a conditional expression feeds a narrow destination, preserve the conditional's natural wider type until the final store. In
EclRunHigh::DispatchShotInstruction, casting bothResolveIntarms toi16made VC7 allocate word temporaries. Leaving both arms asi32lets the compiler merge them in the target dword slots and only then emit the low-word descriptor store. - Multiple guard conditions can have target-visible return sharing. The shot helper is exact as one
if (rejectHuman || rejectYoukai) return;followed by one independent radius guard. Splitting the two flag rejects into separate returns duplicates a five-byte epilogue jump; spelling the inverse as one large positiveifinstead creates long conditional branches. Preserve the original guard grouping when the target has one local reject trampoline. - Float guard polarity is ABI-visible through x87 status masks.
if (radius > 0.0f && distanceSquared < radius) return;emits the targettest ah,0x41/test ah,0x05pattern and also preserves unordered/NaN behavior. Rewriting the condition as orderedradius <= 0 || distance >= radiuschanges the condition-code sequence even though normal finite inputs look equivalent. - TH06/TH07 ECL sources are useful lexical ancestors, not byte evidence. The TH06 movement-mode block and TH07
SpawnBulletVariantexposed the original field/update order for the TH08 helpers, while TH08 target disassembly remained authoritative for new flags, offsets, branch guards, and exact acceptance.
ECL boundary/interpolation helpers: x87 operand ownership and loop-exit topology
- For VC7
/Odfloating comparisons that involve a conversion/operator call, algebraically equivalent operand reversal is not codegen-equivalent.position.x < bound + marginkeeps theFloat3::operator float*()call first and compares the returned member directly;bound + margin > position.xmakes the compiler spill the x87 value to a temporary. Preserve the target-observed lexical left/right ownership instead of normalizing inequalities. - A typed member expression can matter independently of the comparison operator. In the periodic-X move helper,
playerX < enemy->vector2d34.xproduced the targetfld player / fcomp enemy; the same condition through a rawu8* + 0x2D34load was canonicalized to the opposite x87 owner. Prefer proven struct members when their AST shape explains register/FPU ownership. - For direct-vs-wrapped distance selection, preserve the subtraction and comparison order (
directDistance < wrappedDistance). Rewriting it as the equivalentwrappedDistance <= directDistancechanges the x87 stack order and status-mask branch sequence even with identical finite semantics. - Large/small branch polarity is byte-visible. The TH08 boundary helpers require
if (duration <= 0) { immediate body } else { timed helper }: this leaves the large immediate path as fallthrough and places the short timed call at the tail. Reversing the condition changes short/near jump selection. - Loop bodies with an install-and-exit path may require rejection-first spelling.
InstallInterpolationSlotmatches only asif (occupied && affected != raw) continue; ... install ...; break;. The positive formif (empty || affected == raw) { ...; return; }emits a long reject branch plus an explicit epilogue jump and grows the function by 9 bytes. - The eight-entry interpolation callback table is target-owned data at
0x004C6C90, immediately beforeg_EclExInsn @ 0x004C6CB0. Keep these as distinct owners; the interpolation installer indexes the former by slot callback index.
ECL compare/context/ANM helpers: switch label placement, repeated reads, and aggregate owners
- A switch's shared labels may need to live lexically inside the switch.
CompareOperandsis exact only when the shared success block appears after the last case but beforedefault, withdefaultand the failure label co-located afterward. Moving those labels outside the switch makes VC7 synthesize a two-byte default trampoline; turning default into a direct return creates a separate NULL-return block. Source label placement can therefore change the switch bounds-check target even when all case bodies are identical. - For a large repeated compare dispatcher, do not introduce
lhs,rhs, or result locals unless the target proves them. The 12CompareOperandscases deliberately spell their operand ternaries independently; VC7 allocates 24 compiler-owned conditional temporaries and produces the target 0x6C frame. Collapsing the logic into reusable source locals dramatically shrinks and changes the function. - Preserve repeated operand resolution when the target repeats calls.
ApplyInterpolationOperationintentionally resolves operand 2 twice: once fordelta = op1-op2and once fordelta*op3+op2. Caching the second value is semantically tempting but changes call count, temporary ownership, and exact codegen. - Typed aggregate assignment can also recover
rep movsdevaluation order when the source is a true global.CallSubOnEnemy's 0x20 parameter copy matches only when0x004ECE20is represented as the global aggregateg_EclCallParameters; casting the absolute address as a pointer makes VC7 load ESI before the destination, while the global aggregate form emits the target destination/count/source order. Treat target-resident copy sources as semantic globals when relocation evidence supports it. - The 0x228
EnemyEclContextsave/restore paths should use typed aggregate assignment, notmemcpyor assembly. This naturally emits the targetrep movsdand preserves the distinct source/destination evaluation order for call-stack push versus pop. - In extra-ANM cleanup, distinguish adjacent narrow VM fields from actual offsets. The negative sub-ANM path clears
AnmVm::scriptIndex @ +0x21A, notactiveSpriteIndex @ +0x214; the six-byte difference is visible as enemy+0x4CAversus+0x4C4despite the same VM base and stride.
ECL child-spawn sentinels and bitfield setters
- Prefer a real typed owner even when the target materializes a large absolute address. The child-spawn failure sentinel
0x00F4F8F0is exactly&g_EnemyManager.enemies[480], and the failure flag0x00F54E18isg_EnemyManager + 0x9DCEF8. Expressing those throughg_EnemyManagerproduces the correct DIR32 symbol plus large addend and avoids inventing standalone globals. - Closely related child-spawn helpers may share the exact lexical skeleton. TH08 standard and alternate child spawn differ only by
position += parent+0x2D88; keeping the same guard, right-to-left argument expressions, and sentinel result preserves identical compiler-temp placement aroundSpawnEnemy2. - If the target computes
(value & 1) << bitfirst, then loads an existing flag word, masks it, and performsor old,new, use a genuine bitfield assignment before hand-writing an RMW expression. The Spellcard ECL bit6/bit11 setters match exactly as bitfield stores; equivalent mask/or expressions changed the OR destination register even at identical size. - Header edits can require an explicit PCH rebuild when the edited type is included indirectly by
th_pch.h.Spellcard.hppis pulled into the PCH throughGameManager.hpp; after adding member declarations, remove/rebuild the PCH artifact before interpreting "not a member" diagnostics as source errors.
ECL direct-call closure: TU ownership and bullet/laser field recovery
- A four-byte near-match can be a translation-unit/profile error rather than an expression problem.
Gui::StartStageBackgroundSequence @ 0x00439007emitted 77 bytes in the/OdECL dependency probe but the exact 73-byte target as soon as the same semantic body moved to the real/OsGui.cppowner. The target then naturally reused EAX forimpl->vm2156cand used the compactleaveepilogue. Recover the original TU before tuning registers. BulletManager::RemoveBulletsInRadius @ 0x00430D30proves two source-visible ordering details: materialize&g_BulletManager.bullets[0]before declaring the non-trivialFloat3 delta, and spell the distance rejection asif (LengthSq(delta) > radiusSquared) continue;. The latter produces the target short false-branch plus explicit loop jump and preserves unordered x87 behavior.BulletManager::SpawnLaserPattern @ 0x00430F20retains the TH06 laser-spawn skeleton but with TH08 layouts. The target provesLaserruntime fields at+0x554..+0x599and nine laser-specificBulletSpawnDescriptorfields at+0x1D0..+0x1F0; promoting those fields into the real aggregates lets VC7 emit the complete 639-byte body exactly with#pragma var_order(i, laser, this).- When auditing a large dispatcher closure, compare direct
REL32destinations against the exact ledger after each dependency batch. Once the TH08 RunEcl authored helpers were recovered, its only non-exact direct callees were the math-library wrappersfmodf,sinf,cosf, andsqrtf; keep those out of authored reconstruction work until authored coverage reaches 100%.
SDK/header-inline COMDAT ownership
When the target contains a standalone body for an SDK/header-inline helper, do not copy the SDK implementation into repository source just to manufacture a symbol. Prefer a real reconstructed caller TU that naturally emits the COMDAT under the target VC7 flags, then compare that emitted body and every relocation normally. D3DXVec3Length, D3DXVec3Dot, and D3DXVec3Cross at 0x0040B4C0/0x0040B540/0x0040B7F0 are the corpus example: Background.cpp already uses those SDK inline helpers, so the exact functions are reproducibly emitted without changing their SDK source.
Non-trivial member arrays versus repeated members
For VC7 /Od, an array of non-trivial class members and several individually declared members are not constructor-codegen equivalent. Float3 vectors[6] in BackgroundUnkVectors emitted the vector-constructor iterator ??_H, while the TH08 target constructor at 0x004073B0 contains six direct Float3::Float3() calls at offsets 0x00..0x3C. When every observed use is a constant index, repeated direct ctor calls are strong evidence that the original layout used individual members. Promote the fields individually, preserve their offsets, and regress every accepted consumer before accepting the ABI change.
Effect callback temporaries and tracking expressions
The 0x004264F0/0x00426990/0x00426D70 effect callbacks share a useful VC7 /Od pattern. Perform the persistent vector updates first, then declare the non-trivial Float3 delta; moving that declaration to function entry runs its constructor too early and expands/shifts the frame. The exact local layout is reproduced by #pragma var_order(delta, dot, effect). Do not cache aliases for the tracked enemy, AnmVm::pos2, or a background color: repeated direct owner expressions preserve the target's rematerialization and hidden Float3 return slots. For x87 comparisons, lexical polarity remains significant: if (z >= 0.0f) return 0; return 1; produces the target test ah,1 / jne layout, while the negated equivalent reverses the physical return blocks.
Gui/Enemy cleanup corpus: normalized switches, temporary addresses, and early exits
- Reconcile the numeric enum before tuning a dense switch.
Gui::FUN_00438046 @ 0x00438046uses the zero-based TH08Stageenum; the target normalizes withdec currentStage, soSTAGE1is the out-of-range/default path and the table covers onlySTAGE2..EXTRASTAGE. The exact/Oslayout also requiresdefault:lexically before the first case: placing it last forces a near bounds branch and moves the eight-entry table by one byte. Validate the authored0x205body together with the associated0x20table viacompare_size = 0x225, but count only the body as authored bytes. - Old MSVC can expose a constructor return directly through an address-of temporary.
Enemy::Despawn @ 0x0042BCF0matches the target marker call as&Float3(-999.0f, -999.0f, 0.0f): VC7 constructs the temporary in the branch-local stack slot and immediately pushes the constructor's returned EAX. Declaring a named local and then passing&localinserts a three-byteleaand is not exact. Use this only when the target proves the temporary lifetime/call pattern; never emulate it with padding or asm. - Semantically equivalent body guarding can differ by exactly two bytes.
Enemy::ApplyDamageToParentneedsif (damage == 0) return;so VC7 emitsjne body; jmp epilogue; wrapping the body inif (damage != 0)emits a directje epilogueand shortens the function. Preserve the target's lexical early-exit topology. - Prefer real aggregate/global owners for target absolute storage. The cleanup path promotes
0x018B89B4to the productiong_EnemyTrackedEnemyowner, while the existing ECL enemy table remains its shared production storage. Probe-only aliases may keep their provisional decorated names, but normal code should have one real storage owner rather than a forwarding or duplicate shim. - A typed array/member access can encode useful relocation addends without inventing globals. Supervisor loading-VM cleanup references the three
pendingInterruptmembers throughg_SupervisorLoadingVms[], naturally producing one base symbol with the target addends. Keep the aggregate owner when the shipped code proves repeated fixed-stride members.
ECL interpolation callbacks: named x87 homes and canonical COFF ownership
- A source local can exist only to force a debug-build x87 home while the value remains live in ST0.
InterpolateLinear @ 0x00421120needs separatestartandendlocals with#pragma var_order(end, start). VC7 emitsfst [end](notfstp) after the secondResolveFloat, then immediately computes(end-start)*t+startfrom the still-live x87 value. Inlining the second resolver into the expression removes theendhome, shrinks the frame from0x14to0x10, and makes the function three bytes short. - Do not weaken canonical comparison just because a TU-local
staticfunction lacks a standard COFF function-definition aux record.GetAnmFormat @ 0x00465510already emitted exact bytes, but its internal-linkage symbol could not be consumed bycompare-function.py. Promoting the existing implementation to its namespace-level real owner preserved all 83 target bytes while giving the COFF symbol a reproducible function extent. Prefer a real source owner over special-casing the comparator. - SDK inline COMDATs may have multiple natural callers.
D3DXVec3LengthSq @ 0x0040B500is emitted byte-identically by both EffectManager and Background; one stable production caller object is sufficient as the canonical evidence owner, while the SDK implementation itself remains untouched.
Compiler-generated auxless COMDATs: strict canonical ownership
VC7 does not attach a normal function-definition auxiliary record to several compiler-generated bodies, notably scalar/vector deleting destructors and ??_H vector-constructor iterators. Their source ownership is nevertheless reproducible: the compiler emits an isolated .text COMDAT from a real class/destructor or array-member construction in a production TU.
compare-function.py therefore has an explicit per-unit allow_auxless_comdat = true mode. It is intentionally stricter than ordinary symbol lookup: the target symbol must be section-defined at offset zero, the section must be both code and COMDAT, exactly one external offset-zero function symbol may own the section, and the entire section size must equal the manifest comparison extent. Undefined references with the same decorated name are ignored when selecting the section-defined owner. Relocation multiset validation and replay remain unchanged. Units without the explicit flag still reject auxless symbols.
This corpus attests the natural VC7 emissions for ResultScreen/AnmManager/MidiOutput/ChainElem/Pbg/zwave deleting destructors, PbgArchiveEntry's vector deleting destructor, DummyMidiTimer's implicit destructor, and the 0x00406850 vector-constructor iterator without adding handwritten destructor shims. AnmVmBase::AnmVmBase @ 0x004067C0 is different: declaring/defining the real empty base constructor gives VC7 a normal function aux record while preserving the exact member-construction body, so it stays on the normal comparator path.
ECL EX callback lifetime and branch-owner patterns
FUN_00423A60 @ 0x00423A60demonstrates that a plain local with a declaration initializer can intentionally execute before a later non-trivial local constructor. Writingu8 *bullet = ...;before declaring theFloat3work local makes VC7 materialize the bullet-pool cursor before theFloat3constructor; declaringbulletwithout an initializer and assigning it in the body lets VC7 hoist theFloat3constructor ahead of that assignment even though the later code is semantically equivalent.- For short-circuit conditions over already-spilled locals, preserve lexical operand order. The target zone transition is
currentZone == 0 || previousZone == 0; reversing those operands keeps semantics but swaps the two stack-home compares and misses by two instruction bytes under VC7/Od. - Repeated ECL EX setup callbacks at
0x00423530,0x00423DB0, and0x00424170deliberately repeat two effect spawns, one ANM script assignment, and a callback-global publish. Keep the duplicated lexical template instead of abstracting it away; all three naturally emit the same 0x68-byte shape with only constants changed.
ECL EX barrier render aggregate and callback ABI
- The barrier render globals around
0x004E4B60are one aggregate, not unrelated absolute variables:mode @ +0x00,AnmVm vm0 @ +0x08, andAnmVm vm1 @ +0x2AC. The0x2A4spacing exactly matchessizeof(AnmVm). Naming the aggregate lets VC7 emit one base DIR32 relocation plus natural member addends for scale, color, rotation, and position while retaining the target bytes. FUN_004235A0 @ 0x004235A0is a true no-argument fastcall callback. Its target prologue has no ECX/EDX homes and no argument reads. Earlier setup helpers can still publish its address through a DIR32 relocation; correcting the declaration changes only the COFF relocation symbol spelling, not their target bytes.- A real
VertexDiffuseXyzrhw vertices[10]local is required for the barrier strip. Its non-trivial constructor naturally emits VC7'seh_vector_constructor_iterator; replacing it with raw storage/memset loses both the helper call and the exact 0xE8 frame shape. - Preserve even apparently dead authored locals when target evidence demands them.
FUN_004235A0stores&barrierState.vm0into a stack local that is never read afterward; retaining that unused pointer is necessary for the exact local layout and source chronology. - For a large render callback, existing exact D3D idioms are reusable compiler fingerprints. The barrier function uses the same
SetTextureStageStatesequence as Gui's untextured diffuse strip, then restores ANM render state throughClearVertexShader,ClearColorOp,ClearBlendMode, andClearZWrite.
ECL EX bullet/collision callback source-shape patterns
- In the
0x00424730/0x00424820/0x00424910collision trio,#pragma var_orderfixes physical stack slots independently of constructor chronology. The target slots nearest EBP areposition, outer size, inner size, origin, while the source must construct in the opposite semantic orderorigin -> outer -> inner -> position. Declaring the four realFloat3objects in semantic order while using#pragma var_order(position, outerSize, innerSize, origin, enemy, instruction)reproduces both the constructor call sequence and the0x38frame exactly. - A long positive body gate is not interchangeable with a reject-and-
continuespelling under VC7/Od. ECL EX callbacks0x00424A20,0x00424C40, and0x00424E50requireif (tag & mask) { ...large body... }; spelling the equivalentif (!(tag & mask)) continue;changes operand ownership and produces a characteristic function extent that is two bytes shorter. - Float wrapping in
FUN_004244F0needs both the target comparison polarity and target x87 operand owner. The exact source is equivalent todelta > 0 ? -2*pi + delta : 2*pi + delta;delta < 0, or writingdelta - 2*pi, preserves ordinary arithmetic semantics but changes the x87 status mask and/orfld/faddordering. FUN_004250D0retains a target-provenFloat3local that is constructed and never used. Keep such dead non-trivial locals when their constructor call and frame slot are present in the target; removing them is not source cleanup during matching.- The scripted slowdown callbacks at
0x004251B0and0x00425290proveg_EclGameTimeScaleFlags @ 0x017CE8FCas a separate ECL time-scale state word. Keep it distinct fromg_Supervisor.framerateMultiplier @ +0x188(0x017CE8E0) and preserve the bit-0x20 RMW path.
AsciiManager aggregate ctor and associated switch data
AsciiManager::AsciiManager @ 0x00402000is an empty source body whose 0x128-byte target is entirely the compiler-generated member-constructor sequence. Once the publicAsciiManagermember layout is correct, the empty constructor naturally reproduces 21 relocations including nineAnmVmconstructions, theAsciiManagerStringand popup vector-constructor iterators, Pause/Retry menu constructors, and the demo icon constructor. Treat a long empty aggregate constructor as a strong whole-layout attestation rather than filling its body with manual initialization.AsciiManager::OnDrawLowPrioImpl @ 0x00402B20has0x6A2authored function bytes, while VC7's COFF aux extent is0x6B6because a five-entry0x14switch jump table immediately follows the body. When instruction offsets already align andaux_size - authored_sizeequals the associated table extent, validate withsize = authoredandcompare_size = body + table; do not rewrite correct source merely to force the aux extent down to the authored range.- The boss-marker distance calculation is
fabsf(marker.pos.x - 32.0f - g_Player.position.x). Keeping the typedg_Player.position.xowner produces the targetg_Player + 0x2B4DIR32 addend and fixes both semantics and exact codegen.
Runtime math helper inventory classification
- Target helpers
_sinf @ 0x00409060,_cosf @ 0x00408D40,_sqrtf @ 0x0040B440,fabs @ 0x004031E0,fmodf @ 0x0041F090, andfsincos @ 0x00433880are math-runtime/library entries, not authored game functions. Their imported reccmp names already identify the runtime role, and their target bodies are x87/CRT helper shapes rather than subsystem-owned C++ source. Classify them aslibraryinstead of manufacturing authored replacements (especially inline-asm x87 wrappers, which are forbidden by the project rules). - Inventory correction is separate from exact acceptance: reclassification removes a proven library helper from the authored denominator; it does not claim that helper is reconstructed. After authored reaches 100%, these helpers belong to the explicit library-reconstruction lane.
VC7 float math inline-wrapper classification
- VC7
MATH.Hdefinesacosf,atanf, andtanfinline as float-returning wrappers around the corresponding double CRT functions. TH08 targets0x00462210,0x00462230, and0x00462250are the expected 0x15-byte wrapper shape (fldfloat argument, call CRT core,fstcompiler temp,ret 4). Treat these as library/runtime inventory, like the already-classifiedsinf/cosf/sqrtf/fmodf/fsincos, rather than manufacturing game-authored replacements.
Large /Os setup-thread source-shape recovery
GameManager::GameplaySetupThread @ 0x0043ABD7shows that equal addresses are not enough under VC7/Os: the target deliberately mixes a cachedgameManagerlocal with directg_GameManageraccesses. Preserve the lexical owner seen in the target instead of normalizing every access through one spelling.- Keep anti-tamper refreshes inside their original branch arms. Hoisting six identical
UpdateAntiTampercalls to shared tails shortened the target by dozens of bytes even though values were equivalent. - Preserve apparently redundant helpers when target locals prove them. The stage-5 spell-practice arm calls
IsSpellNumberEqualTo(212)and discards the result; its compiler-owned BOOL work slots are part of the target 0x60-byte frame. - Preserve multidimensional table shape.
g_TimeRequirementParams[stage][difficulty]naturally emits the targetstage << 4plusdifficulty * 4calculation; flattening tostage * 4 + difficultychanges codegen. forand explicitwhileare not interchangeable. The spell-practice BGM table target keeps++iat the body tail; afor (...; ...; i++)spelling introduced a 2-byte trampoline.- The play-count storage acts as seven contiguous
PlstPlayCountsrecords: six difficulty records followed by totals. A narrow typed overlay models that physical table and restores the target constant-index fastcall argument evaluation without one-past-array UB. - For constructor/destructor-free POD owners, naming real allocation work pointers and using direct
operator new/deletecan expose the same machine semantics without VC7 adding a second hidden new/delete temp. In this setup thread,oldCfg,oldGlobals,newCfg,newGlobals, and the malloc/free pointer occupy target homes-0x14..-0x24. - The final exact local order is
#pragma var_order(..., allocation, stageMode, configMode).stageModeuses lexicalif/else, andconfigModeis the integer source of thefild; the 3423-byte body then replays exactly with 183 relocations and no inline asm or padding.
Isolated exact promotion from a dirty production translation unit
- A function that is exact only in a dirty production translation unit must not be ledgered against that uncommitted object. Extract the minimal exact source body into a separate probe TU, preserve the production TU, and point the canonical match unit at the probe object.
TitleScreen::OnUpdateReplayMenu @ 0x0046E136was promoted this way: the minimal probe needs only the function body,g_StageNames,TITLE_MENU_ITEM_START_REPLAY, and the smallInitializeTitleVmAndSetSpritehelper, yet still reproduces all 3671 bytes and 107 relocations. - Keep the probe on the same compile rule as the owning production TU. The replay-menu probe uses
cc_TitleScreen(/Os /Oi- /Ob1), so source-shape conclusions remain comparable to the active Title lane while the normal executable link continues to use the production object. - Do not bundle nearby near-matches into an exact anchor.
DrawPieChartwas accepted independently;TitleScreen::RegisterChainremained separate until GensokyoClub commit1b630bbsupplied the later strict-zero-diffZUN_NEWhypothesis described below.
Title /Os switch tails and local-owner details
TitleScreen::OnUpdateStartMenu @ 0x004674E0andOnUpdateKeyConfig @ 0x00469636both have a 9-entry / 0x24-byte switch table immediately after the authored body. VC7 COFF aux extents include the table, so a naive size check reports +0x24. Recordsizeas the authored body andcompare_size = size + 0x24; replay all nine table relocations without counting them as authored bytes.- In StartMenu, two target tests read
g_GameManager.flags.isReplayand.isSpellPracticedirectly. Replacing them with out-of-lineIsReplay()/IsSpellPractice()calls shortens each site by one byte under/Os; preserve the lexical bitfield owner in source even though the helper is semantically equivalent. - In KeyConfig the entire 2383-byte body was already instruction-for-instruction correct; the residual was only a pair of stack homes. The target local order is
vmPair=-0x4, i=-0x8, keyToChange=-0xC, controllerState=-0x10, reproduced by#pragma var_order(vmPair, i, keyToChange, controllerState).
Cold-PCH recovery of TitleScreen TU-specific inline contracts
- A historical warm object can preserve a header-inline body after the header
has been changed to an out-of-line declaration. The stale TitleScreen object
made
DrawSpellStageSelectandDrawSpellCardSelectappear exact even though a cold build emitted four and twoAsciiManager::SetScalecalls. Each call made the caller six bytes longer, producing the characteristic+0x18and+0x0Cextent regressions. - Do not fix that pattern by making
SetScaleheader-inline globally. Other exact production TUs have target-proven relocations to the standaloneAsciiManager::SetScale @ 0x0042F2F0. In the TitleScreen TU, direct writes tog_AsciiManager.scaleXand.scaleYreproduce the target stores while preserving the out-of-line contract used by those other objects. ActualAddedCallback @ 0x00470A6Cdirectly readsflags.isReplay,flags.isDemoMode, andflags.isSpellPractice; the target direct-call set contains none of the corresponding getters. Under this/Os /Oi- /Ob1profile, replacing all three stale calls restores the exact 0x369-byte body.TitleSetupThread @ 0x00470E10uses the same directflags.isDemoModefingerprint at unit offset0x2D0.TitleScreen::RegisterChain @ 0x0047146Dis now accepted exact. The missing source shape wasZUN_NEW(TitleScreen, "TitleInf"), imported as a hypothesis from GensokyoClub commit1b630bband reproved locally against canonical Japanese 1.00d. Although non-DEBUGZunMemory::AddToRegistryinlines to its pointer argument, keeping the macro call around the new-expression changes VC7's hidden allocation/EH lifetime and restores the target0x40frame. The complete 0x119-byte body and all 20 relocations replay exactly. The debug label itself is optimized out, so its spelling is upstream provenance rather than target-observed semantics.- The rejected shared-header experiments remain useful negative evidence.
Moving
TitleScreen()out of line shrinks the frame too far to0x14; movingAnmVm()out of line reaches0x38but breaks the already exact 203-byte TitleScreen constructor. No artificial stack pad is involved in the accepted solution. - The exact
TitleScreen::TitleScreen @ 0x00471586frame is0x4Cin both target and object, soRegisterChainis not reporting a wrong aggregate layout or a wrong emitted constructor body. Further bounded probes also leave the caller at0x5C: moving the inline definition from the class to earlier in the.cpp, makingnew TitleScreena default-init expression, splitting declaration from assignment, limiting inline depth at the caller or constructor definition, movingTitleScreen.hppinto the PCH, and moving theAnmVm()body later in the same TU. A TU-local factory remains a real call even with__forceinline;/O1changes the whole function rather than only its frame; nothrow/throw()removes the target-observed new-expression EH contract. These are eliminated hypotheses, not candidate fixes; the macro-level allocation expression was the missing dimension. - Any header/TU experiment in this lane must rebuild the PCH as well as the
selected object. Use
scripts/build.py --build-type=objdiffbuild --fresh --object-name TitleScreen.obj; a warm selected-object build can otherwise replay the obsolete PCH state that caused this regression.
Canonical relocation identity and header-inline COMDAT ownership
- A match unit can become a false negative even when its instruction bytes and
relocation fields still replay exactly. Renaming a recovered function or
correcting its return/parameter type changes the decorated COFF symbol stored
in
match-units.toml. Verify the actual object relocation symbol and the target direct-call address, then update the manifest to that exact identity; do not wildcard a decoration or accept by positional bytes alone. - This occurred at the callers of
AnmLoaded::SetAndExecuteScriptIdx @ 0x004069F0, atPlayer::UpdateShooting,AnmManager::Draw2DRotatedOrAxisAligned, andAnmManager::CreateTextureFromFile. After the identity corrections, a cold canonical replay restored seven accepted functions without source changes. - A header-inline member may emit an out-of-line COMDAT in more than one
/Odproduction TU.Supervisor::IsFogDisabled @ 0x00406580belongs to this class: moving its body intoSupervisor.cppremoved the target-shapedmain.objcopy and produced a different/Osepilogue. Restoring the inline header body gives the target 0x1Amain.objCOMDAT while the target-proven call fromAsciiManager.objremains intact. - For this pattern, select the canonical object from detailed production anchors and rebuild from a clean PCH. Verify both the recovered COMDAT and at least one accepted caller before aggregate replay; the object containing a convenient out-of-line definition is not evidence of original TU ownership.
- The fourteen
GameManager/ZunTimerhelpers formerly appended tomain.cppshow how to select that consumer without guessing. Production undefined references plus target adjacency route the query/gauge/clock family toAsciiManager.obj, post-decrement toSpellCard.obj, andSetClockTimetoReplayManager.obj. Restoring natural header bodies emits the exact/OdCOMDATs from those callers; moving them to the nominal/Os /Ob1GameManager.objowner would instead choose the wrong profile. Compare the section-defined symbol, not an undefined symbol with the same decoration. The focused four-object replay passed 133/133 units, followed by a 1,105/1,105 cold aggregate replay. ZunTimer::operator+= @ 0x0041FDF0is the negative control: no current production object has an undefined reference to it. Keep its explicit exact body and its resulting layout residual until a target-backed consumer or owner is recovered. A lower inversion count is not evidence for assigning a COMDAT to an unrelated TU.- Inline visibility must be regressed through optimized callers, not only the
emitted helper. Making
AnmVmBase::Initialize @ 0x004068E0a class-body definition produced an exact/OdAscii COMDAT and a 260/260 focused replay, but a cold aggregate replay changed five/Os /Ob1Title functions. Keeping the header declaration-only and putting the same natural body explicitly inAsciiManager.cpprestores the target-neighbor owner without perturbing the Title compiler state. The expanded donor/recipient/Title/probe replay passed 294/294 before the 1,105/1,105 cold aggregate replay. AnmManager::SpriteHasTexture @ 0x004622C0is the inverse constraint. Moving its exact body out-of-line besideSetInterruptArrayremoved 20 Anm object inversions, but madeTitleScreen::OnDraw @ 0x0047087F0x3B bytes short. The accepted target unit has no relocation toSpriteHasTexture; its body is inlined in the caller. Preserve that header body and accept the deferred standalone COMDAT placement until a natural emission-order explanation is found. Layout metrics do not override caller bytes and relocations.- Shared helper ownership does not always require header-inline emission. The
six Global math helpers routed to
Background.cppandPlayerBomb.cppuse explicit target-local/Oddefinitions; this preserves shared-header visibility while matching both production references and target lexical neighborhoods. Use this form when the helper has a single proven consumer region and changing global visibility would add needless caller risk. - Rehoming such helpers can change secondary implicit COMDAT emission. After
the Global move,
Float3::Float3(float,float,float) @ 0x00404720disappeared fromGlobal.obj; target adjacency toPauseMenu::OnDrawand the exact section-defined production copy inAsciiManager.objsupport Ascii as its canonical owner. Always inspect donor and recipient symbol tables after an ownership move instead of assuming only the moved symbols can change. - The Sound fade cluster shows paired header emission. At
0x00406AC0..0x00406BE0, every outerSoundPlayerforwarding wrapper is immediately followed by itsCStreamingSoundcallee. Restoring both class bodies inline made the sole production consumer,AsciiManager.obj, emit that exact alternating order under/Od;PauseandUnPausefollowed the pairs naturally. Target adjacency plus the outer undefined references can therefore recover nested COMDAT ownership even when the inner helper has no direct reference from the consumer's authored source. - The three accessors at
0x0045E2D0..0x0045E300are the bounded opposite. Every caller is inSoundPlayer.cpp, exact caller relocations preserve real calls, and their target order precedes the explicit Sound constructor/free/ fade tail. Declaration-only headers plus explicit same-profile definitions restored the complete Sound object to 0 inversions / 1 run / 0 span. Do not generalize this to accessors with optimized or unbounded callers; removing inline visibility still requires clean-PCH aggregate replay.
Raw union members, bitfield owners, and local value-flow restoration
- When a union exposes both a narrow raw flags member and named
u32bitfields, the chosen source member controls access width. InAnmVm::SetZRotation @ 0x0040EC00,flags |= 4selected theu16view and emitted a 0x2D-byte word read/store sequence. Assigning the semanticupdateRotationbitfield emits the target dword read/OR/store and restores the exact 0x2B-byte body without changing the shared structure layout. - An out-of-line getter can hide the target value flow even when the getter's
standalone body is independently exact.
GameManager::CollectExtend @ 0x00439B29target code reads thef32life/bomb fields throughglobals, converts each through__ftol2, and compares the integer result. Expressing those two local reads as(i32)this->globals->...restores the complete 0x9E-byte/Os /Ob1body while preserving the accepted standalone getter functions and avoiding an unsupported repository-wide header-inline change. - Prefer the smallest semantic owner/value-flow correction supported by the target. A field-access fingerprint inside one production function does not, by itself, authorize changing the inline contract of every caller TU.
Title spell-card cursor comparisons and switch-tail validation
TitleScreen::OnUpdateSpellCardSelect @ 0x0046BBC0carries an 11-entry / 0x2C-byte jump table after its 0xFCF authored body. Canonical validation usescompare_size = 0xFFB, so the table relocations replay without inflating authored-byte progress.- Two cursor-wrap checks in the recovered source were semantically reversed, not merely codegen-equivalent. The target is
if (cursor >= currentNumberOfSpellCards), while the provisional source hadif (currentNumberOfSpellCards >= cursor). Under/Osboth forms have equal length but swap thecmpmemory owner; strict replay exposed exactly two 7-byte residual spans. Fix the semantics rather than trying to reshape the compare instruction. - When a large function is equal in extent, control flow, and relocation count but has a handful of repeated compare-owner residuals, inspect whether the source relation itself is reversed. Do not assume every operand-owner mismatch is an algebraically equivalent spelling issue.
Repeated inline helper work slots and frame expansion
TitleScreen::OnUpdateSpellStageSelect @ 0x0046B174had the same 589 decoded instructions in the target and provisional object, yet the object was 363 bytes shorter. The real discriminator was the frame: target0x88, object0x7C. Three calls usedAnmLoaded::InitializeAndSetSpritedirectly, while the target-shaped project helperInitializeTitleVmAndSetSpritecarries one source-visibleinlineSlot. Using that helper at all three call sites restores three 4-byte homes, movesthisfrom-0x78to-0x84, changes the repeated deep-stack accesses from disp8 to disp32, and naturally reproduces all 2636 bytes. When instruction counts match but frame size differs by a small multiple of four, inspect repeated inline-helper locals before assuming missing business logic or adding padding.ConvertToFullWidthDigits @ 0x0046D763is a useful tiny fastcall fingerprint: after the algorithm and six relocations matched, the only 16 residual bytes were ani/multiplierstack-home swap.#pragma var_order(i, multiplier)restores targeti @ -0x4,multiplier @ -0x8; do not rewrite the digit loop when the opcode topology already matches.TitleScreen::OnUpdate @ 0x00467399is another associated-tail case: authored coverage is0x10D, while VC7 emits an isolated0x147COMDAT whose final0x3Abytes are switch-associated data before the next mapped function. Keepsize = 0x10Dandcompare_size = 0x147; the full region replays exactly with 25 relocations.
Title Last Word unlock boolean/source-shape recovery
TitleScreen::UnlockLastWordSpellCards @ 0x0046CBBBis a useful VC7/Os /Ob1fingerprint: directif (A || B)does not preserve the target BOOL work slot, while((A || B) ? TRUE : FALSE)lowers to the targettemp=0/1; cmp tempsequence. A singleA > 0normalized through the same ternary lowers to the targetxor/cmp/sbb/neg/testform.- Put the history query on a small inline member view of
Catk, not a free inline(Catk *, shot)helper. The member form preserves the targetpush SHOT_ALL / pop / shlindex and one BOOL compiler temp without adding a separate Catk-pointer argument temp. - An inline unlock helper taking
i32 spellCardNumbernaturally prevents constant-folding ofunlockedLastWordSpellCards[spell-205] = spell; VC7 emits the target dword spell-number work home, variable index, and low-byte store. Ado { ... } while (0)macro is not equivalent here: VC7 kept a six-byte zero-loop tail at every call site. - Keep the target-proven loop lifetimes. The exact source uses distinct
k/n/ii/jj/kkindices for five later loops and#pragma var_order(i, totalCaptures, extraClearCount2, extraClearCount3, k, lastSpellCaptures15, extraClearCount4, n, requiredNormalCaptures, ii, extraClearCount6, jj, extraStageClearCount, kk, lastSpellCaptures30). This produces the target 0xF8 frame and places the Normal-list Catk pointer at-0x64. - Preserve genuine leftovers even when redundant: the target clears
requiredNormalCapturestwice immediately before the Normal spell-list loop. The second four-byte clear is required for the canonical 0xBA8 body.
Title spell-card info formatter source-shape
TitleScreen::FormatSpellCardInfo @ 0x0046D7F9matches as a 0x148-frame/Os /Ob1function with source locals ordered by#pragma var_order(spellCardNumber, i, totalAttempts, commentLine1, commentLine2). Long-livedCatk&or Last-Word-table references are not source locals in the target; spelling those accesses directly lets VC7 create only the target compiler pointer temps at-0x114..-0x148.- Equivalent ternaries are not byte-equivalent. The target uses zero-first lexical forms such as
totalAttempts == 0 ? unknown : spellNameandcaptures[SHOT_ALL] == 0 ? unknown : commentBuffer; reversing the condition and arms flips the short-branch topology. - The Last Word hint table begins at spell 204, one slot before
SPELLCARD_LAST_WORD_START(205). Its 0x30-byte record has two format pointers and two groups of five integer arguments. Recover the source index asspellCardNumber - (SPELLCARD_LAST_WORD_START - 1), notspellCardNumber - SPELLCARD_LAST_WORD_START. spellCardInfoVms[0..6]are the natural typed owners for target offsets0x11F2C..0x12F04; two 128-byte comment buffers and repeated directCatkHistoryreads reproduce the target vararg push order without overlays.
Title completion badge: preserving an inlined GameManager member owner
TitleScreen::DrawCompletionStatusText @ 0x0047052Dproves that an out-of-line-looking helper can still have an inline member source shape at a caller. A freeu16bit-test helper kept the correct mask arithmetic but foldedg_GameManager.clrdDatainto one memory displacement; a probe-local GameManager member view reproduced the targetthisparameter,character * 0x24, explicitadd &clrdData[0], and then the difficulty-indexed word load.- In the inlined stage-clear helper, keep source order
difficultyBits & ZUN_BIT(stage). VC7 evaluates the right operand first, so the target starts withxor/inc/shlfor the stage mask before loading the clear-data word. Reversing the&operands flips evaluation order even though the value is identical. - The completion-status third condition is target-proven as five independent OR arms: Easy, Normal, Hard, Lunatic clear, or
cursor > 3. The earlier reconstructedLunatic && cursor > 3precedence was semantically wrong. - Reuse the low-level
InitializeTitleVmAndSetSpritesource shape instead ofAnmLoaded::InitializeAndSetSprite; four lexical branches then naturally allocate the target VM/AnmLoaded work homes and produce the exact 0x38 frame.
Aggregate exact state requires a cold-build replay
Scope: the complete authored exact ledger and every object named by
config/match-units.toml.
Observed: on 2026-08-19, replaying 1,105 historically accepted rows from a cold VC7 build exposed 14 units that no longer reproduced. The old aggregate path also omitted 23 configured reimplementation objects unless they were built by hand. Focused comparisons had been run against objects and a PCH produced at different repository states, so their individual historical results did not establish a current aggregate result.
Inference: a focused exact remains evidence for that function and that object
state, but aggregate progress is invalidated by an untested shared header,
layout, compiler-flag, PCH, or object-graph change. A successful normal link
does not fill this evidence gap.
Working shape: configure.py derives the aggregate objdiff dependencies from
both objdiff.json and every object in match-units.toml.
verify-exact-units.py --all then regenerates that graph, asks Ninja to clean
its declared outputs, removes the explicitly listed VC7/linker side outputs
that Ninja cannot see, builds serially, and replays only rows accepted by
matches.csv.
Rejected alternative: reusing build/, relying on normal-build objects, or
running aggregate replay only after an agent manually notices missing objects.
Those paths are useful for diagnosis but cannot publish repository-wide exact
totals.
Reproduce:
python3 scripts/analysis/verify-exact-units.py --all --json \
> build/accepted-unit-replay.json
Result: after removing the 14 unreproducible claims, the cold build and replay
passed 1,091 / 1,091 accepted units. --reuse-build is explicitly diagnostic.
Public GitHub Actions cannot perform this attestation because the target and
pinned local VC7 environment are private; it remains a required local gate.
Generalization limit: this establishes current function-level authored replay only. It does not prove original object partition, linked-image layout, target-linked libraries, resources, or complete-PE identity.
Target-linked D3DX fast-table and CRT thunk boundary recovery
- The D3DX optimized dispatch tables at
0x004867B0(SSE) and0x004868D0(SSE2) are stronger naming/provenance evidence than heuristic disassembly labels. Their relocation order matchesobjd/i386/ssefasttable.objandobjd/i386/ssefasttable2.objfrom the VC7 PlatformSDK prereleaseD3DX8.LIB(SHA-2560d4a2b642485dcaa7671926a9a1a545c656d5eb73f160fe971b3deebf0b516b5). In particular, the table slots identify0x0048D3D0as SSE Vec3 normalize,0x0048D4A0as SSE plane normalize,0x0048DA50as SSE Vec4 normalize,0x0048E680as SSE quaternion normalize,0x0048EFB0as SSE2 Vec3 normalize, and0x0048F080as SSE2 plane normalize. Do not infer these identities from vector width alone; table relocation identity distinguishes the same-looking four-float normalize families. - For these optimized D3DX COMDATs, compare the complete archive section after
masking/replaying its COFF relocations. The target non-relocation bytes,
including post-
retalignment padding, exactly match the corresponding archive sections. The accepted function-body extents stop at the compiler return (0xC6,0xD8,0x9C,0x9C,0xC6,0xD8respectively), while the archive sections continue to aligned0xD0,0xE0,0xA0,0xA0,0xD0,0xE0. Keep body extent separate from archive-member padding; a next mapped address is not a boundary proof. - The
+0x0Elabels inside the aligned SSE/SSE2 Vec3/plane normalize bodies are real archive-local secondary symbols (...Normalize$\$1), not independent TH08 inventory functions. They have no target xrefs in the attested IDA session and live inside the same archive COMDAT. Preserve them as internal entry evidence rather than creating overlapping mapping rows. operator delete @ 0x004A43CFis the VC7 static-runtime??3@YAXPAX@Zthunk: the target is exactly one five-byte near jump to_free @ 0x004A427B. With the repository's/MTcompiler profile, the relevant archive isLIBCMT.LIB(SHA-2568815af7b9b6e0e28b77708ede25ab7ecfc4b05e1d8811f092c516cff5ce19d94), memberbuild/intel/mt_obj/delete.obj. That member owns an isolated five-byte.textsection with oneDISP32 _freerelocation. Use the member section/relocation as the extent proof; the adjacentoperator newaddress alone is only corroboration.
Microsoft COFF archive member identity for library replay
- VC7
.LIBlong-name tables use NUL-terminated member names in the archive observed here; do not assume GNU ar's/\nspelling. More importantly, a Microsoft archive can contain repeated member path names. A library comparator must therefore preserve all occurrences and disambiguate the configured member by its section-defined COFF symbol (or fail if more than one occurrence owns the symbol), rather than treating the member path as a unique key. - Library relocation replay must resolve the object field addend plus a pinned
target base. Masking relocation bytes is only a diagnostic. The initial D3DX
family proves
.data1bases0x018DA000(SSE) and0x018DA220(SSE2), plus.databases0x004C9FC0(Vec4 normalize) and0x004CA180(quaternion normalize);operator deleteproves a REL32 target of_free @ 0x004A427B.
Strict bounded symbols inside a shared library COFF section
Some VC7 CRT archive members place more than one externally named function in a
single .text section. Do not reject those functions merely because their
symbol is not at section offset zero, and do not solve the problem with an
arbitrary byte slice. A library match unit may opt into a bounded shared-
section comparison only by pinning both section_offset and the complete
section_size. The comparator then requires the configured symbol to begin at
that exact offset, requires a COFF function-definition auxiliary record whose
total_size equals compare_size, bounds every relocation relative to that
symbol, and rejects any subrange that extends beyond the pinned section. Units
without those explicit fields retain the stricter whole-section ownership rule.
Library archive decoration is naming evidence only after range replay
- VC7 CRT/C++ EH members can turn anonymous imported inventory rows into stable
names, but do not rename a
FUN_*row merely because an archive contains a plausible decorated symbol of the same size. First pin the archive hash and member, require the COFF function-definition extent to match the target body, replay every DIR32/REL32 field, and obtain a zero-difference canonical target comparison. Thetrnsctrl.objfamily at0x004A4419..0x004A4745is the corpus example: eight helpers replay exactly before four anonymous rows are promoted to_CreateFrameInfo,IsExceptionObjectToBeDestroyed,_CallCatchBlock2, and_CallSETranslator. - MSVC decoration can directly prove storage-level facts such as global
__cdecl(YA), return class, and simple parameter lists. It does not justify guessing complicated numbered type back-references. When the decorated_CallSETranslatorsymbol proves__cdeclandintreturn but the back-referenced parameter sequence has not been independently decoded, keep those parameter slots conservative rather than manufacturing a typed ABI.
Library extent repair can emerge from dependency replay
- A mapped library row can be undersized even when it does not overlap a later
row.
_inconsistency @ 0x004AA9E7was seeded as0x26bytes, so the conservative archive proposer initially rejected it.LIBCMT hooks.objcarries a function-definition aux extent and section size of0x2D; the canonical target has the corresponding cleanup path and final tail jump through0x004AAA13. Replaying all four relocations over the full 45-byte range produced zero differences. Repair the mapping from0x26to0x2Drather than weakening the proposer or truncating the archive function.
Library body extent and COFF comparison extent may differ for EH funclets
- VC7 may place a normal function, an alternate cleanup-entry prelude, and a
local EH funclet in one COFF function-definition section. Do not choose
between truncating the archive section and double-counting the funclet.
__FrameUnwindToState @ 0x004AA1B4has a target main-body return at0x004AA260, so its mapping/body extent is0xAD; the sameframe.objfunction-definition section is0xCEbytes and includes the cleanup bytes beginning at offset0xADplus the separately mapped funclet at0x004AA267. Usebody_size = 0xADandcompare_size = 0xCE, replay all ten relocations across the complete COFF section, and count only the main body in library progress.
Auxless library COMDATs require explicit whole-section ownership
- Some VC7 internal CRT helpers are function-type symbols whose auxiliary record
is raw rather than a function-definition extent. Do not infer their size from
the next symbol. A library unit may opt in with
allow_auxless_comdat = trueonly when the archive section itself is a code COMDAT, the named symbol is at offset zero, the complete section is the comparison extent, and that section has exactly one offset-zero function-type owner. The default remains to reject auxless symbols.FindHandler @ 0x004AA72Eis the motivating case.
Adjacent mapped starts can corroborate archive-proven extent repairs
- When a SHA-pinned VC7 member has a larger function-definition extent than the
imported mapping, compare the full member before changing the ledger. Two
lock-runtime dependencies show the pattern:
__amsg_exit @ 0x004A6155was mapped as0x22, whilewincrt0.objis0x25bytes and replays exactly up to the next mapped_fast_error_exit @ 0x004A617A; similarly___crtInitCritSecAndSpinCount @ 0x004AF7F3was0x67, whileinitcrit.objis0x8Bbytes and replays exactly to___crtGetStringTypeA @ 0x004AF87E. The next start is corroboration only; the acceptance proof is the complete archive extent plus relocation replay.
Nested CRT cleanup funclets do not justify truncating the parent extent
- A parent CRT function can have a cleanup funclet physically embedded before
the parent's final epilogue.
calloc @ 0x004A6B99is the canonical example: the imported0xAFmapping ended inside the cleanup region, whilecalloc.objcarries a0xBBfunction-definition extent and the target main epilogue returns at0x004A6C53. The0x004A6C43cleanup row remains a valid nested funclet overlap. Keep the parent at0xBB, replay the complete section, and preserve the overlap exception; do not truncate the parent merely to avoid double ownership in a linear address map.
Archive gaps can reveal missing library inventory starts
- Reconcile contiguous archive members against target control-flow gaps instead
of assuming the imported function list is complete.
trnsctrl.objproved thatCatchGuardHandler @ 0x004A44A1was entirely absent from the library inventory, while the followingTranslatorGuardHandler @ 0x004A44C5was truncated by three bytes. Their auxless code COMDATs are exactly0x24and0x71bytes and fill the target interval between__CxxFrameHandlerand_GetRangeOfTrysToCheckwithout gaps. Add the missing row only after archive symbol, target extent, and relocation replay all agree.
Identical helper COMDATs can collapse to one linked target copy
- Do not create multiple target rows merely because a static archive defines
multiple named COMDAT helpers. VC7
trnsctrl.objdefines_CallMemberFunction0,_CallMemberFunction1, and_CallMemberFunction2as three distinct symbols whose sections are all the same seven bytes (58 59 87 04 24 FF E0). TH08 contains one linked copy at0x004A4412. Preserve the single target range and record the archive alias fact; exact acceptance may use one stable symbol owner without pretending the executable contains three separate bodies. - The five bytes immediately before that helper are not another member-call
function:
_JumpToContinuation @ 0x004A43E2has a0x2Bmain-body extent ending at its indirect jump, while its VC7 function-definition section is0x30and includes the associatedpop ebx; leave; ret 8tail. Compare all0x30bytes but count only the0x2Bmapped body.
Large auxless EH COMDATs can contain mapped funclets beyond the main body
CallCatchBlock @ 0x004AA2E4andBuildCatchObject @ 0x004AA48Bshow that body-vs-comparison separation is not limited to small epilogues. Their target main bodies are0x97and0x170bytes, while the corresponding staticframe.objCOMDAT sections are0x1A7and0x17Cbytes and replay exactly across all 16/22 relocations. The larger sections contain EH cleanup/guard tails, including separately mapped helper starts. Keep the mapped main-body size for progress, compare the full unique COMDAT section, and recover child funclets separately rather than inflating the parent body.
Large CRT assembly helpers need full internal-table extents
memmove @ 0x004A4D50was imported as0x2A0bytes, but the VC7memmove.objfunction-definition is0x33Dbytes and the target continues through both upward/downward copy paths, jump tables, and 1/2/3-byte tails until the next mapped__mkdir @ 0x004A508D. Replaying all 47 DIR32 internal-label relocations across the complete0x33Dregion is exact. Treat jump-table data and tail cases owned by the function as part of its real extent; do not stop at an internal table boundary just because disassembly temporarily looks like data.- A separately mapped EH funclet such as
FUN_004AA427may correspond only to a local COFF label inside a larger function section. Do not manufacture a standalone archive function-definition for it. Such rows require a future explicit local-funclet acceptance rule with pinned section owner, local label, target extent, and non-overlap/overlap semantics.
Tail-local funclets need a stricter opt-in than auxless functions
- A local COFF label inside an accepted parent section is not automatically a
function. The library comparator supports
allow_tail_local_funclet = trueonly for a mapped target range that begins at a COFF label (storage class 6), lives in a pinned code-COMDAT section owned by an explicitly named offset-zero function symbol, and consumes the section tail exactly. The unit must pinowner_symbol,section_offset, andsection_size, and its body/comparison sizes must be identical. This deliberately does not cover middle-of-section cleanup funclets such as thecalloclock-release helper; those need an independently pinned end boundary before they can be accepted.
A mapping extent that ends inside an instruction is conclusively stale
terminate @ 0x004AA9B2was imported as0x2Ebytes, which ends in the middle of the five-byte call/jump encoding beginning at0x004AA9DE.hooks.objcarries a0x35function-definition extent ending exactly at the next mapped_inconsistency @ 0x004AA9E7; replay of$T18546,__SEH_prolog, two__getptdcalls, and the final_abort @ 0x004B05BDrelocation is zero-difference. An instruction-splitting boundary is enough to reject the imported extent immediately; use the archive/control-flow replay to establish the replacement rather than guessing from the next address.
Trap bytes and cleanup tails can be comparison coverage without body progress
_abort @ 0x004B05BDhas a 0x17-byte mapped body ending immediately before the finalint3, while VC7abort.objowns a 0x18-byte function section. Replaying the full section is exact, but only the 0x17 executable body counts toward library progress. Likewise__updatetlocinfo @ 0x004AA00Ekeeps its 0x32 main body while comparing the complete 0x3B section; the final nine bytes are independently accepted as the pinned tail-localFUN_004AA040cleanup range. Do not inflate function progress with trap/padding or a separately tracked cleanup tail just because the archive section owns it.
Signal/runtime parents can retain nested cleanup overlaps at full extent
_raise @ 0x004B17F2was imported as0x164bytes, ending before its main post-handler restoration and final__SEH_epilog.winsig.objcarries a0x179function-definition extent with 19 relocations and replays exactly to the next mapped function. The existing0x004B192Dcleanup helper remains a valid nested funclet overlap. Repair the parent to0x179; do not shrink it around the child or treat the child start as the parent end.
Static archive functions may not appear in the library's global symbol index
siglookup @ 0x004B17C4is a staticwinsig.objfunction with decorated COFF symbol?siglookup@@YAPAU_sigtab@@H@Z. It does not appear in the archive-wide global symbol listing used bynm -A, but the extracted member symbol table provides a normal0x2Efunction-definition extent and exact target bytes. Treat the archive index as a discovery aid, not proof that a member-local function is absent; inspect the owning COFF member when target control flow or relocations point into it.
Exact VC7 archive provenance can correct modern runtime naming
- Do not preserve a modern CRT name when the pinned VC7 archive proves a
different symbol identity. TH08's
0x004A5AC0body is emitted byLIBCMT time.obj::_timeand replays exactly across its 0x39-byte extent; the imported_time32mapping name reflects later CRT terminology, not the VC7 link input. Rename the mapping to_timeonly after the archive member and canonical target comparison agree.
Reject library candidates on non-relocation bytes even when symbol names look perfect
winsig.obj::_signalis a concrete fail-closed example. The archive has an isolated 0x1A9-byte function-definition section, but replaying it at the imported0x004B196BFUN_*row produces hundreds of non-relocation byte differences. Do not rename or accept the row from symbol-name proximity, archive membership, or a plausible signal-family neighborhood. By contrast, the same member's static_siglookupsection at0x004B17C4replays exactly. Keep the mismatch as a provenance/boundary blocker and move on.
Variadic CRT formatting wrappers can be accepted as small isolated archive functions
- VC7
LIBCMTvsprintf.obj::_vsprintf,sprintf.obj::_sprintf, andsscanf.obj::_sscanfare each isolated whole-section function definitions in this target. Exactness still comes from relocation replay: the first two resolve to__outputand__flsbuf, whilesscanfresolves to_strlenand__input. Treat their small size as a convenience, not a reason to skip archive identity or relocation checks.
VC7 onexit body/cleanup separation
LIBCMT onexit.obj::__onexit @ 0x004A3D7Eis a 0x38-byte COFF function-definition extent, while the target main body ends after 0x32 bytes. The final 6 bytes are the separately mapped cleanup tail at0x004A3DB0and replay exactly as a tail-local funclet. Accept the parent asbody_size = 0x32, compare_size = 0x38; do not inflate parent progress or delete the child row to force non-overlap.- The same member's
__onexit_lkand_atexitare independent whole-function extents. Keeping all three member identities plus the explicit cleanup tail makes the exit-registration dependency chain reproducible without treating linker/compiler-owned associated code as authored body bytes.
VC7 onexit uses a mapped main body plus a compiler cleanup tail
__onexit @ 0x004A3D7Ereturns at0x004A3DAF, so its inventory body is correctly0x32bytes.onexit.objdefines a0x38-byte function section; the final six bytes are the separately mapped unlock cleanup at0x004A3DB0. Accept the parent asbody_size = 0x32,compare_size = 0x38, and accept the cleanup only through the explicit tail-local-funclet schema pinned to the__onexitowner. Do not inflate the parent body merely because the COFF function-definition extent includes its cleanup tail.
Static timezone helpers require member-level COFF inspection
LIBCMT tzset.obj::__tzset_lk @ 0x004AAB57and__isindst_lk @ 0x004AAF80are static auxless code COMDATs, so the archive's global symbol index is insufficient for exact acceptance. Read the member COFF symbol table directly, require offset-zero unique function ownership and whole-section extent, then replay every relocation. Their complete 0x271/0x18B target ranges match with 58/30 relocations respectively.- The logical target names are
_tzset_lkand_isindst_lk; retain the extra leading underscore only in the COFF symbol field. This is the same decoration boundary used by the public__tzset/_isindstwrappers.
Resolve FID_conflict: library names only after archive identity and exact replay
_getenv_lk @ 0x004B05D5was imported asFID_conflict:__getenv_lk. VC7LIBCMT getenv.objprovides a single 0x81-byte__getenv_lkfunction definition whose eight relocations and all non-relocation bytes replay exactly at that address. Remove the conflict prefix only after that evidence; a decompiler conflict label is not provenance and should not survive once the original archive owner is established.
Library extent triage: instruction truncation vs associated cleanup
_strcmp @ 0x004AFE80is a hard stale-boundary case: the imported 0x87 extent ends on the first byte of the final two-byte backwardjmp.strcmp.objgives a 0x88 function-definition extent and the complete target range matches, so the mapping must be repaired to 0x88. An extent that splits an instruction is never a valid compiler boundary._msize @ 0x004A6E01is the opposite case. Its main function returns at body offset 0x69, so the imported 0x6A body is correct;msize.objcontinues to a 0x76 function-definition extent containing an alternate-entry prelude and the separately mapped 9-byte cleanup tail at0x004A6E6E. Keepbody_size = 0x6A, compare all 0x76 bytes, and accept the child through the tail-local-funclet schema ($L19142 @ +0x6D).
MBCS runtime parents keep mapped bodies separate from cleanup tails
___updatetmbcinfo @ 0x004B01FAreturns after a 0x63-byte main body, butmbctype.objdefines a 0x6F-byte function section. The associated tail contains the separately mapped 9-byte cleanup at0x004B0260; compare the parent across 0x6F bytes while counting only 0x63 body bytes._setmbcphas the same shape: 0x147 body, 0x150 comparison extent, and a 9-byte mapped tail at0x004B0546.___crtCompareStringA @ 0x004B279Dis the opposite case: the imported 0x356 extent cut off live main control flow. Target execution reaches the return at0x004B2B26, anda_cmp.objsupplies a 0x38A-byte function definition with 38 replayed relocations. Repair the body to 0x38A rather than treating the final 0x34 bytes as associated data.
Static archive-local helpers can still be exact target-linked functions
_strncnt @ 0x004B2781is a statica_cmp.objfunction and therefore is not surfaced by every archive symbol-index workflow. Direct COFF inspection shows a unique auxless 0x1C-byte code COMDAT whose target bytes match exactly with no relocations. When a linked parent relocates to an internal helper, inspect the owning member's full symbol table instead of treating absence from the archive global index as absence from the executable.
VC7 crt0dat startup/termination family
crt0dat.objcontributes a coherent startup/termination chain at0x004A69DF..0x004A6B98:__crtExitProcess,_lockexit,_unlockexit,_cinit,doexit, publicexit,_cexit, and_c_exit. The last wrapper at0x004A6B8Awas absent from the imported inventory; its 0xF-byte archive function fills the exact gap beforecalloc @ 0x004A6B99and replays oneREL32 _doexitrelocation.__crtExitProcesshas a 0x30-byte COFF function-definition extent but only 0x2F target body bytes before the terminalint3, matching the existing_abortprecedent. Keepbody_size = 0x2F, compare_size = 0x30so compiler trap bytes are verified without inflating body progress.
VC7 initsect inventory holes
initsect.objowns_RTC_Initialize @ 0x004ACBFEand_RTC_Terminate @ 0x004ACC42, each as a 0x44-byte function-definition section with five relocations. The imported inventory truncated Initialize to 0x3D and omitted Terminate entirely, even though target control flow has cleanretboundaries at 0x004ACC41 and 0x004ACC85.- When an archive relocation points into an apparently unmapped gap, inspect the exact member before assuming the target address is data or an internal label. Here the archive and target establish a missing public runtime function, so add a new library row rather than folding it into a neighbor.
Distinguish stdio jump-table tails from genuinely truncated main bodies
_output @ 0x004A74F9returns after a 0x775-byte main body.output.objcontinues for 0x20 bytes with a relocation-bearing local-label jump table, so acceptbody_size = 0x775andcompare_size = 0x795._input @ 0x004AB460is different: the imported 0xA70 extent cuts off live parsing/error-return control flow. The target reaches its return at0x004ABF07;input.objdefines exactly 0xAA8 bytes and the next mapped function starts at0x004ABF08. Repair the body to 0xAA8 rather than classifying the final 0x38 bytes as associated data.
Mid-function SEH cleanup funclets do not define parent extents
LIBCMT lseek.obj::__lseek @ 0x004AF0F4,write.obj::__write @ 0x004AF344, andread.obj::__read @ 0x004B224Eeach have a 0xAB COFF function-definition extent. The imported 0xA0 mappings stopped shortly after a nested cleanup funclet, but target control flow continues through errno/doserrno handling and__SEH_epilogto the parent return. All three complete 0xAB ranges replay exactly with 12 relocations each.- Keep the existing
mapping-overlaps.csvnested-funclet rows. A cleanup funclet reached by an internal SEH call is a separately useful target fact, but it does not truncate the containing CRT wrapper when the parent's CFG resumes afterward. Repair the parent boundary and preserve the overlap rather than choosing one fact over the other.
Stdio wrapper parents may legitimately overlap nested unlock funclets
_lseek @ 0x004AF0F4,_write @ 0x004AF344, and_read @ 0x004B224Eeach have a 0xAB VC7 function-definition/body extent. The imported 0xA0 extents stopped before the final error path and SEH epilogue. Their mapped cleanup starts at0x004AF17B,0x004AF3CB, and0x004B22D5remain valid nested funclet overlaps; repair the parent rather than deleting the child.
Stdio internal helper ownership after parent exactness
- Exact parent
output/inputreplay can expose small archive-local helpers that are also independently mapped in the target.output.objownswrite_char @ 0x004A746B,write_multi_char @ 0x004A749E, andwrite_string @ 0x004A74C2;input.objowns_inc @ 0x004AB44A. Their isolated auxless COMDATs replay exactly and may be accepted independently without double-counting any parent extent. 0x004B0C93was imported asFID_conflict:_ungetc, but exactungetc.obj::__ungetc_lkreplay fixes the identity to logical_ungetc_lk. Prefer archive-proven internal names over decompiler conflict labels once member identity, extent, and relocations all agree.
File-handle wrapper dependency closure
- The repaired
_lseek,_write, and_readwrappers point directly at VC7 worker functions_lseek_lk,_write_lk, and_read_lk, plus the shared_lock_fhandle/_unlock_fhandlepair. All five are isolated function-definition candidates inlseek.obj,write.obj,read.obj, orosfinfo.objand replay exactly with their COFF relocations. - Once exact archive identity is established, replace anonymous
FUN_*target labels with the logical CRT names while preserving the decorated COFF symbol separately inlibrary-match-units.toml. This keeps target inventory readable without losing provenance.
Stdio buffer and multibyte conversion dependency closure
- The accepted
output/inputcore calls small CRT support members across_getbuf.obj,_filbuf.obj,wctomb.obj, andmbtowc.obj._getbuf,_filbuf,wctomb,mbtowc,__wctomb_mt, and__mbtowc_mteach replay exactly as their own VC7 function-definition extents with explicit relocations. - Treat
_mtconversion helpers as normal library functions when the archive exposes a real function-definition symbol; unlike compiler local cleanup labels, they do not require funclet-specific acceptance rules.
Public file wrappers should close through exact _lk workers first
- Before repairing higher-level close/flush/seek wrappers, pin their direct worker functions. VC7
osfinfo.obj,lseeki64.obj,fflush.obj, andfclose/close.objprovide exact_free_osfhnd,_lseeki64_lk,_flush,_fflush_lk,_fclose_lk, and_close_lktarget functions. - This direction reduces ambiguity when a public wrapper contains an SEH cleanup funclet: the parent can be validated against known exact lock/unlock and worker targets rather than treating internal calls as unnamed CFG noise.
Math-runtime conflict labels should collapse to the original archive symbol
0x004AD28Carrived asFID_conflict:__set_errno_from_matherr. VC7fpexcept.objdefines__set_errnoat an isolated 0x28-byte function extent; both relocations and every non-relocation byte replay exactly. Prefer the original archive identity once exact evidence resolves the imported conflict label, just as with_getenv_lk.
Public close/commit wrapper inventory repair
fclose.obj::_fcloseuniquely matches0x004B2609..0x004B2659(0x51 bytes), a gap omitted from the imported inventory between exact_fclose_lkand the next wrapper. Add a realfcloselibrary row rather than attributing those bytes to padding or a neighbor.commit.obj::__commit @ 0x004B265Aandclose.obj::__close @ 0x004B2E0Dreplay their full 0xBC / 0x9B function-definition extents. The imported 0xB1 / 0x90 mappings stopped inside the parent error/SEH path because each contains a nested unlock cleanup funclet. Preserve the child overlap rows and repair the parent extents through their final__SEH_epilog/ret.
fflush.obj gap inventory
- The target gap immediately after
_fflush_lk @ 0x004B1C3Bis real CRT code, not padding.fflush.objgives a static auxless_flsallsection of 0xD5 bytes at0x004B1C69followed by the 0x9-byte public_flushallwrapper at0x004B1D3E; the next existing mapping starts exactly at0x004B1D47. - Full relocation replay establishes
_flsallwith 13 relocations and_flushallwith oneREL32 _flsall. When a mapping gap aligns exactly with consecutive archive sections, add the missing library rows rather than widening a neighbor or calling the bytes alignment.
x87 common.obj uses multiple function definitions inside one shared code section
- The nine helpers from
_twoToTOS @ 0x004A7E60through_check_range_exit @ 0x004A7F49all live in a single 0x18C-byte.textsection in VC7common.obj. Each symbol still has its own function-definition aux extent, so exact units must pin bothsection_offsetandsection_size = 0x18C; treating each function as an isolated section fails closed in the comparator. Relocation offsets remain function-relative.
Bounded functions inside one shared VC7 .text section
common.objstores nine x87 helpers in one 0x18C non-COMDAT code section:_twoToTOS,_load_CW,_convertTOStoQNaN,_fload_withFB,_checkTOS_withFB,_fast_exit,_math_exit,_check_overflow_exit, and_check_range_exit. Function-definition aux records give individual extents, but each symbol'ssection_offsetmust be retained against the common member-widesection_size.0x004A7F35was a true inventory hole. Its 0x14 target bytes exactly match__check_overflow_exitat section offset 0xD5, between_math_exitand_check_range_exit.
D3DX x3d quaternion archive identity
- The SHA-pinned VC7 prerelease
D3DX8.LIBcontains a coherentobjd/i386/x3d_quat.objfamily. Seven target rows at0x0048F776,0x0048FD22,0x0048FD8E,0x0048FEC5,0x0048FF52,0x00490048, and0x00490194replay exactly from that member after explicit COFF relocation resolution. - Prefer the archive-decorated
x3d_D3DXQuaternion*identities over importedFUN_*names. MSVC decoration directly establishes the global__stdcallABI; use it to type return/argument pointer classes, but do not infer unrelated source ownership from thex3d_prefix. x3d_D3DXQuaternionSquadSetup @ 0x00490194is a useful high-density acceptance fixture: the 0xA59-byte function replays 66 relocations with zero differences. A large exact result here is evidence for the archive/member/relocation model, not permission to accept neighboring D3DX functions without their own unit replay.
D3DX x3d matrix archive identity
objd/i386/x3d_matx.objin the same SHA-pinned prereleaseD3DX8.LIBnow has six independently replayed target owners: MatrixIdentity, MatrixTransformation, MatrixRotationYawPitchRoll, MatrixRotationAxis, MatrixTransformation_K7, and MatrixInverse_K7.- Do not assume a uniform calling convention from the family name. VC7 decoration shows
x3d_D3DXMatrixIdentityas global__cdecl(YA...) while the other five use global__stdcall(YG...). Preserve the decorated ABI per symbol. - The 0x1198-byte MatrixTransformation and 0x1154-byte MatrixTransformation_K7 bodies both replay exactly, so large x3d functions are valid direct archive match units when their own COFF relocation graph is explicit. Their success does not authorize range-based acceptance of neighboring matrix code.
Return-type-only VC7 symbol migrations
- A source return-type cleanup can preserve every instruction while changing
the VC7 decorated symbol. Changing
EnemyManager::SpawnEnemy1/2fromvoid *toEnemy *leaves the x86 thiscall ABI unchanged but moves the compiler symbol fromQAEPAX...toQAEPAUEnemy@2@.... - Read the replacement identity from the rebuilt defining COFF object, then
migrate every configured caller relocation together. Do not guess the
class/struct decoration (
PAVversusPAU) from source spelling.