Files
Cesarus85--mjolnir-vr/docs/object_matching_logs/sort_obj.md
T
Jonas VolmanandClaude Opus 5 3328df1847 Match cseries sort object
Byte-exact, all four functions, whole object.  Source was a pure skeleton with
no code; status MISSING work now NonMatching -> Matching.

An iterative quicksort for 2-byte and 4-byte elements with a 30-deep bounds
stack and a selection-sort cutoff at 8.  The two code_* symbols are the
selection sorts and take hi in EAX under VC7's custom convention for statics.
The comparator takes values, not pointers, and the zero-extension in the target
pins the 2-byte element type to word rather than short.

Five source-shape findings, each read off the disassembly:

- the swap loads both elements then stores to *best first, which is
  temp = *best; *best = *hi; *hi = temp - the mirror of the natural form.  That
  alone made both selection sorts exact.
- element_count and size are unsigned: the target uses jb, ja and shr where
  signed types give jl, jg and sar.
- the source writes lo+1 < j, not the algebraically identical lo < j-1, which
  reverses the lea and cmp operands.
- the larger-partition test compares raw byte differences.  On word pointers
  every element-count form makes MSVC emit two sars or cancel them and mask,
  costing 16 bytes; only an explicit byte-basis comparison gives the plain dec.
  j-1-lo >= hi-i, j-lo > hi-i and the byte form are integer-identical and only
  the last matches.
- depth = 0 is assigned before hi; the reverse order swaps the two stores.

Both qsort bodies went exact from the same five fixes, which is good evidence
the shapes are right rather than coincidental.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 04:33:37 -07:00

6.6 KiB

source/cseries/sort.obj — MATCHING

Status: byte-exact. All four functions, whole object. Lane: claude/libcmt-stream · Object status in config/config.json: Matching

Toolchain: XDK 3911 CL.Exe 13.00.9254.1 · Flags: the game set, /O2 /Oy- /DDEBUG /Dxbox Start state: the source was a pure skeleton — symbol-map header and empty section comments, no code.

symbol target ours exact
_code_00080360 (2-byte selection sort) 96 B / 0 rel 96 B / 0 rel yes
_code_000803c0 (4-byte selection sort) 80 B / 0 rel 80 B / 0 rel yes
_qsort_2byte 336 B / 1 rel 336 B / 1 rel yes
_qsort_4byte 320 B / 1 rel 320 B / 1 rel yes

section_infos_equal passes for all four; .text sections are identical with relocation fields masked. Normalized SHA-256: 21df9a493f4dd024, c98ad64dbdb87dbc, c09f5aead7542561, 1cedc780c4faa025.


1. What the object is

An iterative quicksort specialised for 2-byte and 4-byte elements, with an explicit 30-deep bounds stack and a selection-sort cutoff at 8 elements. The two _code_* symbols are the selection sorts; they take hi in EAX under VC7's custom calling convention for statics with fully-known call sites.

The comparator takes values, not pointers: the selection sort does xor eax,eax; mov ax,[ebx] before pushing, i.e. zero-extension — so the 2-byte element type is word (unsigned short), not short. texture_page.c, itself an exact object, declares the callee as boolean (*)(short, short); that prototype disagrees with this definition but is ABI-compatible (both push 4 bytes), and the zero-extension in the target is decisive for what sort.c itself uses.

compare(a, b) returns true when a sorts after b — proven by the selection sort using it to find the maximum and swap it to the high end.

2. Source-shape findings

