Repository navigation
FATX and Save Contracts
Original source baseline: SpiritBoot main 27149596, reconstructed from Roswell 1569e2e8 with the ordered patches. The zero-slot repair discussed below merged in PR #88 at 11b35b93 and is present in patch 0041. It was absent from the older 27149596 baseline. All twelve refreshed-head CI checks passed; five production-body slot cases pass. Bounded native directory creation/menu qualification is complete, while issue #87 remains open for loading and usable-save/reopen qualification.
A title saving data traverses several contracts:
NtCreateFile / NtWriteFile / NtSetInformationFile / NtFlushBuffersFile
→ Xbox ABI bridge and native I/O manager
→ filesystem create/read/write/information handler
→ directory metadata and FAT-chain operations
→ Cc/Nxc cache and paging I/O
→ storage driver and emulated disk
→ completion status + transferred-byte count
An on-screen “disc problem” is a title's interpretation of a returned error. It does not prove an optical read failed. A scoreboard/replay directory creation failure can prevent later child-file creation and surface as the same generic message.
Public call behavior is in Runtime-Calls. Cache ownership and completion are in Cache-and-Pinning. This page preserves the filesystem distinctions that those APIs alone cannot express.
FATX directory entries are 64 bytes. Filename length and attributes determine whether an entry is a live file/directory, reusable, or an end marker. Do not apply FAT12/16/32 short-name or long-file-name entry rules to FATX by analogy.
| Filename-length byte | Older 27149596 predicate |
Merged #88 handling | Important distinction |
|---|---|---|---|
0xe5 |
Deleted/reusable | Unchanged | A later entry can remain live |
0xff |
End marker | Unchanged | Existing scan terminates here |
0x00 |
Not recognized as deleted or end | Unused/reusable and skipped as non-live | Not an end marker: later valid entries must remain visible |
| Valid nonzero length | Occupied | Unchanged | Validate filename/attributes and resolve the actual cluster chain |
The independent FATX format account describes zero and 0xff as unused states and 0xe5 as deleted. This is a behavioral format reference, not source used for implementation. #88 changes the local shared predicate and tests the maintained local directory allocator.
A zero first cluster for an empty directory is not, by itself, proof of corrupted media: this implementation's FATXAddEntry() initializes a new entry with FirstCluster = 0, including when setting the directory attribute. vfatInitFCBFromDirEntry() derives directory size from its chain, treats first cluster zero as size zero, and uses first cluster one for the fixed-root region. A directory's recorded on-disk file-size field is therefore not an ordinary file EOF oracle. This establishes the implementation convention; it does not establish compatibility with every external FATX writer. The relevant routines are in Spirit's filesystem implementation.
vfatFindDirSpace() in dirwr.c is amended by SpiritBoot's directory/cache patches. It initializes the directory cache, derives entry count from the FCB's in-memory size, and scans for a contiguous run of reusable entries or the end marker.
Pins are bounded by the smaller of disk cluster size and 16 KiB. This prevents a 32/64 KiB cluster from becoming an illegal single cache pin. Entry strides and file offsets must advance together across block boundaries. A pointer into a previous pin must not be reused after unpinning.
If there is insufficient space, a growable directory gains one cluster. The new region is initialized in bounded cache chunks (0xff for FATX). An entry following newly occupied end-marker space is reinitialized when necessary. The exact growth guard is vfatFCBIsRoot(pDirFcb) && FatType != FAT32: it rejects growth of FAT12, FAT16 and FATX roots. FATX32 is a separate type from FAT32 and does not escape this guard merely because its FAT entries are 32 bits.
The helper returns a Boolean. Failure can originate from:
- Cache initialization failure.
- An exception while pinning existing directory data.
- A fixed-root limit.
- Allocation/growth failure.
- A failure while initializing or updating new entry space.
These are not all disk-capacity exhaustion. Callers that translate any false result into STATUS_DISK_FULL lose the original cause. This distinction was decisive in issue #87: the volume had ample free space, yet directory scanning failed before allocation.
The older 27149596 scan passed two valid entries plus zero-filled unused slots, then failed on the next cache block. Source tracing observed cache initialization succeeding, followed by the false-return path; it did not reach the allocation-return probe. This is evidence of the scan/pin failure route, not proof that the allocator ran out of clusters. A non-null Context in that trace is not proof of a live reference or of use after free: the scan unpins the prior BCB and then passes the output slot to CcPinRead(), which replaces it on success; a raised failure exits through the exception handler before entry data is used.
#88 reuses the earlier unused slot. Its focused fixture covers zero tails, a zero hole before a later valid entry, 0xff tails, deleted entries, and a genuinely full directory with failed growth. Avoiding that unnecessary scan is a bounded fix. It does not establish that every second-block I/O failure or every Boolean-to-status translation is correct.
A logical file offset maps through the file's cluster chain. FATX16 and FATX32 have different entry widths and terminal/range checks. A full-width FATX32 next-cluster value must not be truncated to 16 bits.
Patch 0021 and patch 0029 respectively address entry width and bounded traversal. Metadata pins should be released before file-data I/O; retaining them across recursive I/O can exhaust the small cache. A traversal bound must reject cycles/out-of-range chains rather than scan forever.
Testing must include fragmented chains, page/cache-block crossings, terminal entries, malformed/cyclic chains and propagated metadata-read failures. Contiguous-file timing alone does not qualify these paths.
Observed compatibility edge — issue #89: the existing Forza HDD uses 0x0fffffff at a directory-chain end. The release FATX32 reader preserves all 32 bits and normalizes only values at or above 0xfffffff8, so it does not recognize that value as EOF. A matched Complex run uses the existing directory successfully. #88 avoids unnecessary traversal by reusing its earlier slots; it does not repair the chain contract. Do not globally apply a DOS 28-bit mask: that could destroy legitimate full-width FATX32 values. Qualify a specific compatibility rule separately.
| Quantity | Meaning | Must not be inferred from |
|---|---|---|
| Logical EOF | Bytes currently part of the file | Allocated capacity alone |
| Allocation size / backing chain | Reserved storage available to the file | EOF alone, particularly after overwrite |
| Valid-data length | Initialized/readable content boundary tracked by the cache/FCB | A successful allocation alone |
| Directory-entry first cluster | Start of persistent backing chain | An assumed in-memory seek cache |
| Cached seek state | Acceleration of chain traversal | Permanent storage identity |
The public patch 0040 fixture specifies this sequence: create/write a nonempty file, close, reopen with FILE_OVERWRITE_IF, observe EOF zero, extend EOF to its previous size, close/reopen, and read the prior bytes. It includes same-handle and reopened-zero-EOF cases plus an ordinary-open preservation control. Its comment identifies an independent synthetic reference observation of retained bytes; the public patch does not identify the reference firmware or publish a complete qualification record. The verified source contract below is the released implementation, rather than a general claim about another kernel.
Patch 0040 implements the independently specified behavior through VfatResetFileSizeForOverwrite():
- Ask
MmCanFileBeTruncated()whether resetting EOF is permitted; reject withSTATUS_USER_MAPPED_FILEwhen not. - Reset logical file size and valid-data length to zero.
- Preserve allocation, first cluster and seek state.
- Notify the cache of new sizes and update the directory entry.
- If updating metadata fails, restore the old file size, valid-data length, entry size, flags and cache sizes.
This is not a rule that every truncate, delete, or replacement preserves data. Explicit size/allocation changes still follow their own implementation.
Reopening a zero-EOF file can reconstruct an FCB with lost in-memory allocation size. VfatRestoreRetainedAllocation() handles the retained chain for non-directory FATX files. It skips ordinary nonzero-size cases and zero first-cluster cases, rejects invalid cluster geometry/values, bounds traversal by volume cluster count and arithmetic capacity, propagates FAT-read failures, and restores allocation and last-cluster/offset state only after successful traversal.
A successful save needs more than a successful create call or a displayed progress message:
- Parent directory exists and child paths resolve.
- Requested writes report correct completion status and byte counts.
- Metadata reflects the final EOF and backing chain.
- Flush/close does not endlessly retry failed write-back.
- Reopen returns the expected bytes.
- A fresh process or ordinary load can use the save.
Do not terminate the emulator while a title is still creating or signing its save. Directory entries and several completed files do not establish completion of the entire title transaction. A missing signature file may reflect an interrupted save. Wait for a defined title-level completion state, then close normally and validate by ordinary reopen. Fixed-duration menu input diagnostics and performance benchmarks have different completion contracts.
An emulator snapshot is not interchangeable with an ordinary game save. It restores process/device/memory state and can preserve stale input or pending operations. Warm reset and new-process boot are also different paths. Keep HDD, ROM memory layout, emulator memory setting, reset method and input route explicit during qualification.
A previously used HDD must remain part of compatibility coverage; passing only a fresh filesystem is insufficient. Preserve the original user image and test copies. Parser failure is not a corruption finding until its geometry, FAT width, terminal values and directory conventions are validated.
- Preserve specific cache/pin errors rather than masking all directory-space failures as disk full.
- Qualify failed and short write-back completion in explicit flush/last-close paths.
- Establish outstanding-BCB versus final cache-map teardown ownership.
- Complete usable-save/reopen and loading qualification under issue #87; merged slot reuse and bounded directory creation do not establish these outcomes.
- Keep clean-room observations separate from inferred commercial implementation details.
See Clean-Room-and-Validation for evidence levels and Home for the complete call reference.
Source baseline: SpiritBoot 27149596, Roswell 1569e2e8 plus ordered patches. See Clean-Room-and-Validation for evidence boundaries and Export-Index for complete export coverage.
- Retail-5838-BIOS-Size-and-Layout
- Retail-5838-Function-Ledger
- Retail-5838-Public-API-Comparison
- Retail-5838-Data-and-Physical-Atlas
- Retail-5838-Unclassified-Image-Ranges
- SpiritBoot-Main-Emitted-Extent-Ledger
- SpiritBoot-Main-Physical-Ownership
- SpiritBoot-Latest-Candidate-Extent-Ledger
- Memory-Reduction-Targets
- Memory-Comparison-and-Removal-Guide
- Memory-Three-BIOS-Public-API-Comparison
- Memory-Reduction-Workbook — physical-owner challenge ledger, game headroom, allocation pressure and removal decisions.
- Memory-Pool-Reuse-Cost-and-Attribution — current 64 KiB candidate saving, eight-run allocator costs and Conker attribution limits.
- Kernel-Overhaul-Size-and-Performance — lower-level subsystem specialization, runtime roots, performance work and focused PR delivery.