8.9 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.
-
Swap order. The target loads both elements, then stores to
*bestfirst:dx=*hi; cx=*best; *best=dx; *hi=cx. That istemp = *best; *best = *hi; *hi = temp;— the mirror of the naturaltemp = *hi; *hi = *best; *best = temp;. This alone made both selection sorts exact. -
Unsigned comparisons throughout. The target uses
jbfor theelement_count < 2guard,jafor the cutoff test, andshr(notsar) for the midpoint. Soelement_countand the runningsizeare unsigned long. -
lo+1 < j, notlo < j-1. The target computeslea eax,[esi+2]and compares againstj; writing the algebraically identicallo < j-1produceslea eax,[edi-2]with the operands reversed. -
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 twosars or cancel them and mask (and eax,0xfffffffe) — 16 bytes too many. Only an explicit byte-basis comparison reproduces the plaindec:if ((char *)j-(char *)lo-1>=(char *)hi-(char *)i)Note
j-1-lo >= hi-i,j-lo > hi-iand the above are all integer-identical; only the third matches the emitted code. -
depth = 0is assigned beforehi. The target stores[ebp+0xc](which the optimiser reuses fordepthonce theelement_countparameter is dead) at 0x1f andhiat 0x26. Ordering the source the other way emits the two stores swapped. This was the last 3 instructions.
3. Notes
dwordis not acseries.htype; the 4-byte variant useslong.- Both
qsortbodies 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.
Comparator ABI boundary (2026-09-12)
A runtime finding from Stian Eklund's non-byte-matching NTSC decomp identified
renderer corruption when its profile_sort32_compare_proc was declared as
returning int rather than a byte-sized Boolean. That finding is consistent
with the January evidence here: the custom qsort_2byte and qsort_4byte
callbacks return the project boolean type (typedef byte boolean), and all
four functions in this object are strict exact with that ABI. Do not widen
either custom-sort callback to int; a callee that only defines AL cannot be
safely consumed as though it defined all of EAX.
This rule does not apply to compare_profile_sections in profile.c.
That function is passed to the C runtime qsort, whose comparator contract is
int (*)(const void *, const void *), and it correctly remains int. The
custom value-comparator ABI and the CRT pointer-comparator ABI are separate
boundaries. Current January gates also keep profile_enter_private and
profile_exit_private strict exact, so downstream renderer differences must
not be "repaired" by changing either proven interface.
Two user-supplied analyses of Xbox debug build 2276 provide independent
cross-build corroboration: profile_sort_decompilations.md (SHA-256
28cfc10694a9b6437d91f32c9dad3e7d4a3389c533c26643f9e48cc6560ce732)
and profile_sort_corruption_analysis.md (SHA-256
ada613e65c8992fed8b165d5529e2c65076c51511d7da2a3358f831c1add9a51).
Its 16-bit selection sort tests AL immediately
after the indirect comparator call at 0x91d22; its 32-bit selection sort does
the same at 0x91d7c; and both quicksort comparator sites test AL at
0x91f86 and 0x91fa3. The reported structure-surface comparator also
defines only the byte result. Declaring that callback as returning int
therefore makes a rebuilt caller test stale upper EAX bits, which can retain
the timestamp written by nearby profiling code and corrupt the surface order.
This explains the observed renderer symptom, but it does not move January's
custom sorts out of their target-proven sort.obj owner or change the CRT
comparator above. Treat the 2276 addresses and source ownership as cross-build
evidence; the January split object and exact gates remain authoritative.
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.