Skip to content

FATX and Save Contracts

Codex edited this page Oct 9, 2026 · 3 revisions

FATX directories, files, saves, and failure propagation

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.

The complete operation matters

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.

Directory entry states

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.

Finding directory space

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.

Forza observation and candidate scope

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.

FAT chains and metadata mappings

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.

EOF, allocation, and valid data are separate

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

FILE_OVERWRITE_IF compatibility

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():

  1. Ask MmCanFileBeTruncated() whether resetting EOF is permitted; reject with STATUS_USER_MAPPED_FILE when not.
  2. Reset logical file size and valid-data length to zero.
  3. Preserve allocation, first cluster and seek state.
  4. Notify the cache of new sizes and update the directory entry.
  5. 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.

Save qualification and reset

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.

Further work explicitly not claimed complete

  • 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.

SpiritBoot reference

Internal contracts

BIOS sizes and layouts

Resident memory research

Retail 5838 and current main sizing update — 2026-10-10

Memory removal workbook and kernel overhaul

Clone this wiki locally