Five corrections were needed; each was read directly off the disassembly.

  1. Swap order. The target loads both elements, then stores to *best first: dx=*hi; cx=*best; *best=dx; *hi=cx. That is temp = *best; *best = *hi; *hi = temp; — the mirror of the natural temp = *hi; *hi = *best; *best = temp;. This alone made both selection sorts exact.

  2. Unsigned comparisons throughout. The target uses jb for the element_count < 2 guard, ja for the cutoff test, and shr (not sar) for the midpoint. So element_count and the running size are unsigned long.

  3. lo+1 < j, not lo < j-1. The target computes lea eax,[esi+2] and compares against j; writing the algebraically identical lo < j-1 produces lea eax,[edi-2] with the operands reversed.

  4. The larger-partition test is a byte-basis comparison. The target computes raw byte differences with no shift and no mask:

    e0  mov eax, edi     ; j
    e2  sub eax, esi     ; j - lo, in BYTES
    e4  mov edx, ecx     ; hi
    e6  dec eax
    e7  sub edx, ebx     ; hi - i, in BYTES
    e9  cmp eax, edx
    eb  jl  <right-larger>
    

    Written on word * pointers, any element-count form makes MSVC either emit two sars or cancel them and mask (and eax,0xfffffffe) — 16 bytes too many. Only an explicit byte-basis comparison reproduces the plain dec:

    if ((char *)j-(char *)lo-1>=(char *)hi-(char *)i)
    

    Note j-1-lo >= hi-i, j-lo > hi-i and the above are all integer-identical; only the third matches the emitted code.

  5. depth = 0 is assigned before hi. The target stores [ebp+0xc] (which the optimiser reuses for depth once the element_count parameter is dead) at 0x1f and hi at 0x26. Ordering the source the other way emits the two stores swapped. This was the last 3 instructions.

3. Notes

  • dword is not a cseries.h type; the 4-byte variant uses long.
  • Both qsort bodies are identical modulo element type, and both went exact from the same five fixes — a useful confirmation that the shapes are right rather than coincidental.
  • The bounds stacks are lo_stack[30] / hi_stack[30], fixed by the frame size (sub esp,0xf4 = 4 + 120 + 120) and the [ebp+eax*4-0x7c] / [ebp+eax*4-0xf4] addressing.

4. Current-integration revalidation (2026-08-13)

The completed source was recovered from Claude's local claude/libcmt-stream campaign commit bf44b00d3e97f5781068b85d35d86f6d8751226e, then cherry-picked onto the current authoritative integration base 66af3489. This was an audit and admission lane, not a fresh reconstruction: reusing the already-proven work avoided repeating the source-shape search above.

This checkout successfully rebuilt the object through Ninja with XDK 3911 CL.Exe 13.00.9254.1 and the configured /O2 /Oy- /DDEBUG /Dxbox flags. The hardened comparator then reproduced the complete hashes and relocation evidence:

symbol padded bytes relocations normalized SHA-256 strict result
_code_00080360 96 / 96 0 / 0 21df9a493f4dd0244dfd9e3bdf4c1cfc68b33b8fa11f2e6bc8e30a76102b9205 exact
_code_000803c0 80 / 80 0 / 0 c98ad64dbdb87dbc89d00c9ff63b6ae4b1ec6685291add335aac30724f7032d4 exact
_qsort_2byte 336 / 336 1 / 1 c09f5aead7542561d64a5e7b41b4ba79c23bfbdf1cd45ed8c713fafa99dab200 exact
_qsort_4byte 320 / 320 1 / 1 1cedc780c4faa02599391876115607221a735e6d1cd51f7bcecb98eeca37dd3c exact

Both quicksort relocations match in address, type, destination symbol, and addend. The January object owns only these four .text COMDAT sections; it has no .data, .bss, or .rdata to reconstruct. The candidate's .drectve and .debug$S sections are the ordinary configured compiler artifacts and contain no game-owned data.

The fail-closed TU regression gate reported all four symbols as still_exact, with no changed non-exact functions, warnings, or failures. The full 466-object halobetacache_build succeeded, ordinary and semantic progress generation completed with zero unit errors, and all 173 tooling unit tests passed.

House-rule audit

The implementation is readable C: no assembly, volatile, __forceinline, undefined behavior, byte patching, special flags, or comparator exceptions are used. Every parameter is on its own line, every function has an explicit final return;, and the private helpers are correctly file-local. The two public quicksort functions also retain their direct element_count < 2 early return. This is the narrow exception to the preferred single-return style: it is the January-proven control-flow shape, and replacing it with an artificial shared exit would change otherwise exact code. No tag/object access macros apply to this generic cseries utility.