]> FriiDump Source - friidump.git/commitdiff
FriiDump 0.5.3.15: close XGD1 geometry and lead-in capture test/xgd1-security-cache-probe-v1 v0.5.3.15-pf1
authorjtryba <jtryba2010@gmail.com>
Sat, 18 Jul 2026 17:56:40 +0000 (12:56 -0500)
committerjtryba <jtryba2010@gmail.com>
Sat, 18 Jul 2026 17:56:40 +0000 (12:56 -0500)
Promote the validated drive-captured 32-sector game lead-in, retain compact XGD1 closure evidence, correct executable-relative Redump DAT discovery, and remove transient probe artifacts.

27 files changed:
.gitignore
README
docs/ChangeLog
docs/README.technical
docs/XBOX.md
docs/evidence/xgd1/FRIIDUMP_XGD1_CLOSURE_EVIDENCE.json [new file with mode: 0644]
docs/evidence/xgd1/FRIIDUMP_XGD1_GEOMETRY_SAMPLES.csv [new file with mode: 0644]
docs/evidence/xgd1/FRIIDUMP_XGD1_V70_LIVE_SUMMARY.txt [new file with mode: 0644]
docs/evidence/xgd1/FRIIDUMP_XGD1_V70_MEDIA_EVIDENCE.json [new file with mode: 0644]
docs/evidence/xgd1/README.md [new file with mode: 0644]
docs/options
docs/reports/FRIIDUMP_XGD1_CACHE_ALIGNED_GEOMETRY_PROBE_0.5.3.13.md [new file with mode: 0644]
docs/reports/FRIIDUMP_XGD1_DRIVE_CAPTURED_GAME_LEADIN_0.5.3.14.md [new file with mode: 0644]
docs/reports/FRIIDUMP_XGD1_LIVE_CLOSURE_AND_RELEASE_CLEANUP_0.5.3.15.md [new file with mode: 0644]
docs/reports/FRIIDUMP_XGD1_LOGICAL_BOUNDARY_PROBE_0.5.3.11.md [new file with mode: 0644]
docs/reports/FRIIDUMP_XGD1_RAW_ID_GEOMETRY_PROBE_0.5.3.12.md [new file with mode: 0644]
libfriidump/CMakeLists.txt
libfriidump/dumper.c
libfriidump/xbox_ref/utils.c
libfriidump/xbox_ref_bridge.c
libfriidump/xbox_ref_bridge.h
libfriidump/xbox_xgd1_raw_id_probe.c [new file with mode: 0644]
msvc32_friidump.rsp
src/friidump.c
src/redump_dat.c
src/redump_dat.h
tests/test_redump_dat_resolver.c [new file with mode: 0644]

index 568cc4936ec39f8c94f71c177ea31a0fc0461dbe..419b1f95b2afc1f26cd9611c7549b0a812a41b14 100644 (file)
@@ -1,36 +1,36 @@
-# CMake and generic build output
-/CMakeCache.txt
-/CMakeFiles/
-/Makefile
-/cmake_install.cmake
-/install_manifest.txt
-/BUILD/
+# CMake and generated build trees
+CMakeCache.txt
+CMakeFiles
+Makefile
+cmake_install.cmake
+install_manifest.txt
 /build/
 /build-*/
-/out/
-
-# Windows/MSVC output
-/.vs/
 /build-msvc32/
-/friidump.exe
-/friidump.pdb
-/friidump.ilk
-/friidump.exp
-/friidump.lib
-/friidump_msvc32_build_results.zip
+
+# Compiler and executable outputs
 *.obj
+*.o
+*.a
+*.so
+*.dll
+*.exe
+*.pdb
+*.ilk
 
-# Runtime logs and generated media artifacts
+# Runtime logs and generated disc outputs at repository root
 /friidump.log
-*.log
-*.iso
-*.xiso
-*.raw
-*.pfi.bin
-*.dmi.bin
-*.redump.json
-*.media.json
-*.xdvd.json
+/*.iso
+/*.xiso
+/*.raw
+/*.iso.log
+/*.media.json
+/*.xdvd.json
+/*.pfi.bin
+/*.dmi.bin
+/*.ad_c0.bin
+/*_build_results.zip
 
-.DS_Store
-Thumbs.db
+# Local test/evidence bundles
+/*-live-test-*.zip
+/*-results.zip
diff --git a/README b/README
index 00bbfb86082a09273b426da05dba6b5a597284b0..a481527d4d38da594015dea1f206887da49abb6f 100644 (file)
--- a/README
+++ b/README
@@ -201,6 +201,22 @@ checked against DiscImageCreator's Original Xbox/XGD1 model: total size
 3,820,880 sectors, layer break LBA 1,913,776, DVD start PSN 0x30000, and
 Xbox/game start PSN 0x60600.
 
+FriiDump 0.5.3.11 adds a read-only evidence mode for the unresolved synthetic
+XGD1 ranges:
+
+```powershell
+friidump -d <drive> --xgd1-layout-probe xgd1-layout-probe.json
+```
+
+This mode creates no ISO. On the GDR-8050L challenge-handshake profile it
+establishes locked/video and unlocked/game states, reads selected boundary LBAs
+with READ(10) and READ(12), verifies the exact expected capacities, and writes
+an atomic JSON report containing SCSI status/sense evidence, SHA-1, nullable
+failed-read classifications, explicit command comparability, and the complete
+sector data for successful reads. The current pregame padding, postgame padding, and 32-sector
+game lead-in remain unresolved; probe success must not be represented as an
+exact Redump image.
+
 On a GDR-8050L, FriiDump follows the original dumper's state order and timing:
 primary handshake, media-cycle, re-handshake, RefreshVolume plus full settle
 delays, metadata/XBE probe, media-cycle back to the visible DVD-video view,
@@ -476,9 +492,9 @@ does the final handshake for game/XDVDFS data.
 
 FriiDump 0.5.3.9 also closes the Xbox live-summary boundary: the copied GDR-8050L path now returns title, media ID, finalized output sectors, and elapsed evidence to the final validation summary, and final STOP UNIT ownership is singular.
 
-FriiDump 0.5.3.10 checks each successfully completed ISO against the canonical
-Redump DAT selected for the detected or forced disc type. Verification begins
-only after CRC32, MD5, SHA-1, and SHA-256 have been finalized.
+FriiDump checks each successfully completed ISO against the canonical Redump
+DAT selected for the detected or forced disc type. Verification begins only
+after CRC32, MD5, SHA-1, and SHA-256 have been finalized.
 
 Stable DAT paths:
 
@@ -542,18 +558,22 @@ an incomplete object with missing string values.
 For the GDR-8050L/cross-flashed GDR-8163B full-ISO path, the report identifies
 `GDR-8050L reference finalized full-file hashes` as the hash source. FriiDump
 also records that XGD1 acquisition success and exact Redump hash identity are
-separate claims. The current reconstruction contains documented synthetic
-ranges, so a clean physical read can legitimately produce `NO EXACT MATCH`.
-That result must not be promoted to archive identity.
+separate claims. The current reconstruction contains unresolved zero-filled pregame and
+postgame spans, so a clean physical read can legitimately produce
+`NO EXACT MATCH`. That result must not be promoted to archive identity.
 
 Options:
 
-- `--redump-dat-dir <dir>` selects another directory containing the three stable
-  DAT filenames above.
+- `--redump-dat-dir <dir>` explicitly selects a directory containing the
+  three stable DAT filenames above. Without it, FriiDump checks `redump_dat`
+  beside the executable and then under the current working directory.
 - `--redump-report <file>` writes the atomic machine-readable evidence report.
 - `--no-redump-verify` disables DAT lookup for a run.
 - `--nohash` also prevents DAT verification because the required hashes are not
   calculated.
+- `--xgd1-layout-probe <file>` is a separate read-only Windows diagnostic for
+  the GDR-8050L challenge-handshake profile. It cannot be combined with image
+  output, conversion, all-methods, or HLDS 0xE7 probe options.
 
 Example:
 
@@ -568,5 +588,68 @@ Example:
 ```
 
 The DAT files are data inputs, not compiled into the executable. Replace the
-three files in `redump_dat` with newer Redump exports while keeping the stable
-filenames, or point `--redump-dat-dir` at an alternate set.
+three files in the executable-relative `redump_dat` directory with newer Redump
+exports while keeping the stable filenames, or point `--redump-dat-dir` at an
+alternate set. The current-working-directory lookup is retained as a fallback
+for source-tree and legacy workflows.
+
+FriiDump 0.5.3.12 adds a second read-only XGD1 evidence mode for the user's
+modified GDR-8050L firmware:
+
+  --xgd1-raw-id-probe <report.json>
+
+The probe establishes the locked/video and unlocked/game views, performs
+controlled 16-sector READ(12) cache fills, then uses the accepted HIT 0xE7
+memdump command at cache base 0x80000000 to capture the selected 2064-byte raw
+sector. The report records the raw ID field and decodes its 24-bit physical
+sector number. This is intended to test logical-to-physical geometry; it does
+not read inaccessible filler sectors, does not modify the current XGD1 image
+layout, and requires the modified 0xE7 firmware profile.
+
+The 0.5.3.11 live logical probe found 17 successful READ(10)/READ(12) pairs,
+all byte-identical. Both active capacities rejected the sampled out-of-range
+LBAs with 05/21/00. Therefore ordinary logical reads do not supply the current
+synthetic pregame/postgame filler bytes.
+
+FriiDump 0.5.3.13 corrects the raw-ID probe after the first live v68 run
+showed that arbitrary request LBAs could leave cache base 0x80000000 pointing
+at an earlier or block-start window. The corrected probe aligns every request
+to a 16-sector block, issues the proven zero-transfer READ(12) cache flush,
+and dumps only the target slot's 12-byte header and four-byte EDC. It validates
+layer bits and raw/normalized PSNs against the current full-output geometry.
+Raw-cache user-data equality is no longer treated as evidence because Method 8
+reconstructs that field from READ(12) data before unscrambling. Geometry may be
+resolved by a complete match; inaccessible filler bytes remain unresolved.
+
+FriiDump 0.5.3.14 drive-captured XGD1 game lead-in
+----------------------------------------------------
+
+The 0.5.3.13 live cache-aligned raw-ID run matched all 13 selected locked/video
+and unlocked/game samples. It proved that unlocked source LBA 0..31 occupies
+full-output LBA 198144..198175 and immediately precedes the XDVDFS header at
+unlocked source LBA 32. FriiDump therefore no longer synthesizes the 32-sector
+game lead-in for the GDR-8050L redump-style path.
+
+The reference Xbox path now reads unlocked source LBA 0..31 strictly before
+copying source LBA 32..3431263. The native libfriidump path uses the same source
+mapping and retains per-sector zero-fill only as an explicitly counted fallback
+for a genuinely unreadable lead-in sector. Pregame and postgame physical
+locations are resolved, but their inaccessible content remains zero-filled and
+must not be described as exact without independent evidence.
+
+
+FriiDump 0.5.3.15 release cleanup
+-----------------------------------
+
+The 0.5.3.15 release candidate consolidates the XGD1 investigation into a
+repository-ready tree. Local build logs, runner transcripts, package rehearsals,
+duplicate stdout/stderr captures, and full logical-sector byte dumps are not
+retained as source authority. Compact evidence is stored under
+`docs/evidence/xgd1`.
+
+Automatic DAT discovery is now executable-relative first, with the historical
+current-working-directory lookup retained as a fallback. An explicit
+`--redump-dat-dir` remains authoritative.
+
+The reference and native full-output mappings remain those validated in
+0.5.3.14. No XGD1 output bytes changed in this cleanup.
index d865b6b469ee4254bfedf07a6a493c318a9cc572..f893c7815650782e79638859565f6533082181e6 100644 (file)
@@ -1,3 +1,101 @@
+2026-07-18 - FriiDump 0.5.3.15 XGD1 live closure and release cleanup
+
+  * Closed the 0.5.3.14 hardware validation on a modified GDR-8050L revision
+    0012. The 3,820,880-sector output completed successfully and the
+    drive-captured 32-sector game lead-in matched the independent 0.5.3.13
+    geometry evidence at both boundaries.
+  * Recorded final output hashes: CRC32 cc5f0e71, MD5
+    1207b2090701ba1f221d486c83c863e7, SHA-1
+    5ea9b2262ba951afd9a5d2d44cd2b62a4d9decc7, and SHA-256
+    5995f823c6d67b9e3e3b3e1c2b54e1e036793188bec6b5be4220f559305f53b9.
+  * Confirmed NO EXACT MATCH against 2,689 Microsoft Xbox DAT entries. This
+    does not invalidate acquisition: the inaccessible pregame and postgame
+    contents remain zero-filled unresolved placeholders.
+  * Made automatic DAT discovery executable-relative first, with the historical
+    current-working-directory lookup retained as a fallback. Explicit
+    --redump-dat-dir remains authoritative.
+  * Corrected sidecar wording: hardware geometry validation is complete, while
+    filler content remains unresolved.
+  * Replaced 71 local build/probe/debug artifacts with a compact evidence
+    directory containing the final geometry samples, machine-readable closure
+    record, media-layout evidence, and completed live summary.
+  * Added a C resolver test and expanded .gitignore coverage for build outputs,
+    runtime logs, and generated disc images.
+  * No XGD1 output-layout bytes changed after the accepted 0.5.3.14 live dump.
+
+2026-07-18 - FriiDump 0.5.3.14 drive-captured XGD1 game lead-in
+
+  * Promoted the successful 0.5.3.13 live geometry result: all 13 selected
+    locked/video and unlocked/game raw-sector IDs matched the expected
+    full-output mapping.
+  * Replaced the GDR-8050L full-output synthetic game lead-in with drive reads
+    from unlocked source LBA 0..31 at output LBA 198144..198175.
+  * Kept XDVDFS/game data at unlocked source LBA 32..3431263 and output LBA
+    198176..3629407.
+  * Updated .redump.json, .media.json, and .xdvd.json layout evidence to record
+    the drive-captured lead-in source and the 0.5.3.13 physical-geometry proof.
+  * Pregame and postgame physical locations are now documented as geometry
+    verified, while their inaccessible content remains zero-filled and
+    unresolved.
+  * No claim of Redump identity is made until a new 0.5.3.14 full-disc dump is
+    hashed and compared with the canonical DAT.
+
+2026-07-18 - FriiDump 0.5.3.13 XGD1 cache-aligned raw-ID geometry correction
+
+  * Corrected the v68 raw-ID probe to mirror the proven modified-GDR-8050L
+    Method 8 cache contract: zero-transfer READ(12) cache flush, 16-sector
+    block-aligned streaming fill, and 2064-byte slot indexing.
+  * Removed raw-cache user-data equality as a geometry criterion. Method 8
+    reconstructs sectors from cache header/EDC plus READ(12) data before
+    unscrambling, so equality is not expected.
+  * Added explicit full-output LBA mapping, layer-bit decoding, 24-bit layer-1
+    PSN normalization, expected raw/normalized PSNs, and per-sample geometry
+    verdicts.
+  * Schema 2 reports now set physical_geometry_resolved only when all 13
+    selected locked/video and unlocked/game samples match exactly.
+  * Retained the v68 live run as negative/diagnostic evidence; it is not a
+    geometry promotion. No reconstruction bytes or filler policy changed.
+
+2026-07-18 - FriiDump 0.5.3.12 XGD1 raw-sector ID geometry probe
+
+  * Live validation of 0.5.3.11 confirmed that sampled READ(10) and READ(12)
+    sectors are identical in both locked/video and unlocked/game states.
+  * The locked view ends at LBA 6991 and the unlocked view ends at LBA
+    3431263; ordinary logical reads expose no alternate source for the
+    documented synthetic pregame or postgame ranges.
+  * Added --xgd1-raw-id-probe for modified GDR-8050L firmware with HIT 0xE7
+    memdump support. It performs controlled READ(12) cache fills, captures the
+    corresponding 2064-byte raw cache sectors at 0x80000000, and records the
+    decoded DVD physical-sector number from each raw ID field.
+  * XGD1 probe modes now measure real elapsed time and suppress the unrelated
+    HLDS GC/Wii validation summary that previously reported misleading
+    seed/dump/STOP fields for a successful diagnostic run.
+  * No reconstruction mapping changed. Raw IDs are geometry evidence only;
+    inaccessible filler bytes remain unresolved.
+
+2026-07-18 - FriiDump 0.5.3.11 read-only XGD1 logical-boundary probe
+
+- Adds `--xgd1-layout-probe <file>` for the GDR-8050L challenge-handshake
+  profile on Windows.
+- Establishes and verifies the locked/video and unlocked/game views using
+  READ CAPACITY(10), then probes selected XGD1 boundary LBAs with one-sector
+  READ(10) and READ(12) commands.
+- Records per-command success, Win32 error, SCSI status, sense key/ASC/ASCQ,
+  nullable nonzero-byte/all-zero fields for failed reads, SHA-1, complete
+  2048-byte sector data for successful reads, and explicit READ(10)/READ(12)
+  comparability in an atomic JSON report.
+- Includes the current output-layout constants and probes the visible video
+  boundaries, synthetic pregame/postgame boundaries, game lead-in/XDVDFS
+  boundary, output layer break mapping, and each reported-capacity edge.
+- Restores the entry locked/video state when the probe began locked; an
+  already-unlocked entry remains in the game view.
+- Keeps the existing 0.5.3.10 ISO/XISO output unchanged. The probe explicitly
+  leaves the pregame padding, postgame padding, and game lead-in unresolved
+  until live evidence supports a source mapping.
+- Uses only logical read commands, the established authentication handshake,
+  tray transitions, volume guard, and STOP UNIT cleanup. No firmware write,
+  flash, erase, 3B06, 3B07, or FLASHUP path was added or changed.
+
 2026-07-18 - FriiDump 0.5.3.10 state-aware GDR-8050L handshake cleanup
 
 - Replaces the copied reference bridge's unconditional unlock -> tray cycle ->
index 058de1600dee0790ec8dbb0beba1e156f064fe5f..c820e863d2e7c55a828c1a21c44c6b8a7d03b2ac 100644 (file)
@@ -315,9 +315,9 @@ media FriiDump writes this layout:
 
 - output LBA 0..6831: visible DVD-video L0, read from source LBA 0..6831;
 - output LBA 6832..198143: pregame padding, currently zero-filled;
-- output LBA 198144..198175: 32-sector game lead-in; FriiDump attempts to read
-  these sectors from the active unlocked/game view and zero-fills only unreadable
-  sectors;
+- output LBA 198144..198175: 32-sector game lead-in, read from unlocked source
+  LBA 0..31; the native libfriidump path zero-fills only a genuinely unreadable
+  sector and records the fallback count;
 - output LBA 198176..3629407: unlocked game/XDVDFS data, normally read from
   source LBA 32 on GDR-8050L, or from the detected XDVDFS source LBA on other
   supported profiles;
@@ -342,9 +342,10 @@ detected XDVDFS start LBA through LBA 1913920 and prepends a 32-sector game
 lead-in. For single-layer/homebrew-style media it treats the XDVDFS VolumeSize
 field as a byte count and converts it to 2048-byte sectors.
 
-Both Xbox output styles attempt to read their 32-sector game lead-in from the
-active drive view and zero-fill only sectors that are not readable through the
-selected drive/profile.
+Both Xbox output styles preserve the 32-sector game lead-in from unlocked
+source LBA 0..31. The GDR-8050L reference path fails the dump if this proven
+range cannot be captured; the native libfriidump path retains a counted
+per-sector zero-fill fallback for other profiles or transient read failures.
 
 
 Supported-drive notes
index 29c1f6ea5246fc04a13472a53bcb88065b69326a..40b17fbc4186f977b3cead1bc7b6764badf6e795 100644 (file)
@@ -70,11 +70,12 @@ When available, FriiDump also writes these files next to the ISO:
 - `<name>.redump.json` - JSON metadata describing the layout, hashes, drive
   profile, PFI/DMI filenames, and lead-in capture status.
 
-For the GDR-8050L redump path, the 32-sector game lead-in follows the original
-dumper's option-1 behavior and is written as zero-fill before copying unlocked
-XDVDFS data from source LBA `32`.  For non-8050L Xbox strategies, FriiDump still
-attempts to read lead-in sectors when the active drive/profile can expose them,
-and zero-fills only unreadable lead-in sectors.
+For the GDR-8050L redump path, the 32-sector game lead-in is captured from
+unlocked source LBA `0..31` before copying XDVDFS data from source LBA `32`.
+FriiDump 0.5.3.13 proved this placement with cache-aligned raw-sector IDs. The
+native libfriidump path retains per-sector zero-fill only for a genuinely
+unreadable lead-in sector and records the read/zero-fill counts; the reference
+Xbox path treats failure to capture the proven lead-in as a dump failure.
 
 FriiDump also records a `discimagecreator_reference` section in the JSON. The
 redump-style constants are checked against DiscImageCreator's Original
@@ -109,6 +110,36 @@ to per-sector zero-fill only where reads fail.
 `--xiso` is a separate output mode and cannot be combined with `-i`, `-r`, or
 `-a` in one run.
 
+## Read-only XGD1 logical-boundary probe
+
+FriiDump 0.5.3.11 adds a Windows-only diagnostic that does not create or alter
+an ISO:
+
+```powershell
+friidump -d f: --xgd1-layout-probe "docs\debug\xgd1-layout-probe.json"
+```
+
+The mode is currently restricted to the GDR-8050L challenge-handshake profile.
+It performs the following evidence sequence:
+
+1. Record the entry READ CAPACITY(10) state.
+2. Cycle the tray and verify the exact 6,992-sector locked/video capacity.
+3. Probe selected boundary LBAs with one-sector READ(10) and READ(12).
+4. Apply the established full Xbox handshake and verify the unlocked/game
+   capacity.
+5. Probe the corresponding unlocked-source and capacity-edge LBAs.
+6. Restore the locked/video entry state when the run began locked.
+7. Atomically publish the JSON report and issue STOP UNIT.
+
+Each command record includes the Win32 result, SCSI status, sense
+key/ASC/ASCQ, all-zero/nonzero classification, SHA-1, and full 2048-byte sector
+data when the read succeeds. Expected out-of-range failures are evidence and
+do not by themselves invalidate the report.
+
+This is a logical-read probe, not a physical-sector dumper. It does not assert
+that inaccessible filler is zero, does not replace the current synthetic
+ranges, and does not promote the reconstructed XGD1 image to Redump identity.
+
 ## GDR-8050L handshake sequence
 
 The GDR-8050L path performs this sequence when switching from visible DVD-video view
@@ -185,7 +216,7 @@ bare `-i` with XBE/DMI-derived names without hashing the 7.29 GiB file twice.
 Use `--redump-report <file>` to publish the same atomic schema-2 evidence record
 used by GameCube/Wii paths.
 
-The current XGD1 image map contains documented synthetic zero-filled ranges.
+The current XGD1 image map contains unresolved zero-filled pregame and postgame ranges.
 Accordingly, `NO EXACT MATCH` is a valid evidence result even when all readable
 sectors were acquired successfully. The verifier reports acquisition evidence
 and archive identity as separate claims. `-X` remains outside DAT verification
@@ -194,6 +225,39 @@ because XISO is not a full Redump disc image.
 GDR-3120L remains on the separate `FF 08 01` vendor lock/unlock path. Forced Xbox mode on non-supported Xbox unlock profiles is rejected rather than routed through FriiDump GC/Wii fallback methods.
 
 
+
+## XGD1 cache-aligned raw-ID geometry correction (0.5.3.13)
+
+The first 0.5.3.12 hardware report contained a mixture of valid physical IDs
+and stale/block-start cache entries. The corrected probe now performs the same
+zero-transfer READ(12) flush and 16-sector block alignment used by the proven
+modified-GDR-8050L Method 8 path. It dumps only the raw 12-byte header and
+four-byte EDC for the selected slot, decodes the layer bit, normalizes layer-1
+PSNs by 24-bit complement, and compares each sample with its mapped full-output
+LBA.
+
+The live 0.5.3.13 run matched all 13 selected samples and set
+`physical_geometry_resolved` true. The result proves the physical placement of
+the visible video ranges, the full unlocked game range, and the omitted
+pregame/postgame spans. It does not establish the byte content of the omitted
+filler spans.
+
+## Drive-captured XGD1 game lead-in (0.5.3.14)
+
+The successful 0.5.3.13 geometry report established these adjacent mappings:
+
+- unlocked source LBA `0..31` -> output LBA `198144..198175`, raw PSN
+  `0x060600..0x06061F`;
+- unlocked source LBA `32` -> output LBA `198176`, raw PSN `0x060620`, where
+  the `MICROSOFT*XBOX*MEDIA` XDVDFS descriptor begins.
+
+FriiDump 0.5.3.14 therefore replaces the historical synthetic 32-sector
+game-lead-in write with drive capture from source LBA `0..31`. This changes the
+full XGD1 output bytes and requires a new live full-disc dump plus Redump DAT
+comparison. The 191,312-sector pregame and postgame spans retain zero-filled
+placeholder content: their physical geometry is proven, but their inaccessible
+byte content is not.
+
 ## Logging
 
 The original Xbox reference logger is now the shared FriiDump run logger. Every
@@ -273,3 +337,48 @@ XDVD sidecar authentication evidence now distinguishes two separate facts:
 
 The compatibility field `requires_media_transition` is also `false`; it refers
 to authentication/unlock acceptance, not to restoring the locked video view.
+
+## XGD1 raw-sector ID geometry probe (0.5.3.12)
+
+Live 0.5.3.11 evidence closed the ordinary logical-read branch: all 17
+successful READ(10)/READ(12) pairs were identical, the locked view stopped at
+6991, and the unlocked view stopped at 3431263. Output-space LBAs outside
+those capacities returned ILLEGAL REQUEST / LBA OUT OF RANGE (05/21/00).
+This means neither READ opcode exposes a hidden logical source for the
+synthetic pregame or postgame ranges.
+
+`--xgd1-raw-id-probe <file>` is the next read-only geometry probe. On the
+modified GDR-8050L profile it fills the drive cache with a controlled 16-sector
+READ(12), dumps the selected 2064-byte raw sector through `E7 48 49 54 01`, and
+decodes bytes 1..3 of the raw DVD ID as the physical-sector number. Adjacent
+locked and unlocked boundary samples can establish whether the active logical
+views map linearly or jump across omitted physical regions.
+
+Raw physical IDs do not reveal the contents of inaccessible filler sectors.
+No zero-filled range may be promoted to exact merely because its geometry is
+confirmed.
+
+
+## 0.5.3.15 live closure
+
+The required 0.5.3.14 hardware run completed on the modified GDR-8050L
+revision `0012`.
+
+The drive-captured source LBA `0..31` lead-in was independently verified at
+output LBA `198144..198175`, and source LBA `32` remained the XDVDFS boundary at
+output LBA `198176`.
+
+Final output:
+
+- sectors: `3,820,880`;
+- bytes: `7,825,162,240`;
+- SHA-256:
+  `5995f823c6d67b9e3e3b3e1c2b54e1e036793188bec6b5be4220f559305f53b9`.
+
+The Microsoft Xbox DAT scan returned `NO EXACT MATCH`. Pregame and postgame
+geometry is proven, but their inaccessible content remains zero-filled and
+unresolved.
+
+Without `--redump-dat-dir`, DAT discovery checks the executable-relative
+`redump_dat` directory before the current working directory. This prevents
+runner working directories from hiding the packaged DAT set.
diff --git a/docs/evidence/xgd1/FRIIDUMP_XGD1_CLOSURE_EVIDENCE.json b/docs/evidence/xgd1/FRIIDUMP_XGD1_CLOSURE_EVIDENCE.json
new file mode 100644 (file)
index 0000000..df5c415
--- /dev/null
@@ -0,0 +1,347 @@
+{
+  "schema": "project-frankenstein-friidump-xgd1-closure.v1",
+  "generated_utc": "2026-07-18T17:00:00Z",
+  "software": {
+    "friidump_version_under_test": "0.5.3.14",
+    "cleanup_release_candidate": "0.5.3.15",
+    "base_commit_before_cleanup": "6e43cd3fd4f7324a177331a01d653b4c19c4bca7"
+  },
+  "hardware": {
+    "drive_model": "HL-DT-ST DVD-ROM GDR8050L",
+    "firmware_revision": "0012",
+    "modified_firmware_e7_memdump": true
+  },
+  "logical_boundary_probe": {
+    "locked_visible_sectors": 6992,
+    "unlocked_visible_sectors": 3431264,
+    "read10_read12_successful_pairs": 17,
+    "read10_read12_all_equal": true,
+    "ordinary_reads_expose_pregame_or_postgame_filler": false
+  },
+  "physical_geometry_probe": {
+    "schema_version": 2,
+    "required_states_verified": true,
+    "geometry_validation_failures": 0,
+    "physical_geometry_resolved": true,
+    "geometry_model": {
+      "dvd_start_psn": 196608,
+      "game_output_start_lba": 198144,
+      "layer_break_lba": 1913776,
+      "layer_zero_last_normalized_psn": 2110383,
+      "layer_one_raw_psn_is_24bit_complement": true
+    },
+    "sample_count": 13,
+    "samples": [
+      {
+        "state": "locked_video",
+        "lba": 0,
+        "label": "locked_logical_start",
+        "cache_fill_lba": 0,
+        "cache_index": 0,
+        "layer_number": 0,
+        "physical_sector_number": 196608,
+        "normalized_physical_sector_number": 196608,
+        "output_lba": 0,
+        "expected_layer_number": 0,
+        "expected_physical_sector_number": 196608,
+        "expected_normalized_physical_sector_number": 196608,
+        "geometry_matches_expected": true,
+        "logical_sha1": "605db3fdbaff4ba13729371ad0c4fbab3889378e",
+        "raw_header_hex": "20030000d6f5000000000000",
+        "raw_edc_hex": "0560250d"
+      },
+      {
+        "state": "locked_video",
+        "lba": 31,
+        "label": "locked_early_lba_31",
+        "cache_fill_lba": 16,
+        "cache_index": 15,
+        "layer_number": 0,
+        "physical_sector_number": 196639,
+        "normalized_physical_sector_number": 196639,
+        "output_lba": 31,
+        "expected_layer_number": 0,
+        "expected_physical_sector_number": 196639,
+        "expected_normalized_physical_sector_number": 196639,
+        "geometry_matches_expected": true,
+        "logical_sha1": "605db3fdbaff4ba13729371ad0c4fbab3889378e",
+        "raw_header_hex": "2003001ff7cb000000000000",
+        "raw_edc_hex": "83903453"
+      },
+      {
+        "state": "locked_video",
+        "lba": 32,
+        "label": "locked_early_lba_32",
+        "cache_fill_lba": 32,
+        "cache_index": 0,
+        "layer_number": 0,
+        "physical_sector_number": 196640,
+        "normalized_physical_sector_number": 196640,
+        "output_lba": 32,
+        "expected_layer_number": 0,
+        "expected_physical_sector_number": 196640,
+        "expected_normalized_physical_sector_number": 196640,
+        "geometry_matches_expected": true,
+        "logical_sha1": "3e4eb33f37c052df89b0ccc4e239828c3b9556aa",
+        "raw_header_hex": "20030020b6b5000000000000",
+        "raw_edc_hex": "0b23613f"
+      },
+      {
+        "state": "locked_video",
+        "lba": 6831,
+        "label": "video_l0_last",
+        "cache_fill_lba": 6816,
+        "cache_index": 15,
+        "layer_number": 0,
+        "physical_sector_number": 203439,
+        "normalized_physical_sector_number": 203439,
+        "output_lba": 6831,
+        "expected_layer_number": 0,
+        "expected_physical_sector_number": 203439,
+        "expected_normalized_physical_sector_number": 203439,
+        "geometry_matches_expected": true,
+        "logical_sha1": "36661a56261adccc33185e383fb022c0e36da109",
+        "raw_header_hex": "20031aaf7cea000000000000",
+        "raw_edc_hex": "cf771b52"
+      },
+      {
+        "state": "locked_video",
+        "lba": 6832,
+        "label": "video_l1_first",
+        "cache_fill_lba": 6832,
+        "cache_index": 0,
+        "layer_number": 1,
+        "physical_sector_number": 16573776,
+        "normalized_physical_sector_number": 203439,
+        "output_lba": 3820720,
+        "expected_layer_number": 1,
+        "expected_physical_sector_number": 16573776,
+        "expected_normalized_physical_sector_number": 203439,
+        "geometry_matches_expected": true,
+        "logical_sha1": "605db3fdbaff4ba13729371ad0c4fbab3889378e",
+        "raw_header_hex": "01fce550135b000000000000",
+        "raw_edc_hex": "d8734d6e"
+      },
+      {
+        "state": "locked_video",
+        "lba": 6991,
+        "label": "locked_video_last",
+        "cache_fill_lba": 6976,
+        "cache_index": 15,
+        "layer_number": 1,
+        "physical_sector_number": 16573935,
+        "normalized_physical_sector_number": 203280,
+        "output_lba": 3820879,
+        "expected_layer_number": 1,
+        "expected_physical_sector_number": 16573935,
+        "expected_normalized_physical_sector_number": 203280,
+        "geometry_matches_expected": true,
+        "logical_sha1": "775869f6db440ccfd5f1cc826a725c745ffda9d4",
+        "raw_header_hex": "01fce5efcf38000000000000",
+        "raw_edc_hex": "84c7dc79"
+      },
+      {
+        "state": "unlocked_game",
+        "lba": 0,
+        "label": "unlocked_source_start",
+        "cache_fill_lba": 0,
+        "cache_index": 0,
+        "layer_number": 0,
+        "physical_sector_number": 394752,
+        "normalized_physical_sector_number": 394752,
+        "output_lba": 198144,
+        "expected_layer_number": 0,
+        "expected_physical_sector_number": 394752,
+        "expected_normalized_physical_sector_number": 394752,
+        "geometry_matches_expected": true,
+        "logical_sha1": "22815399cde9e12d539d64cec38f332830994614",
+        "raw_header_hex": "6e060600deb0000000000000",
+        "raw_edc_hex": "60ae4fb1"
+      },
+      {
+        "state": "unlocked_game",
+        "lba": 31,
+        "label": "unlocked_leadin_last",
+        "cache_fill_lba": 16,
+        "cache_index": 15,
+        "layer_number": 0,
+        "physical_sector_number": 394783,
+        "normalized_physical_sector_number": 394783,
+        "output_lba": 198175,
+        "expected_layer_number": 0,
+        "expected_physical_sector_number": 394783,
+        "expected_normalized_physical_sector_number": 394783,
+        "geometry_matches_expected": true,
+        "logical_sha1": "b9aee89323d1fa0071c13d736b3536251a4773d2",
+        "raw_header_hex": "6e06061fff8e000000000000",
+        "raw_edc_hex": "e873673b"
+      },
+      {
+        "state": "unlocked_game",
+        "lba": 32,
+        "label": "xdfs_source_start",
+        "cache_fill_lba": 32,
+        "cache_index": 0,
+        "layer_number": 0,
+        "physical_sector_number": 394784,
+        "normalized_physical_sector_number": 394784,
+        "output_lba": 198176,
+        "expected_layer_number": 0,
+        "expected_physical_sector_number": 394784,
+        "expected_normalized_physical_sector_number": 394784,
+        "geometry_matches_expected": true,
+        "logical_sha1": "827a963f0b7db9749bf127525c45f84cf18ef666",
+        "raw_header_hex": "6e060620bef0000000000000",
+        "raw_edc_hex": "5f747f7e"
+      },
+      {
+        "state": "unlocked_game",
+        "lba": 33,
+        "label": "xdfs_source_next",
+        "cache_fill_lba": 32,
+        "cache_index": 1,
+        "layer_number": 0,
+        "physical_sector_number": 394785,
+        "normalized_physical_sector_number": 394785,
+        "output_lba": 198177,
+        "expected_layer_number": 0,
+        "expected_physical_sector_number": 394785,
+        "expected_normalized_physical_sector_number": 394785,
+        "geometry_matches_expected": true,
+        "logical_sha1": "cdda4c9956920f00e23cea7a48a4e7a74244b565",
+        "raw_header_hex": "6e060621bdf2000000000000",
+        "raw_edc_hex": "fe1f7ff9"
+      },
+      {
+        "state": "unlocked_game",
+        "lba": 1715631,
+        "label": "mapped_layer_break_source_minus_one",
+        "cache_fill_lba": 1715616,
+        "cache_index": 15,
+        "layer_number": 0,
+        "physical_sector_number": 2110383,
+        "normalized_physical_sector_number": 2110383,
+        "output_lba": 1913775,
+        "expected_layer_number": 0,
+        "expected_physical_sector_number": 2110383,
+        "expected_normalized_physical_sector_number": 2110383,
+        "geometry_matches_expected": true,
+        "logical_sha1": "ad29c120fb69e05dceec32ef76674d8d4c38c739",
+        "raw_header_hex": "6e2033af66b4000000000000",
+        "raw_edc_hex": "8fc60c80"
+      },
+      {
+        "state": "unlocked_game",
+        "lba": 1715632,
+        "label": "mapped_layer_break_source",
+        "cache_fill_lba": 1715632,
+        "cache_index": 0,
+        "layer_number": 1,
+        "physical_sector_number": 14666832,
+        "normalized_physical_sector_number": 2110383,
+        "output_lba": 1913776,
+        "expected_layer_number": 1,
+        "expected_physical_sector_number": 14666832,
+        "expected_normalized_physical_sector_number": 2110383,
+        "geometry_matches_expected": true,
+        "logical_sha1": "99b55f2cc7da36729f1597a5f6b28131e8be6417",
+        "raw_header_hex": "4fdfcc500905000000000000",
+        "raw_edc_hex": "f420ba4b"
+      },
+      {
+        "state": "unlocked_game",
+        "lba": 3431263,
+        "label": "unlocked_source_last",
+        "cache_fill_lba": 3431248,
+        "cache_index": 15,
+        "layer_number": 1,
+        "physical_sector_number": 16382463,
+        "normalized_physical_sector_number": 394752,
+        "output_lba": 3629407,
+        "expected_layer_number": 1,
+        "expected_physical_sector_number": 16382463,
+        "expected_normalized_physical_sector_number": 394752,
+        "geometry_matches_expected": true,
+        "logical_sha1": "de52cf1b22c636d9f360e4faeaad55271dff459d",
+        "raw_header_hex": "4ff9f9ffb101000000000000",
+        "raw_edc_hex": "79dfff25"
+      }
+    ]
+  },
+  "drive_captured_game_leadin": {
+    "unlocked_source_start_lba": 0,
+    "sector_count": 32,
+    "output_start_lba": 198144,
+    "output_end_lba": 198175,
+    "nonzero_bytes": 65266,
+    "sha1": "e5cd93fe0f7c11c93878d2cb9d909de1c333978c",
+    "sha256": "0ada0cffb6fe9ff506955cb2a16a7f33a4ff9ae8360e9c352a4fcc843abf5af2",
+    "first_sector_sha1": "22815399cde9e12d539d64cec38f332830994614",
+    "last_sector_sha1": "b9aee89323d1fa0071c13d736b3536251a4773d2",
+    "xdfs_output_lba": 198176,
+    "xdfs_first_sector_sha1": "827a963f0b7db9749bf127525c45f84cf18ef666",
+    "xdfs_magic": "MICROSOFT*XBOX*MEDIA",
+    "source": "drive_read10",
+    "verified": true
+  },
+  "full_output": {
+    "title": "Red Faction II",
+    "media_id": "TQ00501A",
+    "sector_size": 2048,
+    "sector_count": 3820880,
+    "byte_count": 7825162240,
+    "hashes": {
+      "crc32": "cc5f0e71",
+      "md5": "1207b2090701ba1f221d486c83c863e7",
+      "sha1": "5ea9b2262ba951afd9a5d2d44cd2b62a4d9decc7",
+      "sha256": "5995f823c6d67b9e3e3b3e1c2b54e1e036793188bec6b5be4220f559305f53b9"
+    },
+    "layout": {
+      "layout_type": "original_xbox_xgd1_redump_style_2048",
+      "layers": 2,
+      "layer_break_lba": 1913776,
+      "video_l0_start_lba": 0,
+      "video_l0_sector_count": 6832,
+      "pregame_padding_start_lba": 6832,
+      "pregame_padding_sector_count": 191312,
+      "game_output_start_lba": 198144,
+      "game_leadin_unlocked_source_start_lba": 0,
+      "game_leadin_source_sector_count": 32,
+      "game_leadin_source": "drive_read10",
+      "game_unlocked_source_start_lba": 32,
+      "game_unlocked_source_sector_count": 3431232,
+      "game_xiso_leadin_sector_count": 32,
+      "xdfs_volume_lba_within_game_region": 32,
+      "game_sector_count": 3431264,
+      "postgame_padding_start_lba": 3629408,
+      "postgame_padding_sector_count": 191312,
+      "video_l1_start_lba": 3820720,
+      "video_l1_sector_count": 160,
+      "legacy_contiguous_visible_sector_count": 198144,
+      "drive_locked_visible_sector_count": 6992,
+      "drive_reported_unlocked_sector_count": 3431264,
+      "reconstructed_output_sector_count": 3820880
+    }
+  },
+  "redump_verification": {
+    "dat_name": "Microsoft - Xbox.dat",
+    "dat_sha256": "e7a76ba732b7b978148e93ecd92befd3b59ef8e850eca10241390d405f7f4116",
+    "entries_scanned": 2689,
+    "status": "NO MATCH",
+    "overall": "NO EXACT MATCH",
+    "exact_matches": 0,
+    "size_matches": 2626,
+    "crc32_matches": 0,
+    "md5_matches": 0,
+    "sha1_matches": 0,
+    "detail": "No entry matched all required fields and no hash-correlated candidate was found."
+  },
+  "evidence_boundary": {
+    "pregame_physical_geometry_resolved": true,
+    "postgame_physical_geometry_resolved": true,
+    "pregame_content_resolved": false,
+    "postgame_content_resolved": false,
+    "current_filler_policy": "zero-filled unresolved placeholder",
+    "exact_redump_identity_established": false
+  }
+}
diff --git a/docs/evidence/xgd1/FRIIDUMP_XGD1_GEOMETRY_SAMPLES.csv b/docs/evidence/xgd1/FRIIDUMP_XGD1_GEOMETRY_SAMPLES.csv
new file mode 100644 (file)
index 0000000..3b4c19f
--- /dev/null
@@ -0,0 +1,14 @@
+state,lba,label,cache_fill_lba,cache_index,layer_number,physical_sector_number,normalized_physical_sector_number,output_lba,expected_layer_number,expected_physical_sector_number,expected_normalized_physical_sector_number,geometry_matches_expected,logical_sha1,raw_header_hex,raw_edc_hex
+locked_video,0,locked_logical_start,0,0,0,196608,196608,0,0,196608,196608,True,605db3fdbaff4ba13729371ad0c4fbab3889378e,20030000d6f5000000000000,0560250d
+locked_video,31,locked_early_lba_31,16,15,0,196639,196639,31,0,196639,196639,True,605db3fdbaff4ba13729371ad0c4fbab3889378e,2003001ff7cb000000000000,83903453
+locked_video,32,locked_early_lba_32,32,0,0,196640,196640,32,0,196640,196640,True,3e4eb33f37c052df89b0ccc4e239828c3b9556aa,20030020b6b5000000000000,0b23613f
+locked_video,6831,video_l0_last,6816,15,0,203439,203439,6831,0,203439,203439,True,36661a56261adccc33185e383fb022c0e36da109,20031aaf7cea000000000000,cf771b52
+locked_video,6832,video_l1_first,6832,0,1,16573776,203439,3820720,1,16573776,203439,True,605db3fdbaff4ba13729371ad0c4fbab3889378e,01fce550135b000000000000,d8734d6e
+locked_video,6991,locked_video_last,6976,15,1,16573935,203280,3820879,1,16573935,203280,True,775869f6db440ccfd5f1cc826a725c745ffda9d4,01fce5efcf38000000000000,84c7dc79
+unlocked_game,0,unlocked_source_start,0,0,0,394752,394752,198144,0,394752,394752,True,22815399cde9e12d539d64cec38f332830994614,6e060600deb0000000000000,60ae4fb1
+unlocked_game,31,unlocked_leadin_last,16,15,0,394783,394783,198175,0,394783,394783,True,b9aee89323d1fa0071c13d736b3536251a4773d2,6e06061fff8e000000000000,e873673b
+unlocked_game,32,xdfs_source_start,32,0,0,394784,394784,198176,0,394784,394784,True,827a963f0b7db9749bf127525c45f84cf18ef666,6e060620bef0000000000000,5f747f7e
+unlocked_game,33,xdfs_source_next,32,1,0,394785,394785,198177,0,394785,394785,True,cdda4c9956920f00e23cea7a48a4e7a74244b565,6e060621bdf2000000000000,fe1f7ff9
+unlocked_game,1715631,mapped_layer_break_source_minus_one,1715616,15,0,2110383,2110383,1913775,0,2110383,2110383,True,ad29c120fb69e05dceec32ef76674d8d4c38c739,6e2033af66b4000000000000,8fc60c80
+unlocked_game,1715632,mapped_layer_break_source,1715632,0,1,14666832,2110383,1913776,1,14666832,2110383,True,99b55f2cc7da36729f1597a5f6b28131e8be6417,4fdfcc500905000000000000,f420ba4b
+unlocked_game,3431263,unlocked_source_last,3431248,15,1,16382463,394752,3629407,1,16382463,394752,True,de52cf1b22c636d9f360e4faeaad55271dff459d,4ff9f9ffb101000000000000,79dfff25
diff --git a/docs/evidence/xgd1/FRIIDUMP_XGD1_V70_LIVE_SUMMARY.txt b/docs/evidence/xgd1/FRIIDUMP_XGD1_V70_LIVE_SUMMARY.txt
new file mode 100644 (file)
index 0000000..d23ac3a
--- /dev/null
@@ -0,0 +1,27 @@
+FriiDump XGD1 drive-captured lead-in live closure
+Result: PASS
+Validated FriiDump version: 0.5.3.14
+Drive: HL-DT-ST DVD-ROM GDR8050L revision 0012
+Output sectors: 3820880
+Output bytes: 7825162240
+CRC32: cc5f0e71
+MD5: 1207b2090701ba1f221d486c83c863e7
+SHA-1: 5ea9b2262ba951afd9a5d2d44cd2b62a4d9decc7
+SHA-256: 5995f823c6d67b9e3e3b3e1c2b54e1e036793188bec6b5be4220f559305f53b9
+Lead-in unlocked source LBA: 0..31
+Lead-in output LBA: 198144..198175
+Lead-in nonzero bytes: 65266
+Lead-in SHA-1: e5cd93fe0f7c11c93878d2cb9d909de1c333978c
+Lead-in SHA-256: 0ada0cffb6fe9ff506955cb2a16a7f33a4ff9ae8360e9c352a4fcc843abf5af2
+Lead-in first sector SHA-1: 22815399cde9e12d539d64cec38f332830994614
+Lead-in last sector SHA-1: b9aee89323d1fa0071c13d736b3536251a4773d2
+XDVDFS output LBA: 198176
+XDVDFS first sector SHA-1: 827a963f0b7db9749bf127525c45f84cf18ef666
+XDVDFS magic: MICROSOFT*XBOX*MEDIA
+DAT: Microsoft - Xbox.dat
+DAT SHA-256: e7a76ba732b7b978148e93ecd92befd3b59ef8e850eca10241390d405f7f4116
+DAT entries scanned: 2689
+DAT status: NO EXACT MATCH
+Exact matches: 0
+Pregame content: unresolved zero-filled placeholder
+Postgame content: unresolved zero-filled placeholder
diff --git a/docs/evidence/xgd1/FRIIDUMP_XGD1_V70_MEDIA_EVIDENCE.json b/docs/evidence/xgd1/FRIIDUMP_XGD1_V70_MEDIA_EVIDENCE.json
new file mode 100644 (file)
index 0000000..ec395c7
--- /dev/null
@@ -0,0 +1,57 @@
+{
+  "format": "xdvd-media-profile",
+  "version": 1,
+  "media_id": "TQ00501A",
+  "title_hint": "Red_Faction_II",
+  "image": {
+    "sector_size": 2048,
+    "sector_count": 3820880,
+    "byte_count": 7825162240,
+    "hashes": {
+      "crc32": "cc5f0e71",
+      "md5": "1207b2090701ba1f221d486c83c863e7",
+      "sha1": "5ea9b2262ba951afd9a5d2d44cd2b62a4d9decc7",
+      "sha256": "5995f823c6d67b9e3e3b3e1c2b54e1e036793188bec6b5be4220f559305f53b9"
+    }
+  },
+  "layout": {
+    "layout_type": "original_xbox_xgd1_redump_style_2048",
+    "layers": 2,
+    "layer_break_lba": 1913776,
+    "video_l0_start_lba": 0,
+    "video_l0_sector_count": 6832,
+    "pregame_padding_start_lba": 6832,
+    "pregame_padding_sector_count": 191312,
+    "game_output_start_lba": 198144,
+    "game_leadin_unlocked_source_start_lba": 0,
+    "game_leadin_source_sector_count": 32,
+    "game_leadin_source": "drive_read10",
+    "game_unlocked_source_start_lba": 32,
+    "game_unlocked_source_sector_count": 3431232,
+    "game_xiso_leadin_sector_count": 32,
+    "xdfs_volume_lba_within_game_region": 32,
+    "game_sector_count": 3431264,
+    "postgame_padding_start_lba": 3629408,
+    "postgame_padding_sector_count": 191312,
+    "video_l1_start_lba": 3820720,
+    "video_l1_sector_count": 160,
+    "legacy_contiguous_visible_sector_count": 198144,
+    "drive_locked_visible_sector_count": 6992,
+    "drive_reported_unlocked_sector_count": 3431264,
+    "reconstructed_output_sector_count": 3820880
+  },
+  "reconstruction": {
+    "is_reconstructed_layout": true,
+    "filler_policy": "pregame_and_postgame_zero_fill_content_placeholder",
+    "filler_geometry_verified": true,
+    "filler_verified_from_disc": false,
+    "pending_hardware_capture": false,
+    "unresolved_filler_content": true,
+    "note": "Pregame and postgame physical locations are verified; inaccessible contents remain unresolved. The 32-sector game lead-in is drive-captured."
+  },
+  "validation": {
+    "byte_count_matches_sector_count": true,
+    "lead_in_mapping_verified": true,
+    "redump_exact_match": false
+  }
+}
diff --git a/docs/evidence/xgd1/README.md b/docs/evidence/xgd1/README.md
new file mode 100644 (file)
index 0000000..d4f7f08
--- /dev/null
@@ -0,0 +1,31 @@
+# XGD1 geometry and drive-captured lead-in evidence
+
+This directory contains the compact, repository-appropriate evidence retained
+from the FriiDump 0.5.3.11 through 0.5.3.14 investigation.
+
+Retained:
+
+- the 13 cache-aligned physical-sector geometry samples;
+- the consolidated machine-readable closure record;
+- the completed 0.5.3.14 full-output and Redump comparison summary;
+- a path-sanitized media/layout evidence record.
+
+Not retained:
+
+- local build logs;
+- runner transcripts;
+- package checksum rehearsals;
+- duplicate stdout/stderr captures;
+- full logical-sector byte dumps;
+- the 7.29 GiB ISO;
+- PFI, DMI, AD C0, or other disc-specific binary payloads.
+
+Evidence conclusion:
+
+- unlocked source LBA `0..31` is real drive-readable content and maps to
+  full-output LBA `198144..198175`;
+- unlocked source LBA `32` begins XDVDFS at output LBA `198176`;
+- pregame and postgame physical geometry is resolved;
+- inaccessible pregame and postgame contents remain unresolved zero-filled
+  placeholders;
+- no exact Microsoft Xbox DAT identity was established.
index b39d26830932abc9ce1348c38c1068def8f78332..b106e85fc7c89a2687245561f25b9df98e8fb316 100644 (file)
@@ -1,4 +1,4 @@
-FriiDump 0.5.3.2 - Copyright (C) 2007 Arep
+FriiDump 0.5.3.15 - Copyright (C) 2007 Arep
 This software comes with ABSOLUTELY NO WARRANTY.
 This is free software, and you are welcome to redistribute it
 under certain conditions; see COPYING for details.
@@ -36,21 +36,24 @@ Available command line options:
                                3 - DVD
                                4 - Xbox/XGD 2048-byte-sector mode
                                    Native profiles: GDR-8050L and GDR-3120L.
-                                   Forced Xbox mode requires a supported Xbox
-                                   unlock profile; no GC/Wii fallback is used.
+                                   Other drives keep normal FriiDump behavior
+                                   unless Xbox mode is explicitly forced.
  -S, --size <sectors>          Force disc size
  -r, --raw <file>              Output to file <file> in raw format (2064-byte
                                sectors)
- -i, --iso <file>              Output to file <file> in ISO format (2048-byte
-                               sectors). For Xbox/XGD this reconstructs the
-                               redump-style XGD1 layout and writes .pfi.bin,
-                               .dmi.bin, and .redump.json metadata when possible
- -X, --xiso <file>             Output Xbox/XGD game partition as XISO (.xiso).
-                               Attempts to read the 32-sector game lead-in
-                               from drive-readable sectors and zero-fills only
-                               unreadable lead-in sectors. Use -i for
-                               redump-style ISO + PFI/DMI/JSON; use -X only
-                               for game-partition XISO.
+ -i, --iso[=<file>]            Output to file <file> in ISO format (2048-byte
+                               sectors). For Xbox/GDR-8050L, omitting <file>
+                               derives Title[MediaID].iso from the XBE/DMI;
+                               providing <file> is an explicit override. For
+                               Xbox/XGD this reconstructs the redump-style
+                               XGD1 layout and writes .pfi.bin, .dmi.bin, and
+                               .redump.json metadata when possible
+ -X, --xiso[=<file>]   Output Xbox/XGD game partition as XISO (.xiso).
+                               For Xbox/GDR-8050L, omitting <file> derives
+                               Title[MediaID].xiso from the XBE/DMI; providing
+                               <file> is an explicit override. Attempts to read
+                               the 32-sector game lead-in from drive-readable
+                               sectors and zero-fills only unreadable sectors.
  -u, --unscramble <file>       Convert (unscramble) raw image contained in
                                <file> to ISO format
  -H, --nohash                  Do not compute CRC32/MD5/SHA-1/SHA-256 hashes
@@ -77,14 +80,23 @@ Available command line options:
  -9, --method9                 Use dumping method 9 (Read and dump 5 blocks
                                at a time, using streaming read, using DMA and
                                some speed tricks)
+     --hlds-e7-scan            Probe HLDS HIT 0xE7 cache/memdump bases only;
+                               writes JSON and does not crack seeds or dump data
+     --hlds-e7-subcmd-sweep    Probe HIT 0xE7 subcommands/address candidates only;
+                               writes JSON and does not crack seeds or dump data
+     --hlds-e7-memrange-sweep  Sweep wider HIT 0xE7 subcmd 0x01 address ranges;
+                               writes JSON and does not crack seeds or dump data
+     --scan-log <file>         JSON output path for --hlds-e7-scan
+     --scan-dump-prefix <prefix>       Optional raw 0xE7 window dump prefix for --hlds-e7-scan
+     --hlds-profile-report <file> Write selected HLDS profile/evidence JSON
+     --redump-dat-dir <dir> Directory containing canonical Redump DAT files
+                              (default: executable-relative redump_dat, then current directory)
+     --redump-report <file>   Write atomic Redump evidence JSON
+     --no-redump-verify       Disable automatic post-dump DAT verification
+     --xgd1-layout-probe <file> Read-only locked/unlocked XGD1 boundary probe;
+                              writes atomic JSON and does not create an ISO
+     --xgd1-raw-id-probe <file> Modified-firmware cache-flushed, block-aligned raw-ID probe;
+                              maps logical LBAs to decoded physical sector IDs
  -A, --allmethods              Try all known command/method combinations until
                                one works. Reopens the drive for each command so
                                command-specific vendor handlers are rebound.
-
-Xbox dump notes:
- - GDR-8050L redump-style -i follows the original dumper order:
-   primary handshake, media-cycle, re-handshake, metadata/auth probe,
-   media-cycle back to visible DVD-video view, capture video L0/L1, final
-   handshake, zero-fill the 32-sector game lead-in, read game data, then write
-   .pfi.bin, .dmi.bin, and .redump.json.
- - FriiDump sends STOP UNIT / spin-down after dump attempts, success or failure.
diff --git a/docs/reports/FRIIDUMP_XGD1_CACHE_ALIGNED_GEOMETRY_PROBE_0.5.3.13.md b/docs/reports/FRIIDUMP_XGD1_CACHE_ALIGNED_GEOMETRY_PROBE_0.5.3.13.md
new file mode 100644 (file)
index 0000000..78a1a92
--- /dev/null
@@ -0,0 +1,60 @@
+# FriiDump 0.5.3.13 XGD1 cache-aligned raw-ID geometry probe
+
+## Objective
+
+Correct the v68 raw-ID acquisition contract and test the current XGD1
+logical-to-physical output mapping with the same cache discipline used by the
+proven modified-GDR-8050L Method 8 path.
+
+## Why v68 was not promotable
+
+The v68 hardware run succeeded at issuing commands, but its raw-ID table mixed
+valid physical IDs with stale or block-start cache entries. It issued streaming
+READ(12) at arbitrary LBAs without the zero-transfer cache flush and without
+aligning the request to the 16-sector block represented at cache base
+`0x80000000`. It also treated raw-cache user-data equality as meaningful even
+though Method 8 reconstructs the raw sector from cache header/EDC plus READ(12)
+user data before unscrambling.
+
+## Corrected capture sequence
+
+For every selected logical LBA:
+
+1. Align the cache-fill LBA downward to a 16-sector boundary.
+2. Issue READ(12) with byte 1 set to `0x08` and zero transfer length to reset the
+   cache mapping.
+3. Issue a 16-sector streaming READ(12) at the aligned block start.
+4. Retain the requested logical sector from the returned 2048-byte data.
+5. Dump only the selected cache slot's 12-byte ID/IED/CPR_MAI header and
+   four-byte EDC using `E7 48 49 54 01`.
+6. Decode layer number from sector-information bit 0 and decode the 24-bit raw
+   physical-sector number.
+7. Normalize layer-1 PSNs by 24-bit complement and compare each sample to the
+   full reconstructed-output geometry.
+
+## Geometry model under test
+
+- DVD start PSN: `0x030000`.
+- Full output layer break: LBA `1,913,776`.
+- Layer-0 last normalized PSN: `0x2033AF`.
+- Unlocked/game view maps to full output beginning at LBA `198,144`.
+- Locked LBAs `0..6831` map to output video L0.
+- Locked LBAs `6832..6991` map to output LBAs `3,820,720..3,820,879`.
+- Layer-1 raw PSNs are compared in their 24-bit complemented representation.
+
+The probe reports `physical_geometry_resolved=true` only when both states are
+verified and all 13 selected samples match the expected layer and PSN exactly.
+A mismatch is preserved in the atomic JSON report and causes a nonzero exit.
+
+## Interpretation boundary
+
+A full geometry match validates placement of readable video and game sectors in
+the current reconstructed output. It does not reveal inaccessible pregame or
+postgame filler sectors and does not establish their byte values. Filler-content
+claims remain false.
+
+## Safety
+
+The probe uses readiness, tray, authentication, READ CAPACITY, cache-flush
+READ(12), streaming READ(12), and HIT 0xE7 data-in reads. It issues no firmware
+write, flash, erase, 3B06, 3B07, or FLASHUP command and creates no ISO.
diff --git a/docs/reports/FRIIDUMP_XGD1_DRIVE_CAPTURED_GAME_LEADIN_0.5.3.14.md b/docs/reports/FRIIDUMP_XGD1_DRIVE_CAPTURED_GAME_LEADIN_0.5.3.14.md
new file mode 100644 (file)
index 0000000..3658701
--- /dev/null
@@ -0,0 +1,73 @@
+# FriiDump 0.5.3.14 drive-captured XGD1 game lead-in
+
+## Evidence basis
+
+The 0.5.3.13 live cache-aligned raw-ID probe completed successfully on the
+modified `HL-DT-ST DVD-ROM GDR8050L` revision `0012`. Both required drive states
+were established, all 13 selected samples matched the expected layer and raw
+physical-sector number, independent validation reported zero failures, and the
+report set `physical_geometry_resolved` to true.
+
+The decisive adjacent samples were:
+
+| Unlocked source LBA | Output LBA | Raw PSN | Meaning |
+|---:|---:|---:|---|
+| 0 | 198144 | `0x060600` | first game-region lead-in sector |
+| 31 | 198175 | `0x06061F` | last game-region lead-in sector |
+| 32 | 198176 | `0x060620` | XDVDFS descriptor sector |
+| 33 | 198177 | `0x060621` | next XDVDFS sector |
+
+This closes the earlier ambiguity around the 32-sector region. It is not
+synthetic padding: it is a readable physical range exposed by the unlocked
+GDR-8050L view.
+
+## 0.5.3.14 behavior
+
+The redump-style full-output path now writes:
+
+- unlocked source LBA `0..31` to output LBA `198144..198175`;
+- unlocked source LBA `32..3431263` to output LBA `198176..3629407`.
+
+The Windows reference path treats failure to capture LBA `0..31` as a dump
+failure. The native libfriidump path uses the existing per-sector read/fallback
+helper and records `readable_sectors` and `zero_filled_sectors` in
+`.redump.json`.
+
+The metadata sidecars now distinguish:
+
+- drive-captured game lead-in;
+- geometry-verified pregame/postgame spans;
+- unresolved pregame/postgame content that remains zero-filled.
+
+## Evidence boundary
+
+This update does not claim the pregame or postgame zero-filled bytes are exact.
+The 0.5.3.13 proof establishes where those omitted physical spans lie, not their
+inaccessible contents.
+
+## Live closure
+
+The required full-disc 0.5.3.14 run completed on the modified GDR-8050L
+revision `0012`.
+
+Observed output:
+
+- sectors: `3,820,880`;
+- bytes: `7,825,162,240`;
+- CRC32: `cc5f0e71`;
+- MD5: `1207b2090701ba1f221d486c83c863e7`;
+- SHA-1: `5ea9b2262ba951afd9a5d2d44cd2b62a4d9decc7`;
+- SHA-256:
+  `5995f823c6d67b9e3e3b3e1c2b54e1e036793188bec6b5be4220f559305f53b9`.
+
+The lead-in at output LBA `198144..198175` contained `65,266` nonzero bytes.
+Its first and last sector SHA-1 values matched the independent 0.5.3.13 source
+LBA `0` and source LBA `31` evidence. The XDVDFS descriptor at output LBA
+`198176` also matched source LBA `32` and contained
+`MICROSOFT*XBOX*MEDIA`.
+
+The Microsoft Xbox DAT comparison scanned `2,689` entries and returned
+`NO EXACT MATCH`: `2,626` size matches and zero CRC32, MD5, or SHA-1 matches.
+No archive identity is established. The unresolved pregame and postgame
+placeholder contents remain the documented reason that a clean acquisition can
+differ from a canonical full-disc image.
diff --git a/docs/reports/FRIIDUMP_XGD1_LIVE_CLOSURE_AND_RELEASE_CLEANUP_0.5.3.15.md b/docs/reports/FRIIDUMP_XGD1_LIVE_CLOSURE_AND_RELEASE_CLEANUP_0.5.3.15.md
new file mode 100644 (file)
index 0000000..9bb1191
--- /dev/null
@@ -0,0 +1,59 @@
+# FriiDump 0.5.3.15 XGD1 live closure and release cleanup
+
+## Scope
+
+This release candidate consolidates the accepted 0.5.3.11 through 0.5.3.14
+XGD1 investigation into a repository-ready state. It does not change the
+0.5.3.14 full-output mapping.
+
+## Proven behavior
+
+The modified GDR-8050L revision `0012` established:
+
+- locked/video visible capacity: `6,992` sectors;
+- unlocked/game visible capacity: `3,431,264` sectors;
+- all 17 successful paired READ(10)/READ(12) samples were byte-identical;
+- all 13 cache-aligned raw-sector-ID samples matched the expected physical
+  geometry;
+- unlocked source LBA `0..31` maps to output LBA `198144..198175`;
+- unlocked source LBA `32` begins XDVDFS at output LBA `198176`.
+
+The completed 0.5.3.14 full-output run independently verified the first and last
+lead-in sectors, the XDVDFS boundary, all sidecar mappings, and finalized output
+hashes.
+
+## Evidence boundary
+
+The pregame and postgame physical locations are resolved. Their inaccessible
+contents are not. FriiDump continues to write zero-filled placeholders for
+those two spans and records that limitation in sidecars and Redump evidence.
+
+The Microsoft Xbox DAT comparison returned `NO EXACT MATCH`. Acquisition
+success and archive identity remain separate claims.
+
+## Repository cleanup
+
+The development packages contained local CMake logs, package rehearsals,
+stdout/stderr captures, runner transcripts, and duplicate raw probe reports.
+Those files are not source authority.
+
+The cleaned tree retains:
+
+- the implementation and diagnostic probe commands;
+- four engineering reports for versions 0.5.3.11 through 0.5.3.14;
+- a compact machine-readable XGD1 closure record;
+- the 13 geometry samples;
+- path-sanitized 0.5.3.14 media-layout evidence;
+- the completed live summary;
+- a C test for DAT path resolution.
+
+## DAT discovery correction
+
+Without `--redump-dat-dir`, FriiDump now checks:
+
+1. `redump_dat` beside the running executable;
+2. `redump_dat` under the current working directory.
+
+An explicit `--redump-dat-dir` remains the first and authoritative choice.
+This prevents a valid packaged DAT set from being missed merely because a test
+runner launches FriiDump from another directory.
diff --git a/docs/reports/FRIIDUMP_XGD1_LOGICAL_BOUNDARY_PROBE_0.5.3.11.md b/docs/reports/FRIIDUMP_XGD1_LOGICAL_BOUNDARY_PROBE_0.5.3.11.md
new file mode 100644 (file)
index 0000000..0ccca91
--- /dev/null
@@ -0,0 +1,94 @@
+# FriiDump 0.5.3.11 XGD1 logical-boundary probe
+
+## Objective
+
+Collect direct, read-only evidence about the XGD1 ranges that FriiDump
+0.5.3.10 still represents with documented synthetic zero-fill. This update
+does not change the reconstructed ISO layout.
+
+## Command
+
+```powershell
+friidump -d f: --xgd1-layout-probe "docs\debug\xgd1-layout-probe.json"
+```
+
+The option is exclusive with image output, conversion, `--allmethods`, and the
+HLDS 0xE7 probe modes.
+
+## State sequence
+
+1. Record entry READ CAPACITY(10).
+2. Cycle the tray to establish the locked/video view.
+3. Probe locked/video boundary LBAs using one-sector READ(10) and READ(12).
+4. Apply the existing GDR-8050L full authentication handshake.
+5. Verify the unlocked/game READ CAPACITY(10) state.
+6. Probe unlocked-source and capacity-edge LBAs using READ(10) and READ(12).
+7. Restore the entry locked/video state when required.
+8. Atomically publish the JSON report.
+9. Return to FriiDump for one STOP UNIT cleanup.
+
+## Evidence captured per command
+
+- command success;
+- Win32 error;
+- SCSI status;
+- sense key, ASC, and ASCQ;
+- count of nonzero bytes for a successful read, otherwise `null`;
+- all-zero classification for a successful read, otherwise `null`;
+- SHA-1 of a successful 2048-byte sector;
+- complete successful sector bytes as hexadecimal;
+- explicit READ(10)/READ(12) comparability and equality for the same LBA;
+- exact 6,992-sector locked/video and 3,431,264-sector unlocked/game state
+  verification.
+
+## Probe points
+
+The locked/video state includes the logical start, LBA 31/32, VIDEO-L0 end,
+locked VIDEO-L1 start/end, locked-capacity edge, synthetic pregame boundary,
+game-output start, output layer break, synthetic postgame boundary, VIDEO-L1
+output start, and full-output end.
+
+The unlocked/game state includes source LBA 0/31/32/33, the synthetic-gap
+length, output-boundary values interpreted as unlocked source LBAs, the mapped
+output-layer-break source, the final reported game-view sector, and
+out-of-range capacity/output candidates.
+
+## Interpretation boundary
+
+A successful logical read proves only that the active firmware state exposes
+that LBA through ordinary READ(10) or READ(12). A failed read is also useful
+sense evidence. Neither result alone proves the physical mastering contents of
+an inaccessible XGD1 filler range.
+
+The report therefore fixes these fields to false until later analysis:
+
+- `pregame_padding_resolved`
+- `postgame_padding_resolved`
+- `game_leadin_resolved`
+
+## Safety
+
+No firmware update, write, flash, erase, vendor-memory write, 3B06, 3B07, or
+FLASHUP command is used. The probe uses readiness checks, tray transitions,
+the already-established authentication handshake, READ CAPACITY, logical
+READ(10)/READ(12), a Windows volume guard, atomic report publication, and STOP
+UNIT cleanup.
+
+## Live validation result (2026-07-18)
+
+The cross-flashed GDR-8163B running modified GDR-8050L 0012 firmware passed
+the probe. Entry state was the 3,431,264-sector game view. A tray cycle
+established the exact 6,992-sector locked/video view, the full challenge
+handshake restored the exact game view, and the run ended in its original
+game-view state.
+
+Seventeen sampled LBAs were readable in-range. READ(10) and READ(12) returned
+identical 2048-byte sectors for every successful pair. Eleven sampled
+out-of-range LBAs failed under both commands with sense 05/21/00. Locked LBA
+6992 was rejected, and unlocked LBA 3431264 was rejected.
+
+This closes the ordinary logical-read branch: there is no second READ(12)
+window and no numeric output-LBA path to the synthetic pregame or postgame
+regions. Unlocked LBA 0..31 is directly readable and nonzero, while LBA 32
+contains `MICROSOFT*XBOX*MEDIA`; exact physical placement still requires raw
+sector-ID geometry evidence.
diff --git a/docs/reports/FRIIDUMP_XGD1_RAW_ID_GEOMETRY_PROBE_0.5.3.12.md b/docs/reports/FRIIDUMP_XGD1_RAW_ID_GEOMETRY_PROBE_0.5.3.12.md
new file mode 100644 (file)
index 0000000..20019b6
--- /dev/null
@@ -0,0 +1,58 @@
+# FriiDump 0.5.3.12 XGD1 raw-sector ID geometry probe
+
+## Objective
+
+Use the modified GDR-8050L HIT 0xE7 memory-dump surface to recover the raw DVD
+ID fields associated with selected logical LBAs. The 24-bit physical-sector
+number in each raw ID can test the logical-to-physical geometry that ordinary
+READ(10)/READ(12) could not resolve.
+
+## Command
+
+```powershell
+friidump -d f: --xgd1-raw-id-probe "docs\debug\xgd1-raw-id-probe.json"
+```
+
+This mode is Windows-only, exclusive with all image-output and other probe
+modes, limited to the GDR-8050L challenge-handshake path, and additionally
+requires the modified HIT 0xE7 profile.
+
+## Capture sequence
+
+1. Record entry capacity.
+2. Tray-cycle to the 6,992-sector locked/video state.
+3. For selected locked LBAs, issue a 16-sector streaming READ(12), retain the
+   target 2048-byte logical sector, and dump the corresponding 2064-byte raw
+   cache slot from base 0x80000000.
+4. Apply the existing challenge handshake and verify the 3,431,264-sector game
+   state.
+5. Repeat at selected unlocked LBAs, including source start, XDFS start, mapped
+   layer-break neighbors, and source end.
+6. Restore the entry state when needed, atomically publish JSON, and issue one
+   STOP UNIT.
+
+## Evidence per sample
+
+- logical LBA, cache-fill LBA, cache slot, and absolute memory address;
+- READ(12) and HIT 0xE7 command status plus sense;
+- full logical 2048-byte sector and full raw 2064-byte cache sector;
+- SHA-1 for logical, raw, and raw-user-data regions;
+- raw-user/logical equality;
+- raw 12-byte ID/IED/CPR_MAI header and four-byte EDC;
+- sector-information byte;
+- decoded 24-bit physical-sector number.
+
+## Interpretation boundary
+
+A valid raw ID can prove geometry or a discontinuity between logical views. It
+does not make omitted filler sectors readable and does not establish their
+bytes. `physical_geometry_resolved`, `pregame_filler_content_resolved`, and
+`postgame_filler_content_resolved` remain false in the generated report until
+the captured values are analyzed.
+
+## Safety
+
+The probe issues readiness, tray, authentication, READ CAPACITY, READ(12), and
+HIT 0xE7 data-in memory reads only. It does not issue firmware updates, flash,
+erase, vendor-memory writes, 3B06, 3B07, or FLASHUP commands and does not create
+an ISO.
index 1356bab9f93bd56dd864210fabc8ba692b6282e0..0670c8712cc9c73b09f8b3dcde9be090639901d3 100644 (file)
@@ -33,6 +33,7 @@ add_library (
        win32compat.c
        xbox_ref_bridge.h
        xbox_ref_bridge.c
+       xbox_xgd1_raw_id_probe.c
        xbox_ref/xbox_ref_log.c
 )
 
index 36ac682aeacbd2322884bae58773ea1888d028d0..52795358a73672c06b41832b54d2be56a0a88dee 100644 (file)
@@ -321,6 +321,10 @@ static void xbox_write_redump_metadata (dumper *dmp, xbox_redump_metadata *meta)
        fprintf (json, "    \"pregame_padding_sector_count\": %u,\n", XBOX_XGD1_GAME_OUTPUT_START_LBA - XBOX_XGD1_VIDEO_L0_SECTORS);
        fprintf (json, "    \"game_output_start_lba\": %u,\n", XBOX_XGD1_GAME_OUTPUT_START_LBA);
        fprintf (json, "    \"game_leadin_sector_count\": %u,\n", XBOX_XISO_LEADIN_SECTORS);
+       fprintf (json, "    \"game_leadin_unlocked_source_start_lba\": %u,\n",
+               (meta -> game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ?
+               meta -> game_source_lba - XBOX_XISO_LEADIN_SECTORS : 0);
+       fprintf (json, "    \"game_leadin_source_policy\": \"drive_read10_with_per_sector_zero_fallback\",\n");
        fprintf (json, "    \"game_unlocked_source_start_lba\": %u,\n", meta -> game_source_lba);
        fprintf (json, "    \"game_unlocked_source_sector_count\": %u,\n", meta -> game_source_sectors);
        fprintf (json, "    \"postgame_padding_start_lba\": %u,\n", XBOX_XGD1_GAME_OUTPUT_START_LBA + XBOX_XGD1_UNLOCKED_GAME_VIEW_SECTORS);
@@ -339,8 +343,13 @@ static void xbox_write_redump_metadata (dumper *dmp, xbox_redump_metadata *meta)
        fprintf (json, "  },\n");
        fprintf (json, "  \"lead_in_capture\": {\n");
        fprintf (json, "    \"game_leadin_attempted\": true,\n");
+       fprintf (json, "    \"unlocked_source_start_lba\": %u,\n",
+               (meta -> game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ?
+               meta -> game_source_lba - XBOX_XISO_LEADIN_SECTORS : 0);
+       fprintf (json, "    \"sector_count\": %u,\n", XBOX_XISO_LEADIN_SECTORS);
        fprintf (json, "    \"readable_sectors\": %u,\n", meta -> game_leadin_read_sectors);
-       fprintf (json, "    \"zero_filled_sectors\": %u\n", meta -> game_leadin_zero_sectors);
+       fprintf (json, "    \"zero_filled_sectors\": %u,\n", meta -> game_leadin_zero_sectors);
+       fprintf (json, "    \"physical_geometry_evidence\": \"friidump_0.5.3.13_cache_aligned_raw_id_probe\"\n");
        fprintf (json, "  },\n");
        fprintf (json, "  \"dvd_structures\": {\n");
        fprintf (json, "    \"pfi\": "); if (pfi_leaf) xbox_json_string (json, pfi_leaf); else fprintf (json, "null"); fprintf (json, ",\n");
@@ -348,7 +357,8 @@ static void xbox_write_redump_metadata (dumper *dmp, xbox_redump_metadata *meta)
        fprintf (json, "  },\n");
        fprintf (json, "  \"notes\": [\n");
        fprintf (json, "    \"PFI and DMI are stored as separate binary READ DVD STRUCTURE captures because they are not READ(10) user-data sectors.\",\n");
-       fprintf (json, "    \"Pregame and postgame padding are zero-filled unless a future workflow proves readable source sectors for those ranges.\"\n");
+       fprintf (json, "    \"The 32-sector game lead-in is captured from unlocked source LBA 0..31; only unreadable lead-in sectors use zero-fill fallback.\",\n");
+       fprintf (json, "    \"Pregame and postgame padding retain zero-filled placeholder content; their physical locations are resolved but their inaccessible bytes are not.\"\n");
        fprintf (json, "  ]\n");
        fprintf (json, "}\n");
        fclose (json);
@@ -1064,21 +1074,19 @@ static int dumper_dump_xbox_redump_iso (dumper *dmp, u_int32_t *current_sector)
        if (game_source_lba != XBOX_XISO_STANDARD_GAME_LBA && disc_is_xbox_challenge_drive (dmp -> dsk))
                warning ("Xbox XDVDFS was detected at LBA %u, not the expected GDR-8050L game-view LBA 32", game_source_lba);
 
-       if (disc_is_xbox_challenge_drive (dmp -> dsk)) {
-               /* Match the original GDR-8050L dumper's option-1 redump path: the
-                * 32-sector XISO/game lead-in area is synthetic zero-fill, and the real
-                * XDVDFS data begins at unlocked source LBA 32. */
-               xbox_ref_log_fprintf (stderr, "[XBOX] Writing 32-sector game lead-in as zero-fill for GDR-8050L redump flow.\n");
-               meta.game_leadin_zero_sectors += XBOX_XISO_LEADIN_SECTORS;
-               if (current_sector) *current_sector = XBOX_XGD1_GAME_OUTPUT_START_LBA;
-               if (!xbox_write_zero_iso_range (dmp, XBOX_XISO_LEADIN_SECTORS, XBOX_XGD1_GAME_OUTPUT_START_LBA, meta.output_sectors, "GAME-XISO-LEADIN"))
-                       goto cleanup;
-       } else {
-               if (!xbox_write_leadin_iso_range (dmp,
-                               (game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ? game_source_lba - XBOX_XISO_LEADIN_SECTORS : 0,
-                               XBOX_XGD1_GAME_OUTPUT_START_LBA, meta.output_sectors, &meta, "redump game", true, current_sector))
-                       goto cleanup;
-       }
+       /* FriiDump 0.5.3.13 cache-aligned raw-ID validation proved that the
+        * unlocked GDR-8050L view exposes source LBA 0..31 as the physical
+        * game-region lead-in immediately preceding the XDVDFS header at LBA 32.
+        * Read those sectors from the drive; retain the existing per-sector
+        * zero-fill fallback only for genuinely unreadable sectors. */
+       xbox_ref_log_fprintf (stderr,
+               "[XBOX] Capturing 32-sector game lead-in from unlocked source LBA %u..%u.\n",
+               (game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ? game_source_lba - XBOX_XISO_LEADIN_SECTORS : 0,
+               (game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ? game_source_lba - 1 : XBOX_XISO_LEADIN_SECTORS - 1);
+       if (!xbox_write_leadin_iso_range (dmp,
+                       (game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ? game_source_lba - XBOX_XISO_LEADIN_SECTORS : 0,
+                       XBOX_XGD1_GAME_OUTPUT_START_LBA, meta.output_sectors, &meta, "redump game", true, current_sector))
+               goto cleanup;
 
        if (!xbox_dump_read10_iso_range (dmp, game_source_lba, XBOX_XGD1_GAME_SOURCE_SECTORS,
                        XBOX_XGD1_GAME_OUTPUT_START_LBA + XBOX_XISO_LEADIN_SECTORS, meta.output_sectors, "GAME-XDVDFS", true, current_sector))
index 3cf28114b01b0df5f00cc14de1003f7e4e6a4cb1..c63b03bedb6aef1afa5d739592ecd0f2cedefac6 100644 (file)
@@ -1981,7 +1981,7 @@ static void JsonWriteValidationWarnings(FILE *f, const XboxDvdSidecarCapture *ca
     if (!payloadFilesPresent)
         WRITE_WARNING("payload_files_not_fully_present");
     if (redumpStyleZeroFilledPadding)
-        WRITE_WARNING("redump_style_padding_zero_filled_pending_hardware_capture");
+        WRITE_WARNING("redump_style_padding_zero_filled_unresolved_content");
 
 #undef WRITE_WARNING
 
@@ -2476,6 +2476,9 @@ static BOOL WriteXboxDvdMediaProfileFile(const char *isoFilename,
         fprintf(json, "    \"pregame_padding_start_lba\": %u,\n", redumpStyle ? XGD1_VIDEO_L0_SECTORS : 0U);
         fprintf(json, "    \"pregame_padding_sector_count\": %u,\n", redumpStyle ? (XGD1_GAME_OUTPUT_START_LBA - XGD1_VIDEO_L0_SECTORS) : 0U);
         fprintf(json, "    \"game_output_start_lba\": %u,\n", gameOutputLba);
+        fprintf(json, "    \"game_leadin_unlocked_source_start_lba\": %u,\n", redumpStyle ? 0U : gameSourceLba);
+        fprintf(json, "    \"game_leadin_source_sector_count\": %u,\n", redumpStyle ? XGD1_XISO_LEADIN_SECTORS : 0U);
+        fprintf(json, "    \"game_leadin_source\": "); JsonWriteEscapedString(json, redumpStyle ? "drive_read10" : "not_applicable"); fprintf(json, ",\n");
         fprintf(json, "    \"game_unlocked_source_start_lba\": %u,\n", gameSourceLba);
         fprintf(json, "    \"game_unlocked_source_sector_count\": %u,\n", redumpStyle ? XGD1_GAME_SOURCE_SECTORS : gameSectors);
         fprintf(json, "    \"game_xiso_leadin_sector_count\": %u,\n", redumpStyle ? XGD1_XISO_LEADIN_SECTORS : 0U);
@@ -2494,10 +2497,12 @@ static BOOL WriteXboxDvdMediaProfileFile(const char *isoFilename,
 
     fprintf(json, "  \"reconstruction\": {\n");
     fprintf(json, "    \"is_reconstructed_layout\": %s,\n", redumpStyle ? "true" : "false");
-    fprintf(json, "    \"filler_policy\": "); JsonWriteEscapedString(json, redumpStyle ? "zero_fill_until_drive_can_read_filler" : "not_applicable"); fprintf(json, ",\n");
+    fprintf(json, "    \"filler_policy\": "); JsonWriteEscapedString(json, redumpStyle ? "pregame_and_postgame_zero_fill_content_placeholder" : "not_applicable"); fprintf(json, ",\n");
+    fprintf(json, "    \"filler_geometry_verified\": %s,\n", redumpStyle ? "true" : "false");
     fprintf(json, "    \"filler_verified_from_disc\": false,\n");
     fprintf(json, "    \"filler_byte_value\": %s,\n", redumpStyle ? "0" : "null");
-    fprintf(json, "    \"pending_hardware_capture\": %s,\n", redumpStyle ? "true" : "false");
+    fprintf(json, "    \"pending_hardware_capture\": false,\n");
+    fprintf(json, "    \"unresolved_filler_content\": %s,\n", redumpStyle ? "true" : "false");
     fprintf(json, "    \"filler_ranges\": [\n");
     if (redumpStyle)
     {
@@ -2515,7 +2520,7 @@ static BOOL WriteXboxDvdMediaProfileFile(const char *isoFilename,
         fprintf(json, "      }\n");
     }
     fprintf(json, "    ],\n");
-    fprintf(json, "    \"note\": \"Padding/filler ranges are intentionally zero-filled placeholders until a drive/workflow capable of reading those regions is available.\"\n");
+    fprintf(json, "    \"note\": \"Pregame and postgame physical locations are verified by cache-aligned raw sector IDs, but their inaccessible contents remain zero-filled placeholders. The 32-sector game lead-in is drive-captured from unlocked LBA 0..31.\"\n");
     fprintf(json, "  },\n");
 
     fprintf(json, "  \"dvd_structures\": {\n");
@@ -2628,6 +2633,9 @@ static BOOL WriteXboxDvdSidecarFiles(const char *isoFilename,
         fprintf(json, "      \"pregame_padding_start_lba\": %u,\n", redumpStyle ? XGD1_VIDEO_L0_SECTORS : 0U);
         fprintf(json, "      \"pregame_padding_sector_count\": %u,\n", redumpStyle ? (XGD1_GAME_OUTPUT_START_LBA - XGD1_VIDEO_L0_SECTORS) : 0U);
         fprintf(json, "      \"game_output_start_lba\": %u,\n", gameOutputLba);
+        fprintf(json, "      \"game_leadin_unlocked_source_start_lba\": %u,\n", redumpStyle ? 0U : gameSourceLba);
+        fprintf(json, "      \"game_leadin_source_sector_count\": %u,\n", redumpStyle ? XGD1_XISO_LEADIN_SECTORS : 0U);
+        fprintf(json, "      \"game_leadin_source\": "); JsonWriteEscapedString(json, redumpStyle ? "drive_read10" : "not_applicable"); fprintf(json, ",\n");
         fprintf(json, "      \"game_unlocked_source_start_lba\": %u,\n", gameSourceLba);
         fprintf(json, "      \"game_unlocked_source_sector_count\": %u,\n", redumpStyle ? XGD1_GAME_SOURCE_SECTORS : gameSectors);
         fprintf(json, "      \"game_xiso_leadin_sector_count\": %u,\n", redumpStyle ? XGD1_XISO_LEADIN_SECTORS : 0U);
@@ -2941,20 +2949,28 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin
                 printf("[WARN] XDFS was detected at unlocked source LBA %u, not the expected XISO header LBA 32.\n", detectedXdfsLba);
             }
 
-            // Build the Redump-style XISO/game region using the same proven
-            // convention as option 2: the first 32 sectors are synthetic XISO
-            // lead-in/padding, and the real XDFS volume begins at source LBA 32.
-            // Do not read unlocked source LBA 0..31 here; on the 8050L path those
-            // LBAs are not the XDFS header sectors we want in the rebuilt image.
+            // FriiDump 0.5.3.13 cache-aligned raw-ID validation proved that
+            // unlocked source LBA 0..31 is the physical 32-sector game-region
+            // lead-in and that XDVDFS begins at source LBA 32. Preserve the
+            // drive-captured lead-in instead of synthesizing zero sectors.
             gameSourceLba = XGD1_GAME_SOURCE_START_LBA;
             gameSectors = REDUMP_SECTORS;
             rawVideoSectors = XGD1_GAME_OUTPUT_START_LBA;
             rawGameSourceLba = gameSourceLba;
             rawGameSectors = gameSectors;
 
-            printf("[RAW] Writing %u-sector XISO lead-in/padding at output LBA %u.\n",
-                   XGD1_XISO_LEADIN_SECTORS, XGD1_GAME_OUTPUT_START_LBA);
-            if (!WriteZeroSectorsOutput(outFile, hHash, XGD1_XISO_LEADIN_SECTORS, XGD1_GAME_OUTPUT_START_LBA, "GAME-XISO-LEADIN"))
+            printf("[RAW] Capturing %u-sector game lead-in from unlocked source LBA 0..%u at output LBA %u.\n",
+                   XGD1_XISO_LEADIN_SECTORS,
+                   XGD1_XISO_LEADIN_SECTORS - 1,
+                   XGD1_GAME_OUTPUT_START_LBA);
+            if (!DumpSectorRangeWithRetry(hDevice,
+                                          outFile,
+                                          hHash,
+                                          0,
+                                          XGD1_XISO_LEADIN_SECTORS,
+                                          XGD1_GAME_OUTPUT_START_LBA,
+                                          "GAME-XISO-LEADIN",
+                                          TRUE))
             {
                 VirtualFree(videoL1Buffer, 0, MEM_RELEASE);
                 goto cleanup;
index 663805d10f0e04d8c842e2569124734a5c57382b..33e987c69c33fb1bcc4c35341b0f6b886e251ddf 100644 (file)
@@ -13,8 +13,12 @@ void xbox_ref_dump_result_init(xbox_ref_dump_result *result)
 #define _WIN32_WINNT 0x0500
 #include <windows.h>
 #include <winioctl.h>
+#include <ntddscsi.h>
 #include <stdio.h>
+#include <stddef.h>
+#include <io.h>
 #include "xbox_ref/utils.h"
+#include "xbox_ref/sha1.h"
 #include "xbox_ref/unlock.h"
 #include "xbox_ref/xbox_ref_log.h"
 
@@ -27,6 +31,16 @@ static BOOL xbox_ref_capacity_is_game_view(uint32_t sectors)
     return sectors > XBOX_REF_LOCKED_VIDEO_VIEW_MAX_SECTORS;
 }
 
+static BOOL xbox_ref_capacity_is_xgd1_locked_view(uint32_t sectors)
+{
+    return sectors == XGD1_VIDEO_TOTAL_SECTORS;
+}
+
+static BOOL xbox_ref_capacity_is_xgd1_game_view(uint32_t sectors)
+{
+    return sectors == REDUMP_SECTORS;
+}
+
 static uint32_t xbox_ref_refresh_ready_capacity(HANDLE hDevice)
 {
     RefreshVolume(hDevice);
@@ -43,6 +57,746 @@ static char xbox_ref_drive_letter_from_device(const char *device)
     return device[0];
 }
 
+#define XGD1_LAYOUT_PROBE_SCHEMA_VERSION 1
+#define XGD1_LAYOUT_PROBE_SECTOR_BYTES 2048U
+#define XGD1_LAYOUT_PROBE_MAX_SAMPLES 20U
+
+typedef struct xbox_ref_probe_read_s
+{
+    int read_ok;
+    DWORD win32_error;
+    UCHAR scsi_status;
+    UCHAR sense_key;
+    UCHAR asc;
+    UCHAR ascq;
+    uint32_t nonzero_bytes;
+    int all_zero;
+    char sha1[41];
+    unsigned char data[XGD1_LAYOUT_PROBE_SECTOR_BYTES];
+} xbox_ref_probe_read;
+
+typedef struct xbox_ref_probe_sample_s
+{
+    uint32_t lba;
+    const char *label;
+    xbox_ref_probe_read read10;
+    xbox_ref_probe_read read12;
+} xbox_ref_probe_sample;
+
+typedef struct xbox_ref_probe_state_s
+{
+    const char *name;
+    uint32_t capacity;
+    int capacity_valid;
+    int state_verified;
+    uint32_t sample_count;
+    xbox_ref_probe_sample samples[XGD1_LAYOUT_PROBE_MAX_SAMPLES];
+} xbox_ref_probe_state;
+
+typedef struct xbox_ref_probe_point_s
+{
+    uint32_t lba;
+    const char *label;
+} xbox_ref_probe_point;
+
+typedef struct xbox_ref_sptd_with_sense_s
+{
+    SCSI_PASS_THROUGH_DIRECT sptd;
+    ULONG filler;
+    UCHAR sense[32];
+} xbox_ref_sptd_with_sense;
+
+static const xbox_ref_probe_point xbox_ref_locked_probe_points[] = {
+    {0U, "locked_logical_start"},
+    {31U, "locked_early_lba_31"},
+    {32U, "locked_early_lba_32"},
+    {XGD1_VIDEO_L0_SECTORS - 1U, "video_l0_last"},
+    {XGD1_VIDEO_L0_SECTORS, "locked_video_l1_first"},
+    {XGD1_VIDEO_TOTAL_SECTORS - 1U, "locked_video_last"},
+    {XGD1_VIDEO_TOTAL_SECTORS, "locked_capacity_plus_one"},
+    {XGD1_GAME_OUTPUT_START_LBA - 1U, "pregame_output_last"},
+    {XGD1_GAME_OUTPUT_START_LBA, "game_output_start"},
+    {XGD1_REDUMP_LAYER_BREAK_LBA - 1U, "output_layer_break_minus_one"},
+    {XGD1_REDUMP_LAYER_BREAK_LBA, "output_layer_break"},
+    {XGD1_VIDEO_L1_OUTPUT_START_LBA - 1U, "postgame_output_last"},
+    {XGD1_VIDEO_L1_OUTPUT_START_LBA, "video_l1_output_start"},
+    {XGD1_FULL_REDUMP_SECTORS - 1U, "full_output_last"}
+};
+
+static const xbox_ref_probe_point xbox_ref_unlocked_probe_points[] = {
+    {0U, "unlocked_source_start"},
+    {XGD1_XISO_LEADIN_SECTORS - 1U, "unlocked_source_leadin_last"},
+    {XGD1_GAME_SOURCE_START_LBA, "xdfs_source_start"},
+    {XGD1_GAME_SOURCE_START_LBA + 1U, "xdfs_source_next"},
+    {(XGD1_GAME_OUTPUT_START_LBA - XGD1_VIDEO_L0_SECTORS) - 1U, "synthetic_gap_length_minus_one"},
+    {XGD1_GAME_OUTPUT_START_LBA - XGD1_VIDEO_L0_SECTORS, "synthetic_gap_length"},
+    {XGD1_GAME_OUTPUT_START_LBA - 1U, "pregame_output_last_as_unlocked_source"},
+    {XGD1_GAME_OUTPUT_START_LBA, "game_output_start_as_unlocked_source"},
+    {(XGD1_REDUMP_LAYER_BREAK_LBA - XGD1_GAME_OUTPUT_START_LBA) - 1U, "mapped_layer_break_source_minus_one"},
+    {XGD1_REDUMP_LAYER_BREAK_LBA - XGD1_GAME_OUTPUT_START_LBA, "mapped_layer_break_source"},
+    {XGD1_GAME_SOURCE_START_LBA + XGD1_GAME_SOURCE_SECTORS - 1U, "unlocked_source_last"},
+    {REDUMP_SECTORS, "unlocked_capacity_plus_one"},
+    {XGD1_GAME_OUTPUT_START_LBA + REDUMP_SECTORS, "postgame_output_start_as_unlocked_source"},
+    {XGD1_VIDEO_L1_OUTPUT_START_LBA, "video_l1_output_start_as_unlocked_source"}
+};
+
+static void xbox_ref_probe_parse_sense(const UCHAR *sense,
+                                       UCHAR *sense_key,
+                                       UCHAR *asc,
+                                       UCHAR *ascq)
+{
+    UCHAR response;
+
+    if (sense_key) *sense_key = 0;
+    if (asc) *asc = 0;
+    if (ascq) *ascq = 0;
+    if (!sense)
+        return;
+
+    response = (UCHAR)(sense[0] & 0x7f);
+    if (response == 0x70 || response == 0x71)
+    {
+        if (sense_key) *sense_key = (UCHAR)(sense[2] & 0x0f);
+        if (asc) *asc = sense[12];
+        if (ascq) *ascq = sense[13];
+    }
+    else if (response == 0x72 || response == 0x73)
+    {
+        if (sense_key) *sense_key = (UCHAR)(sense[1] & 0x0f);
+        if (asc) *asc = sense[2];
+        if (ascq) *ascq = sense[3];
+    }
+}
+
+static void xbox_ref_probe_sha1(const unsigned char *data,
+                                uint32_t data_size,
+                                char out_sha1[41])
+{
+    SHA1_CTX ctx;
+    unsigned char digest[20];
+    uint32_t i;
+
+    if (!out_sha1)
+        return;
+    out_sha1[0] = '\0';
+    if (!data || data_size == 0)
+        return;
+
+    SHA1_Init(&ctx);
+    SHA1_Update(&ctx, data, data_size);
+    SHA1_Final(digest, &ctx);
+
+    for (i = 0; i < 20U; ++i)
+        sprintf(out_sha1 + (i * 2U), "%02x", digest[i]);
+    out_sha1[40] = '\0';
+}
+
+static void xbox_ref_probe_read_one(HANDLE hDevice,
+                                    UCHAR opcode,
+                                    uint32_t lba,
+                                    xbox_ref_probe_read *result)
+{
+    xbox_ref_sptd_with_sense packet;
+    unsigned char *buffer;
+    DWORD bytes_returned;
+    BOOL ioctl_ok;
+    uint32_t i;
+
+    if (!result)
+        return;
+    memset(result, 0, sizeof(*result));
+
+    buffer = (unsigned char *)VirtualAlloc(NULL,
+                                           XGD1_LAYOUT_PROBE_SECTOR_BYTES,
+                                           MEM_COMMIT | MEM_RESERVE,
+                                           PAGE_READWRITE);
+    if (!buffer)
+    {
+        result->win32_error = ERROR_NOT_ENOUGH_MEMORY;
+        return;
+    }
+
+    memset(buffer, 0, XGD1_LAYOUT_PROBE_SECTOR_BYTES);
+    memset(&packet, 0, sizeof(packet));
+
+    packet.sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
+    packet.sptd.CdbLength = (opcode == 0xA8) ? 12 : 10;
+    packet.sptd.SenseInfoLength = sizeof(packet.sense);
+    packet.sptd.DataIn = SCSI_IOCTL_DATA_IN;
+    packet.sptd.DataTransferLength = XGD1_LAYOUT_PROBE_SECTOR_BYTES;
+    packet.sptd.TimeOutValue = 10;
+    packet.sptd.DataBuffer = buffer;
+    packet.sptd.SenseInfoOffset = offsetof(xbox_ref_sptd_with_sense, sense);
+    packet.sptd.Cdb[0] = opcode;
+    packet.sptd.Cdb[2] = (UCHAR)((lba >> 24) & 0xff);
+    packet.sptd.Cdb[3] = (UCHAR)((lba >> 16) & 0xff);
+    packet.sptd.Cdb[4] = (UCHAR)((lba >> 8) & 0xff);
+    packet.sptd.Cdb[5] = (UCHAR)(lba & 0xff);
+
+    if (opcode == 0xA8)
+    {
+        packet.sptd.Cdb[9] = 1;
+    }
+    else
+    {
+        packet.sptd.Cdb[8] = 1;
+    }
+
+    bytes_returned = 0;
+    ioctl_ok = DeviceIoControl(hDevice,
+                               IOCTL_SCSI_PASS_THROUGH_DIRECT,
+                               &packet,
+                               sizeof(packet),
+                               &packet,
+                               sizeof(packet),
+                               &bytes_returned,
+                               NULL);
+
+    result->win32_error = ioctl_ok ? ERROR_SUCCESS : GetLastError();
+    result->scsi_status = packet.sptd.ScsiStatus;
+    xbox_ref_probe_parse_sense(packet.sense,
+                               &result->sense_key,
+                               &result->asc,
+                               &result->ascq);
+    result->read_ok = (ioctl_ok && packet.sptd.ScsiStatus == 0) ? 1 : 0;
+
+    if (result->read_ok)
+    {
+        memcpy(result->data, buffer, XGD1_LAYOUT_PROBE_SECTOR_BYTES);
+        result->nonzero_bytes = 0;
+        for (i = 0; i < XGD1_LAYOUT_PROBE_SECTOR_BYTES; ++i)
+        {
+            if (buffer[i] != 0)
+                result->nonzero_bytes++;
+        }
+        result->all_zero = (result->nonzero_bytes == 0U) ? 1 : 0;
+        xbox_ref_probe_sha1(buffer,
+                            XGD1_LAYOUT_PROBE_SECTOR_BYTES,
+                            result->sha1);
+    }
+
+    VirtualFree(buffer, 0, MEM_RELEASE);
+}
+
+static void xbox_ref_probe_capture_state(HANDLE hDevice,
+                                         xbox_ref_probe_state *state,
+                                         const xbox_ref_probe_point *points,
+                                         uint32_t point_count)
+{
+    uint32_t i;
+
+    if (!state || !points)
+        return;
+
+    if (point_count > XGD1_LAYOUT_PROBE_MAX_SAMPLES)
+        point_count = XGD1_LAYOUT_PROBE_MAX_SAMPLES;
+
+    state->sample_count = point_count;
+    for (i = 0; i < point_count; ++i)
+    {
+        state->samples[i].lba = points[i].lba;
+        state->samples[i].label = points[i].label;
+
+        printf("[XGD1-PROBE] %s: probing LBA %u (%s) with READ(10) and READ(12).\n",
+               state->name ? state->name : "state",
+               points[i].lba,
+               points[i].label ? points[i].label : "unlabeled");
+
+        xbox_ref_probe_read_one(hDevice,
+                                0x28,
+                                points[i].lba,
+                                &state->samples[i].read10);
+        xbox_ref_probe_read_one(hDevice,
+                                0xA8,
+                                points[i].lba,
+                                &state->samples[i].read12);
+    }
+}
+
+static void xbox_ref_probe_json_string(FILE *json, const char *value)
+{
+    const unsigned char *p;
+
+    if (!json)
+        return;
+    if (!value)
+        value = "";
+
+    fputc('"', json);
+    p = (const unsigned char *)value;
+    while (*p)
+    {
+        switch (*p)
+        {
+            case '\\': fputs("\\\\", json); break;
+            case '"': fputs("\\\"", json); break;
+            case '\b': fputs("\\b", json); break;
+            case '\f': fputs("\\f", json); break;
+            case '\n': fputs("\\n", json); break;
+            case '\r': fputs("\\r", json); break;
+            case '\t': fputs("\\t", json); break;
+            default:
+                if (*p < 0x20)
+                    fprintf(json, "\\u%04x", (unsigned int)*p);
+                else
+                    fputc(*p, json);
+                break;
+        }
+        ++p;
+    }
+    fputc('"', json);
+}
+
+static void xbox_ref_probe_json_hex(FILE *json,
+                                    const unsigned char *data,
+                                    uint32_t data_size)
+{
+    uint32_t i;
+
+    if (!json)
+        return;
+    fputc('"', json);
+    if (data)
+    {
+        for (i = 0; i < data_size; ++i)
+            fprintf(json, "%02x", data[i]);
+    }
+    fputc('"', json);
+}
+
+static void xbox_ref_probe_write_cdb(FILE *json,
+                                      UCHAR opcode,
+                                      uint32_t lba)
+{
+    if (!json)
+        return;
+
+    if (opcode == 0xA8)
+    {
+        fprintf(json,
+                "\"a800%08x000000010000\"",
+                lba);
+    }
+    else
+    {
+        fprintf(json,
+                "\"2800%08x00000100\"",
+                lba);
+    }
+}
+
+static void xbox_ref_probe_write_read(FILE *json,
+                                      const char *indent,
+                                      const char *name,
+                                      const xbox_ref_probe_read *read)
+{
+    if (!json || !read)
+        return;
+
+    fprintf(json, "%s", indent);
+    xbox_ref_probe_json_string(json, name);
+    fprintf(json, ": {\n");
+    fprintf(json, "%s  \"read_ok\": %s,\n", indent, read->read_ok ? "true" : "false");
+    fprintf(json, "%s  \"win32_error\": %lu,\n", indent, (unsigned long)read->win32_error);
+    fprintf(json, "%s  \"scsi_status\": %u,\n", indent, (unsigned int)read->scsi_status);
+    fprintf(json, "%s  \"sense_key\": %u,\n", indent, (unsigned int)read->sense_key);
+    fprintf(json, "%s  \"asc\": %u,\n", indent, (unsigned int)read->asc);
+    fprintf(json, "%s  \"ascq\": %u,\n", indent, (unsigned int)read->ascq);
+    if (read->read_ok)
+    {
+        fprintf(json, "%s  \"nonzero_bytes\": %u,\n", indent, read->nonzero_bytes);
+        fprintf(json, "%s  \"all_zero\": %s,\n", indent, read->all_zero ? "true" : "false");
+    }
+    else
+    {
+        fprintf(json, "%s  \"nonzero_bytes\": null,\n", indent);
+        fprintf(json, "%s  \"all_zero\": null,\n", indent);
+    }
+    fprintf(json, "%s  \"sha1\": ", indent);
+    if (read->read_ok)
+        xbox_ref_probe_json_string(json, read->sha1);
+    else
+        fprintf(json, "null");
+    fprintf(json, ",\n");
+    fprintf(json, "%s  \"data_hex\": ", indent);
+    if (read->read_ok)
+        xbox_ref_probe_json_hex(json, read->data, XGD1_LAYOUT_PROBE_SECTOR_BYTES);
+    else
+        xbox_ref_probe_json_string(json, "");
+    fprintf(json, "\n%s}", indent);
+}
+
+static void xbox_ref_probe_write_state(FILE *json,
+                                       const xbox_ref_probe_state *state,
+                                       int trailing_comma)
+{
+    uint32_t i;
+
+    fprintf(json, "    {\n");
+    fprintf(json, "      \"name\": ");
+    xbox_ref_probe_json_string(json, state->name);
+    fprintf(json, ",\n");
+    fprintf(json, "      \"capacity_valid\": %s,\n", state->capacity_valid ? "true" : "false");
+    fprintf(json, "      \"capacity_sectors\": %u,\n", state->capacity);
+    fprintf(json, "      \"state_verified\": %s,\n", state->state_verified ? "true" : "false");
+    fprintf(json, "      \"samples\": [\n");
+
+    for (i = 0; i < state->sample_count; ++i)
+    {
+        const xbox_ref_probe_sample *sample = &state->samples[i];
+        int identical = 0;
+
+        if (sample->read10.read_ok && sample->read12.read_ok)
+        {
+            identical = (memcmp(sample->read10.data,
+                                sample->read12.data,
+                                XGD1_LAYOUT_PROBE_SECTOR_BYTES) == 0) ? 1 : 0;
+        }
+
+        fprintf(json, "        {\n");
+        fprintf(json, "          \"lba\": %u,\n", sample->lba);
+        fprintf(json, "          \"label\": ");
+        xbox_ref_probe_json_string(json, sample->label);
+        fprintf(json, ",\n");
+        fprintf(json, "          \"read10_cdb_hex\": ");
+        xbox_ref_probe_write_cdb(json, 0x28, sample->lba);
+        fprintf(json, ",\n");
+        fprintf(json, "          \"read12_cdb_hex\": ");
+        xbox_ref_probe_write_cdb(json, 0xA8, sample->lba);
+        fprintf(json, ",\n");
+        xbox_ref_probe_write_read(json, "          ", "read10", &sample->read10);
+        fprintf(json, ",\n");
+        xbox_ref_probe_write_read(json, "          ", "read12", &sample->read12);
+        fprintf(json, ",\n");
+        fprintf(json,
+                "          \"read10_read12_comparable\": %s,\n",
+                (sample->read10.read_ok && sample->read12.read_ok) ? "true" : "false");
+        fprintf(json, "          \"read10_read12_identical\": ");
+        if (sample->read10.read_ok && sample->read12.read_ok)
+            fprintf(json, "%s\n", identical ? "true" : "false");
+        else
+            fprintf(json, "null\n");
+        fprintf(json, "        }%s\n", (i + 1U < state->sample_count) ? "," : "");
+    }
+
+    fprintf(json, "      ]\n");
+    fprintf(json, "    }%s\n", trailing_comma ? "," : "");
+}
+
+static BOOL xbox_ref_probe_write_report(const char *report_path,
+                                        const char *device,
+                                        uint32_t entry_capacity,
+                                        int entry_game_view,
+                                        int volume_lock_acquired,
+                                        const xbox_ref_probe_state *locked_state,
+                                        const xbox_ref_probe_state *unlocked_state,
+                                        int restore_attempted,
+                                        uint32_t restored_capacity,
+                                        int restore_verified)
+{
+    char tmp_path[XBOX_REF_RESULT_PATH_MAX];
+    const char *final_path;
+    FILE *json;
+    int fd;
+    BOOL moved;
+
+    final_path = (report_path && report_path[0]) ? report_path : "xgd1_layout_probe.json";
+    {
+        int path_length = snprintf(tmp_path,
+                                   sizeof(tmp_path),
+                                   "%s.tmp.%lu",
+                                   final_path,
+                                   (unsigned long)GetCurrentProcessId());
+        if (path_length < 0 || (size_t)path_length >= sizeof(tmp_path))
+        {
+            printf("[XGD1-PROBE] Fatal: report path is too long.\n");
+            return FALSE;
+        }
+    }
+
+    json = fopen(tmp_path, "wb");
+    if (!json)
+    {
+        printf("[XGD1-PROBE] Fatal: could not create temporary report %s.\n", tmp_path);
+        return FALSE;
+    }
+
+    fprintf(json, "{\n");
+    fprintf(json, "  \"schema_version\": %d,\n", XGD1_LAYOUT_PROBE_SCHEMA_VERSION);
+    fprintf(json, "  \"producer\": \"friidump-0.5.3.11\",\n");
+    fprintf(json, "  \"probe\": \"original_xbox_xgd1_logical_layout_boundary\",\n");
+    fprintf(json, "  \"device\": ");
+    xbox_ref_probe_json_string(json, device ? device : "");
+    fprintf(json, ",\n");
+    fprintf(json, "  \"safety\": {\n");
+    fprintf(json, "    \"read_only_sector_commands\": true,\n");
+    fprintf(json, "    \"authentication_handshake_used\": true,\n");
+    fprintf(json, "    \"tray_cycle_used\": true,\n");
+    fprintf(json, "    \"firmware_write_used\": false,\n");
+    fprintf(json, "    \"flash_command_used\": false\n");
+    fprintf(json, "  },\n");
+    fprintf(json, "  \"constants\": {\n");
+    fprintf(json, "    \"full_output_sectors\": %u,\n", XGD1_FULL_REDUMP_SECTORS);
+    fprintf(json, "    \"game_output_start_lba\": %u,\n", XGD1_GAME_OUTPUT_START_LBA);
+    fprintf(json, "    \"game_view_sectors\": %u,\n", REDUMP_SECTORS);
+    fprintf(json, "    \"video_total_sectors\": %u,\n", XGD1_VIDEO_TOTAL_SECTORS);
+    fprintf(json, "    \"video_l0_sectors\": %u,\n", XGD1_VIDEO_L0_SECTORS);
+    fprintf(json, "    \"video_l1_sectors\": %u,\n", XGD1_VIDEO_L1_SECTORS);
+    fprintf(json, "    \"video_l1_output_start_lba\": %u,\n", XGD1_VIDEO_L1_OUTPUT_START_LBA);
+    fprintf(json, "    \"output_layer_break_lba\": %u\n", XGD1_REDUMP_LAYER_BREAK_LBA);
+    fprintf(json, "  },\n");
+    fprintf(json, "  \"entry\": {\n");
+    fprintf(json, "    \"capacity_sectors\": %u,\n", entry_capacity);
+    fprintf(json, "    \"game_view\": %s,\n", entry_game_view ? "true" : "false");
+    fprintf(json, "    \"classification\": ");
+    if (xbox_ref_capacity_is_xgd1_game_view(entry_capacity))
+        xbox_ref_probe_json_string(json, "unlocked_game");
+    else if (xbox_ref_capacity_is_xgd1_locked_view(entry_capacity))
+        xbox_ref_probe_json_string(json, "locked_video");
+    else
+        xbox_ref_probe_json_string(json, "other_or_unknown");
+    fprintf(json, "\n");
+    fprintf(json, "  },\n");
+    fprintf(json, "  \"volume_lock_acquired_at_least_once\": %s,\n", volume_lock_acquired ? "true" : "false");
+    fprintf(json,
+            "  \"required_states_verified\": %s,\n",
+            (locked_state->state_verified && unlocked_state->state_verified) ? "true" : "false");
+    fprintf(json, "  \"states\": [\n");
+    xbox_ref_probe_write_state(json, locked_state, 1);
+    xbox_ref_probe_write_state(json, unlocked_state, 0);
+    fprintf(json, "  ],\n");
+    fprintf(json, "  \"restoration\": {\n");
+    fprintf(json, "    \"attempted\": %s,\n", restore_attempted ? "true" : "false");
+    fprintf(json, "    \"capacity_sectors\": %u,\n", restored_capacity);
+    fprintf(json, "    \"verified\": %s\n", restore_verified ? "true" : "false");
+    fprintf(json, "  },\n");
+    fprintf(json, "  \"interpretation_boundary\": {\n");
+    fprintf(json, "    \"pregame_padding_resolved\": false,\n");
+    fprintf(json, "    \"postgame_padding_resolved\": false,\n");
+    fprintf(json, "    \"game_leadin_resolved\": false,\n");
+    fprintf(json, "    \"note\": \"This report records ordinary logical READ(10)/READ(12) behavior in locked/video and unlocked/game states. It does not claim that inaccessible physical filler sectors are zero or that the current reconstructed image is Redump-exact.\"\n");
+    fprintf(json, "  }\n");
+    fprintf(json, "}\n");
+
+    if (fflush(json) != 0)
+    {
+        fclose(json);
+        DeleteFileA(tmp_path);
+        return FALSE;
+    }
+
+    fd = _fileno(json);
+    if (fd < 0 || _commit(fd) != 0)
+    {
+        fclose(json);
+        DeleteFileA(tmp_path);
+        return FALSE;
+    }
+
+    if (fclose(json) != 0)
+    {
+        DeleteFileA(tmp_path);
+        return FALSE;
+    }
+
+    moved = MoveFileExA(tmp_path,
+                        final_path,
+                        MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
+    if (!moved)
+    {
+        printf("[XGD1-PROBE] Fatal: could not atomically publish report %s (error %lu).\n",
+               final_path,
+               (unsigned long)GetLastError());
+        DeleteFileA(tmp_path);
+        return FALSE;
+    }
+
+    printf("[XGD1-PROBE] Atomic report written: %s\n", final_path);
+    return TRUE;
+}
+
+static int xbox_ref_xgd1_layout_probe_core(HANDLE hDevice,
+                                            const char *device,
+                                            const char *report_path,
+                                            BOOL owns_handle)
+{
+    xbox_ref_probe_state locked_state;
+    xbox_ref_probe_state unlocked_state;
+    uint32_t entry_capacity;
+    uint32_t restored_capacity;
+    int entry_game_view;
+    int volume_lock_acquired;
+    int volume_lock_ever;
+    int restore_attempted;
+    int restore_verified;
+    int states_verified;
+    DWORD bytes_returned;
+    BOOL report_ok;
+
+    memset(&locked_state, 0, sizeof(locked_state));
+    memset(&unlocked_state, 0, sizeof(unlocked_state));
+    locked_state.name = "locked_video";
+    unlocked_state.name = "unlocked_game";
+    entry_capacity = 0;
+    restored_capacity = 0;
+    entry_game_view = 0;
+    volume_lock_acquired = 0;
+    volume_lock_ever = 0;
+    restore_attempted = 0;
+    restore_verified = 0;
+    states_verified = 0;
+    bytes_returned = 0;
+
+    xbox_ref_log_open_for_target(report_path, xbox_ref_drive_letter_from_device(device));
+    printf("[XGD1-PROBE] Starting read-only logical layout boundary probe.\n");
+    printf("[XGD1-PROBE] The existing XGD1 output layout will not be modified.\n");
+
+    if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
+    {
+        printf("[XGD1-PROBE] Fatal: invalid drive handle.\n");
+        if (owns_handle)
+            xbox_ref_log_close();
+        return 1;
+    }
+
+    if (!EnsureDriveReady(hDevice, 30000))
+    {
+        printf("[XGD1-PROBE] Fatal: media did not become ready.\n");
+        if (owns_handle)
+            xbox_ref_log_close();
+        return 1;
+    }
+
+    entry_capacity = GetTotalSectors(hDevice);
+    entry_game_view = xbox_ref_capacity_is_xgd1_game_view(entry_capacity) ? 1 : 0;
+    printf("[XGD1-PROBE] Entry READ CAPACITY: %u sectors (%s).\n",
+           entry_capacity,
+           entry_game_view ? "game view" : "locked/video or unknown view");
+
+    DeviceIoControl(hDevice,
+                    FSCTL_UNLOCK_VOLUME,
+                    NULL,
+                    0,
+                    NULL,
+                    0,
+                    &bytes_returned,
+                    NULL);
+
+    printf("[XGD1-PROBE] Cycling tray to establish locked/video view.\n");
+    AutomateTrayCycle(hDevice);
+    locked_state.capacity = xbox_ref_refresh_ready_capacity(hDevice);
+    locked_state.capacity_valid = locked_state.capacity != 0U;
+    locked_state.state_verified = (locked_state.capacity_valid &&
+                                   xbox_ref_capacity_is_xgd1_locked_view(locked_state.capacity)) ? 1 : 0;
+    printf("[XGD1-PROBE] Locked/video READ CAPACITY: %u sectors; verified=%s.\n",
+           locked_state.capacity,
+           locked_state.state_verified ? "yes" : "no");
+
+    if (DeviceIoControl(hDevice,
+                        FSCTL_LOCK_VOLUME,
+                        NULL,
+                        0,
+                        NULL,
+                        0,
+                        &bytes_returned,
+                        NULL))
+    {
+        volume_lock_acquired = 1;
+        volume_lock_ever = 1;
+        printf("[XGD1-PROBE] Windows volume lock acquired.\n");
+    }
+    else
+    {
+        printf("[XGD1-PROBE][WARN] Windows volume lock was not acquired; close AutoPlay and Explorer dialogs if results are unstable.\n");
+    }
+
+    SetDriveSpeedMax(hDevice);
+    xbox_ref_probe_capture_state(hDevice,
+                                 &locked_state,
+                                 xbox_ref_locked_probe_points,
+                                 (uint32_t)(sizeof(xbox_ref_locked_probe_points) /
+                                            sizeof(xbox_ref_locked_probe_points[0])));
+
+    printf("[XGD1-PROBE] Applying the full Xbox handshake to establish game view.\n");
+    UnlockDrive(hDevice);
+    unlocked_state.capacity = xbox_ref_refresh_ready_capacity(hDevice);
+    unlocked_state.capacity_valid = unlocked_state.capacity != 0U;
+    unlocked_state.state_verified = (unlocked_state.capacity_valid &&
+                                     xbox_ref_capacity_is_xgd1_game_view(unlocked_state.capacity)) ? 1 : 0;
+    printf("[XGD1-PROBE] Unlocked/game READ CAPACITY: %u sectors; verified=%s.\n",
+           unlocked_state.capacity,
+           unlocked_state.state_verified ? "yes" : "no");
+
+    xbox_ref_probe_capture_state(hDevice,
+                                 &unlocked_state,
+                                 xbox_ref_unlocked_probe_points,
+                                 (uint32_t)(sizeof(xbox_ref_unlocked_probe_points) /
+                                            sizeof(xbox_ref_unlocked_probe_points[0])));
+
+    if (volume_lock_acquired)
+    {
+        DeviceIoControl(hDevice,
+                        FSCTL_UNLOCK_VOLUME,
+                        NULL,
+                        0,
+                        NULL,
+                        0,
+                        &bytes_returned,
+                        NULL);
+        volume_lock_acquired = 0;
+    }
+
+    if (!entry_game_view && entry_capacity != 0U)
+    {
+        restore_attempted = 1;
+        printf("[XGD1-PROBE] Restoring the entry locked/video state with a final tray cycle.\n");
+        AutomateTrayCycle(hDevice);
+        restored_capacity = xbox_ref_refresh_ready_capacity(hDevice);
+        restore_verified = (restored_capacity != 0U &&
+                            xbox_ref_capacity_is_xgd1_locked_view(restored_capacity)) ? 1 : 0;
+    }
+    else
+    {
+        restored_capacity = GetTotalSectors(hDevice);
+        restore_verified = (entry_game_view &&
+                            xbox_ref_capacity_is_xgd1_game_view(restored_capacity)) ? 1 : 0;
+    }
+
+    states_verified = (locked_state.state_verified &&
+                       unlocked_state.state_verified) ? 1 : 0;
+
+    report_ok = xbox_ref_probe_write_report(report_path,
+                                             device,
+                                             entry_capacity,
+                                             entry_game_view,
+                                             volume_lock_ever,
+                                             &locked_state,
+                                             &unlocked_state,
+                                             restore_attempted,
+                                             restored_capacity,
+                                             restore_verified);
+
+    if (owns_handle)
+    {
+        printf("[XGD1-PROBE] Issuing STOP UNIT / spin-down after probe... ");
+        printf("%s\n", StopDriveUnit(hDevice) ? "OK" : "FAILED");
+        CloseDrive(hDevice);
+        xbox_ref_log_close();
+    }
+
+    if (!report_ok)
+        return 1;
+    if (!states_verified)
+    {
+        printf("[XGD1-PROBE] Probe report is partial because one or more drive states were not verified.\n");
+        return 1;
+    }
+
+    printf("[XGD1-PROBE] Probe complete. Synthetic ranges remain unresolved until the captured evidence supports a source mapping.\n");
+    return 0;
+}
+
+
 static int xbox_ref_gdr8050l_dump_core(HANDLE hDevice, const char *device, const char *filename, char mode, BOOL ownsHandle, xbox_ref_dump_result *result)
 {
     char driveLetter;
@@ -241,6 +995,48 @@ cleanup:
     return ok ? 0 : 1;
 }
 
+int xbox_ref_xgd1_layout_probe_with_handle(void *native_handle,
+                                                const char *device,
+                                                const char *report_path)
+{
+    return xbox_ref_xgd1_layout_probe_core((HANDLE)native_handle,
+                                            device,
+                                            report_path,
+                                            FALSE);
+}
+
+int xbox_ref_xgd1_layout_probe(const char *device,
+                               const char *report_path)
+{
+    char drive_letter;
+    HANDLE hDevice;
+
+    drive_letter = xbox_ref_drive_letter_from_device(device);
+    if (!drive_letter)
+    {
+        xbox_ref_log_open_for_target(report_path, 0);
+        printf("[XGD1-PROBE] Fatal: could not parse drive letter from device '%s'.\n",
+               device ? device : "(null)");
+        xbox_ref_log_close();
+        return 1;
+    }
+
+    hDevice = OpenDrive(drive_letter);
+    if (hDevice == INVALID_HANDLE_VALUE)
+    {
+        xbox_ref_log_open_for_target(report_path, drive_letter);
+        printf("[XGD1-PROBE] Fatal: cannot open drive %c:. Run as Administrator.\n",
+               drive_letter);
+        xbox_ref_log_close();
+        return 1;
+    }
+
+    return xbox_ref_xgd1_layout_probe_core(hDevice,
+                                            device,
+                                            report_path,
+                                            TRUE);
+}
+
 int xbox_ref_gdr8050l_dump_with_handle(void *native_handle, const char *device, const char *filename, char mode, xbox_ref_dump_result *result)
 {
     return xbox_ref_gdr8050l_dump_core((HANDLE)native_handle, device, filename, mode, FALSE, result);
@@ -270,6 +1066,24 @@ int xbox_ref_gdr8050l_dump(const char *device, const char *filename, char mode,
     return xbox_ref_gdr8050l_dump_core(hDevice, device, filename, mode, TRUE, result);
 }
 #else
+int xbox_ref_xgd1_layout_probe_with_handle(void *native_handle,
+                                                const char *device,
+                                                const char *report_path)
+{
+    (void)native_handle;
+    (void)device;
+    (void)report_path;
+    return 1;
+}
+
+int xbox_ref_xgd1_layout_probe(const char *device,
+                               const char *report_path)
+{
+    (void)device;
+    (void)report_path;
+    return 1;
+}
+
 int xbox_ref_gdr8050l_dump_with_handle(void *native_handle, const char *device, const char *filename, char mode, xbox_ref_dump_result *result)
 {
     (void)native_handle; (void)device; (void)filename; (void)mode; xbox_ref_dump_result_init(result); return 1;
index 8d0c278c9b5547d6c45b0e530e907b07ab3c6d3b..6593856fc72cada4861c17c784566fc956141c81 100644 (file)
@@ -38,6 +38,16 @@ int xbox_ref_gdr8050l_dump(const char *device,
                            const char *filename,
                            char mode,
                            xbox_ref_dump_result *result);
+int xbox_ref_xgd1_layout_probe_with_handle(void *native_handle,
+                                                const char *device,
+                                                const char *report_path);
+int xbox_ref_xgd1_layout_probe(const char *device,
+                               const char *report_path);
+int xbox_ref_xgd1_raw_id_probe_with_handle(void *native_handle,
+                                           const char *device,
+                                           const char *report_path);
+int xbox_ref_xgd1_raw_id_probe(const char *device,
+                               const char *report_path);
 
 #ifdef __cplusplus
 }
diff --git a/libfriidump/xbox_xgd1_raw_id_probe.c b/libfriidump/xbox_xgd1_raw_id_probe.c
new file mode 100644 (file)
index 0000000..df21a56
--- /dev/null
@@ -0,0 +1,1112 @@
+#include "xbox_ref_bridge.h"
+
+#include <string.h>
+
+#ifdef WIN32
+#define WIN32_LEAN_AND_MEAN
+#define _WIN32_WINNT 0x0500
+#include <windows.h>
+#include <winioctl.h>
+#include <ntddscsi.h>
+#include <stdio.h>
+#include <stddef.h>
+#include <io.h>
+
+#ifndef SCSI_IOCTL_DATA_UNSPECIFIED
+#define SCSI_IOCTL_DATA_UNSPECIFIED 2
+#endif
+
+#include "xbox_ref/utils.h"
+#include "xbox_ref/sha1.h"
+#include "xbox_ref/unlock.h"
+#include "xbox_ref/xbox_ref_log.h"
+
+#define printf xbox_ref_printf
+
+#define XGD1_RAW_ID_PROBE_SCHEMA_VERSION 2
+#define XGD1_RAW_ID_CACHE_BASE 0x80000000U
+#define XGD1_RAW_SECTOR_BYTES 2064U
+#define XGD1_LOGICAL_SECTOR_BYTES 2048U
+#define XGD1_CACHE_FILL_SECTORS 16U
+#define XGD1_RAW_ID_MAX_SAMPLES 16U
+
+typedef struct xgd1_sptd_with_sense_s
+{
+    SCSI_PASS_THROUGH_DIRECT sptd;
+    ULONG filler;
+    UCHAR sense[32];
+} xgd1_sptd_with_sense;
+
+typedef struct xgd1_cmd_result_s
+{
+    int ok;
+    DWORD win32_error;
+    UCHAR scsi_status;
+    UCHAR sense_key;
+    UCHAR asc;
+    UCHAR ascq;
+} xgd1_cmd_result;
+
+typedef struct xgd1_raw_id_sample_s
+{
+    uint32_t lba;
+    const char *label;
+    uint32_t cache_fill_lba;
+    uint32_t cache_index;
+    uint32_t cache_address;
+    xgd1_cmd_result cache_flush;
+    xgd1_cmd_result cache_fill;
+    xgd1_cmd_result memdump_header;
+    xgd1_cmd_result memdump_edc;
+    int raw_header_valid;
+    UCHAR sector_information;
+    uint32_t layer_number;
+    uint32_t physical_sector_number;
+    uint32_t normalized_physical_sector_number;
+    uint32_t output_lba;
+    uint32_t expected_layer_number;
+    uint32_t expected_physical_sector_number;
+    uint32_t expected_normalized_physical_sector_number;
+    int layer_matches_expected;
+    int psn_matches_expected;
+    int geometry_matches_expected;
+    char logical_sha1[41];
+    unsigned char logical[XGD1_LOGICAL_SECTOR_BYTES];
+    unsigned char raw_header[12];
+    unsigned char raw_edc[4];
+} xgd1_raw_id_sample;
+
+typedef struct xgd1_raw_id_state_s
+{
+    const char *name;
+    int locked_view;
+    uint32_t capacity;
+    int capacity_valid;
+    int state_verified;
+    uint32_t sample_count;
+    uint32_t geometry_validation_failures;
+    int geometry_verified;
+    xgd1_raw_id_sample samples[XGD1_RAW_ID_MAX_SAMPLES];
+} xgd1_raw_id_state;
+
+typedef struct xgd1_probe_point_s
+{
+    uint32_t lba;
+    const char *label;
+} xgd1_probe_point;
+
+static const xgd1_probe_point xgd1_locked_points[] = {
+    {0U, "locked_logical_start"},
+    {31U, "locked_early_lba_31"},
+    {32U, "locked_early_lba_32"},
+    {XGD1_VIDEO_L0_SECTORS - 1U, "video_l0_last"},
+    {XGD1_VIDEO_L0_SECTORS, "video_l1_first"},
+    {XGD1_VIDEO_TOTAL_SECTORS - 1U, "locked_video_last"}
+};
+
+static const xgd1_probe_point xgd1_unlocked_points[] = {
+    {0U, "unlocked_source_start"},
+    {XGD1_XISO_LEADIN_SECTORS - 1U, "unlocked_leadin_last"},
+    {XGD1_GAME_SOURCE_START_LBA, "xdfs_source_start"},
+    {XGD1_GAME_SOURCE_START_LBA + 1U, "xdfs_source_next"},
+    {(XGD1_REDUMP_LAYER_BREAK_LBA - XGD1_GAME_OUTPUT_START_LBA) - 1U, "mapped_layer_break_source_minus_one"},
+    {XGD1_REDUMP_LAYER_BREAK_LBA - XGD1_GAME_OUTPUT_START_LBA, "mapped_layer_break_source"},
+    {REDUMP_SECTORS - 1U, "unlocked_source_last"}
+};
+
+static char xgd1_drive_letter_from_device(const char *device)
+{
+    if (!device || !device[0]) return 0;
+    if (device[0] && device[1] == ':') return device[0];
+    if (device[0] == '\\' && device[1] == '\\' &&
+        device[2] == '.' && device[3] == '\\' &&
+        device[4] && device[5] == ':')
+        return device[4];
+    return device[0];
+}
+
+static int xgd1_capacity_is_locked(uint32_t sectors)
+{
+    return sectors == XGD1_VIDEO_TOTAL_SECTORS;
+}
+
+static int xgd1_capacity_is_game(uint32_t sectors)
+{
+    return sectors == REDUMP_SECTORS;
+}
+
+static uint32_t xgd1_refresh_ready_capacity(HANDLE hDevice)
+{
+    RefreshVolume(hDevice);
+    Sleep(2000);
+    EnsureDriveReady(hDevice, 30000);
+    return GetTotalSectors(hDevice);
+}
+
+static void xgd1_parse_sense(const UCHAR *sense,
+                             UCHAR *sense_key,
+                             UCHAR *asc,
+                             UCHAR *ascq)
+{
+    UCHAR response;
+
+    if (sense_key) *sense_key = 0;
+    if (asc) *asc = 0;
+    if (ascq) *ascq = 0;
+    if (!sense)
+        return;
+
+    response = (UCHAR)(sense[0] & 0x7f);
+    if (response == 0x70 || response == 0x71)
+    {
+        if (sense_key) *sense_key = (UCHAR)(sense[2] & 0x0f);
+        if (asc) *asc = sense[12];
+        if (ascq) *ascq = sense[13];
+    }
+    else if (response == 0x72 || response == 0x73)
+    {
+        if (sense_key) *sense_key = (UCHAR)(sense[1] & 0x0f);
+        if (asc) *asc = sense[2];
+        if (ascq) *ascq = sense[3];
+    }
+}
+
+static void xgd1_sha1(const unsigned char *data,
+                       uint32_t data_size,
+                       char out_sha1[41])
+{
+    SHA1_CTX ctx;
+    unsigned char digest[20];
+    uint32_t i;
+
+    if (!out_sha1)
+        return;
+    out_sha1[0] = '\0';
+    if (!data || data_size == 0)
+        return;
+
+    SHA1_Init(&ctx);
+    SHA1_Update(&ctx, data, data_size);
+    SHA1_Final(digest, &ctx);
+
+    for (i = 0; i < 20U; ++i)
+        sprintf(out_sha1 + (i * 2U), "%02x", digest[i]);
+    out_sha1[40] = '\0';
+}
+
+static void xgd1_cmd_result_init(xgd1_cmd_result *result)
+{
+    if (result)
+        memset(result, 0, sizeof(*result));
+}
+
+static void xgd1_execute_data_in(HANDLE hDevice,
+                                 const UCHAR *cdb,
+                                 UCHAR cdb_length,
+                                 unsigned char *buffer,
+                                 DWORD buffer_length,
+                                 DWORD timeout_seconds,
+                                 xgd1_cmd_result *result)
+{
+    xgd1_sptd_with_sense packet;
+    DWORD bytes_returned;
+    BOOL ioctl_ok;
+
+    xgd1_cmd_result_init(result);
+    if (!result || !cdb || !buffer || buffer_length == 0U)
+        return;
+
+    memset(buffer, 0, buffer_length);
+    memset(&packet, 0, sizeof(packet));
+
+    packet.sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
+    packet.sptd.CdbLength = cdb_length;
+    packet.sptd.SenseInfoLength = sizeof(packet.sense);
+    packet.sptd.DataIn = SCSI_IOCTL_DATA_IN;
+    packet.sptd.DataTransferLength = buffer_length;
+    packet.sptd.TimeOutValue = timeout_seconds;
+    packet.sptd.DataBuffer = buffer;
+    packet.sptd.SenseInfoOffset = offsetof(xgd1_sptd_with_sense, sense);
+    memcpy(packet.sptd.Cdb, cdb, cdb_length);
+
+    bytes_returned = 0;
+    ioctl_ok = DeviceIoControl(hDevice,
+                               IOCTL_SCSI_PASS_THROUGH_DIRECT,
+                               &packet,
+                               sizeof(packet),
+                               &packet,
+                               sizeof(packet),
+                               &bytes_returned,
+                               NULL);
+
+    result->win32_error = ioctl_ok ? ERROR_SUCCESS : GetLastError();
+    result->scsi_status = packet.sptd.ScsiStatus;
+    xgd1_parse_sense(packet.sense,
+                     &result->sense_key,
+                     &result->asc,
+                     &result->ascq);
+    result->ok = (ioctl_ok && packet.sptd.ScsiStatus == 0) ? 1 : 0;
+}
+
+static void xgd1_execute_no_data(HANDLE hDevice,
+                                 const UCHAR *cdb,
+                                 UCHAR cdb_length,
+                                 DWORD timeout_seconds,
+                                 xgd1_cmd_result *result)
+{
+    xgd1_sptd_with_sense packet;
+    DWORD bytes_returned;
+    BOOL ioctl_ok;
+
+    xgd1_cmd_result_init(result);
+    if (!result || !cdb)
+        return;
+
+    memset(&packet, 0, sizeof(packet));
+    packet.sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
+    packet.sptd.CdbLength = cdb_length;
+    packet.sptd.SenseInfoLength = sizeof(packet.sense);
+    packet.sptd.DataIn = SCSI_IOCTL_DATA_UNSPECIFIED;
+    packet.sptd.DataTransferLength = 0;
+    packet.sptd.TimeOutValue = timeout_seconds;
+    packet.sptd.DataBuffer = NULL;
+    packet.sptd.SenseInfoOffset = offsetof(xgd1_sptd_with_sense, sense);
+    memcpy(packet.sptd.Cdb, cdb, cdb_length);
+
+    bytes_returned = 0;
+    ioctl_ok = DeviceIoControl(hDevice,
+                               IOCTL_SCSI_PASS_THROUGH_DIRECT,
+                               &packet,
+                               sizeof(packet),
+                               &packet,
+                               sizeof(packet),
+                               &bytes_returned,
+                               NULL);
+
+    result->win32_error = ioctl_ok ? ERROR_SUCCESS : GetLastError();
+    result->scsi_status = packet.sptd.ScsiStatus;
+    xgd1_parse_sense(packet.sense,
+                     &result->sense_key,
+                     &result->asc,
+                     &result->ascq);
+    result->ok = (ioctl_ok && packet.sptd.ScsiStatus == 0) ? 1 : 0;
+}
+
+static void xgd1_set_lba(UCHAR *cdb, uint32_t lba)
+{
+    cdb[2] = (UCHAR)((lba >> 24) & 0xff);
+    cdb[3] = (UCHAR)((lba >> 16) & 0xff);
+    cdb[4] = (UCHAR)((lba >> 8) & 0xff);
+    cdb[5] = (UCHAR)(lba & 0xff);
+}
+
+static void xgd1_build_memdump_cdb(UCHAR cdb[12],
+                                   uint32_t address,
+                                   uint32_t length)
+{
+    memset(cdb, 0, 12);
+    cdb[0] = 0xE7;
+    cdb[1] = 0x48;
+    cdb[2] = 0x49;
+    cdb[3] = 0x54;
+    cdb[4] = 0x01;
+    cdb[6] = (UCHAR)((address >> 24) & 0xff);
+    cdb[7] = (UCHAR)((address >> 16) & 0xff);
+    cdb[8] = (UCHAR)((address >> 8) & 0xff);
+    cdb[9] = (UCHAR)(address & 0xff);
+    cdb[10] = (UCHAR)((length >> 8) & 0xff);
+    cdb[11] = (UCHAR)(length & 0xff);
+}
+
+static void xgd1_set_expected_geometry(int locked_view,
+                                       xgd1_raw_id_sample *sample)
+{
+    uint32_t output_lba;
+    uint32_t layer_zero_last_psn;
+    uint32_t normalized_psn;
+    uint32_t expected_normalized_psn;
+    uint32_t expected_layer;
+    uint32_t expected_raw_psn;
+
+    if (!sample)
+        return;
+
+    if (locked_view)
+    {
+        if (sample->lba < XGD1_VIDEO_L0_SECTORS)
+            output_lba = sample->lba;
+        else
+            output_lba = XGD1_VIDEO_L1_OUTPUT_START_LBA +
+                         (sample->lba - XGD1_VIDEO_L0_SECTORS);
+    }
+    else
+    {
+        output_lba = XGD1_GAME_OUTPUT_START_LBA + sample->lba;
+    }
+
+    layer_zero_last_psn = 0x30000U + XGD1_REDUMP_LAYER_BREAK_LBA - 1U;
+    if (output_lba < XGD1_REDUMP_LAYER_BREAK_LBA)
+    {
+        expected_layer = 0U;
+        expected_normalized_psn = 0x30000U + output_lba;
+        expected_raw_psn = expected_normalized_psn;
+    }
+    else
+    {
+        expected_layer = 1U;
+        expected_normalized_psn = layer_zero_last_psn -
+            (output_lba - XGD1_REDUMP_LAYER_BREAK_LBA);
+        expected_raw_psn = (~expected_normalized_psn) & 0x00ffffffU;
+    }
+
+    normalized_psn = (sample->layer_number != 0U)
+        ? ((~sample->physical_sector_number) & 0x00ffffffU)
+        : sample->physical_sector_number;
+
+    sample->output_lba = output_lba;
+    sample->normalized_physical_sector_number = normalized_psn;
+    sample->expected_layer_number = expected_layer;
+    sample->expected_physical_sector_number = expected_raw_psn;
+    sample->expected_normalized_physical_sector_number = expected_normalized_psn;
+    sample->layer_matches_expected =
+        (sample->layer_number == expected_layer) ? 1 : 0;
+    sample->psn_matches_expected =
+        (sample->physical_sector_number == expected_raw_psn) ? 1 : 0;
+    sample->geometry_matches_expected =
+        (sample->layer_matches_expected && sample->psn_matches_expected) ? 1 : 0;
+}
+
+
+static void xgd1_capture_sample(HANDLE hDevice,
+                                uint32_t capacity,
+                                int locked_view,
+                                const xgd1_probe_point *point,
+                                xgd1_raw_id_sample *sample)
+{
+    unsigned char *read_buffer;
+    UCHAR flush_cdb[12];
+    UCHAR read_cdb[12];
+    UCHAR e7_cdb[12];
+    uint32_t fill_lba;
+    uint32_t index;
+    uint32_t address;
+
+    if (!sample || !point)
+        return;
+
+    memset(sample, 0, sizeof(*sample));
+    sample->lba = point->lba;
+    sample->label = point->label;
+
+    if (capacity < XGD1_CACHE_FILL_SECTORS || point->lba >= capacity)
+    {
+        sample->cache_flush.win32_error = ERROR_INVALID_PARAMETER;
+        return;
+    }
+
+    fill_lba = point->lba & ~(XGD1_CACHE_FILL_SECTORS - 1U);
+    if (fill_lba > capacity - XGD1_CACHE_FILL_SECTORS)
+        fill_lba = capacity - XGD1_CACHE_FILL_SECTORS;
+    index = point->lba - fill_lba;
+    address = XGD1_RAW_ID_CACHE_BASE + index * XGD1_RAW_SECTOR_BYTES;
+
+    sample->cache_fill_lba = fill_lba;
+    sample->cache_index = index;
+    sample->cache_address = address;
+
+    memset(flush_cdb, 0, sizeof(flush_cdb));
+    flush_cdb[0] = 0xA8;
+    flush_cdb[1] = 0x08;
+    xgd1_set_lba(flush_cdb, fill_lba);
+    xgd1_execute_no_data(hDevice,
+                         flush_cdb,
+                         12,
+                         15,
+                         &sample->cache_flush);
+    if (!sample->cache_flush.ok)
+        return;
+
+    read_buffer = (unsigned char *)VirtualAlloc(
+        NULL,
+        XGD1_CACHE_FILL_SECTORS * XGD1_LOGICAL_SECTOR_BYTES,
+        MEM_COMMIT | MEM_RESERVE,
+        PAGE_READWRITE);
+    if (!read_buffer)
+    {
+        sample->cache_fill.win32_error = ERROR_NOT_ENOUGH_MEMORY;
+        return;
+    }
+
+    memset(read_cdb, 0, sizeof(read_cdb));
+    read_cdb[0] = 0xA8;
+    xgd1_set_lba(read_cdb, fill_lba);
+    read_cdb[9] = (UCHAR)XGD1_CACHE_FILL_SECTORS;
+    read_cdb[10] = 0x80;
+
+    xgd1_execute_data_in(hDevice,
+                         read_cdb,
+                         12,
+                         read_buffer,
+                         XGD1_CACHE_FILL_SECTORS * XGD1_LOGICAL_SECTOR_BYTES,
+                         15,
+                         &sample->cache_fill);
+
+    if (sample->cache_fill.ok)
+    {
+        memcpy(sample->logical,
+               read_buffer + index * XGD1_LOGICAL_SECTOR_BYTES,
+               XGD1_LOGICAL_SECTOR_BYTES);
+        xgd1_sha1(sample->logical,
+                  XGD1_LOGICAL_SECTOR_BYTES,
+                  sample->logical_sha1);
+
+        xgd1_build_memdump_cdb(e7_cdb, address, 12U);
+        xgd1_execute_data_in(hDevice,
+                             e7_cdb,
+                             12,
+                             sample->raw_header,
+                             12U,
+                             15,
+                             &sample->memdump_header);
+
+        xgd1_build_memdump_cdb(e7_cdb, address + 2060U, 4U);
+        xgd1_execute_data_in(hDevice,
+                             e7_cdb,
+                             12,
+                             sample->raw_edc,
+                             4U,
+                             15,
+                             &sample->memdump_edc);
+
+        if (sample->memdump_header.ok && sample->memdump_edc.ok)
+        {
+            sample->raw_header_valid = 1;
+            sample->sector_information = sample->raw_header[0];
+            sample->layer_number = (uint32_t)(sample->sector_information & 1U);
+            sample->physical_sector_number =
+                ((uint32_t)sample->raw_header[1] << 16) |
+                ((uint32_t)sample->raw_header[2] << 8) |
+                (uint32_t)sample->raw_header[3];
+            xgd1_set_expected_geometry(locked_view, sample);
+        }
+    }
+
+    VirtualFree(read_buffer, 0, MEM_RELEASE);
+}
+
+static void xgd1_capture_state(HANDLE hDevice,
+                               xgd1_raw_id_state *state,
+                               const xgd1_probe_point *points,
+                               uint32_t point_count)
+{
+    uint32_t i;
+
+    if (!state || !points || !state->state_verified)
+        return;
+    if (point_count > XGD1_RAW_ID_MAX_SAMPLES)
+        point_count = XGD1_RAW_ID_MAX_SAMPLES;
+
+    state->sample_count = point_count;
+    state->geometry_validation_failures = 0U;
+    for (i = 0; i < point_count; ++i)
+    {
+        printf("[XGD1-RAW-ID] %s: cache-flush, aligned READ(12), and HIT 0xE7 header/EDC capture for LBA %u (%s).\n",
+               state->name ? state->name : "state",
+               points[i].lba,
+               points[i].label ? points[i].label : "unlabeled");
+        xgd1_capture_sample(hDevice,
+                            state->capacity,
+                            state->locked_view,
+                            &points[i],
+                            &state->samples[i]);
+        if (!state->samples[i].cache_flush.ok ||
+            !state->samples[i].cache_fill.ok ||
+            !state->samples[i].raw_header_valid ||
+            !state->samples[i].geometry_matches_expected)
+            state->geometry_validation_failures++;
+    }
+
+    state->geometry_verified =
+        (state->state_verified &&
+         state->sample_count > 0U &&
+         state->geometry_validation_failures == 0U) ? 1 : 0;
+}
+
+static void xgd1_json_string(FILE *f, const char *s)
+{
+    const unsigned char *p;
+
+    fputc('"', f);
+    if (s)
+    {
+        for (p = (const unsigned char *)s; *p; ++p)
+        {
+            switch (*p)
+            {
+                case '\\': fputs("\\\\", f); break;
+                case '"': fputs("\\\"", f); break;
+                case '\b': fputs("\\b", f); break;
+                case '\f': fputs("\\f", f); break;
+                case '\n': fputs("\\n", f); break;
+                case '\r': fputs("\\r", f); break;
+                case '\t': fputs("\\t", f); break;
+                default:
+                    if (*p < 0x20)
+                        fprintf(f, "\\u%04x", (unsigned int)*p);
+                    else
+                        fputc(*p, f);
+                    break;
+            }
+        }
+    }
+    fputc('"', f);
+}
+
+static void xgd1_json_hex(FILE *f,
+                          const unsigned char *data,
+                          uint32_t size)
+{
+    uint32_t i;
+
+    fputc('"', f);
+    for (i = 0; i < size; ++i)
+        fprintf(f, "%02x", data[i]);
+    fputc('"', f);
+}
+
+static void xgd1_write_cmd_result(FILE *f,
+                                  const xgd1_cmd_result *r)
+{
+    fprintf(f, "{\"ok\":%s,\"win32_error\":%lu,\"scsi_status\":%u,"
+               "\"sense_key\":%u,\"asc\":%u,\"ascq\":%u}",
+            (r && r->ok) ? "true" : "false",
+            (unsigned long)(r ? r->win32_error : 0),
+            (unsigned int)(r ? r->scsi_status : 0),
+            (unsigned int)(r ? r->sense_key : 0),
+            (unsigned int)(r ? r->asc : 0),
+            (unsigned int)(r ? r->ascq : 0));
+}
+
+static void xgd1_write_sample(FILE *f,
+                              const xgd1_raw_id_sample *s,
+                              int comma)
+{
+    fprintf(f, "      {\n");
+    fprintf(f, "        \"lba\": %u,\n", s->lba);
+    fprintf(f, "        \"label\": ");
+    xgd1_json_string(f, s->label ? s->label : "");
+    fprintf(f, ",\n");
+    fprintf(f, "        \"cache_fill_lba\": %u,\n", s->cache_fill_lba);
+    fprintf(f, "        \"cache_index\": %u,\n", s->cache_index);
+    fprintf(f, "        \"cache_address\": \"0x%08x\",\n", s->cache_address);
+    fprintf(f, "        \"cache_flush\": ");
+    xgd1_write_cmd_result(f, &s->cache_flush);
+    fprintf(f, ",\n");
+    fprintf(f, "        \"cache_fill\": ");
+    xgd1_write_cmd_result(f, &s->cache_fill);
+    fprintf(f, ",\n");
+    fprintf(f, "        \"memdump_header\": ");
+    xgd1_write_cmd_result(f, &s->memdump_header);
+    fprintf(f, ",\n");
+    fprintf(f, "        \"memdump_edc\": ");
+    xgd1_write_cmd_result(f, &s->memdump_edc);
+    fprintf(f, ",\n");
+    fprintf(f, "        \"raw_header_valid\": %s,\n",
+            s->raw_header_valid ? "true" : "false");
+    if (s->raw_header_valid)
+    {
+        fprintf(f, "        \"sector_information\": %u,\n",
+                (unsigned int)s->sector_information);
+        fprintf(f, "        \"layer_number\": %u,\n", s->layer_number);
+        fprintf(f, "        \"physical_sector_number\": %u,\n",
+                s->physical_sector_number);
+        fprintf(f, "        \"normalized_physical_sector_number\": %u,\n",
+                s->normalized_physical_sector_number);
+        fprintf(f, "        \"output_lba\": %u,\n", s->output_lba);
+        fprintf(f, "        \"expected_layer_number\": %u,\n",
+                s->expected_layer_number);
+        fprintf(f, "        \"expected_physical_sector_number\": %u,\n",
+                s->expected_physical_sector_number);
+        fprintf(f, "        \"expected_normalized_physical_sector_number\": %u,\n",
+                s->expected_normalized_physical_sector_number);
+        fprintf(f, "        \"layer_matches_expected\": %s,\n",
+                s->layer_matches_expected ? "true" : "false");
+        fprintf(f, "        \"psn_matches_expected\": %s,\n",
+                s->psn_matches_expected ? "true" : "false");
+        fprintf(f, "        \"geometry_matches_expected\": %s,\n",
+                s->geometry_matches_expected ? "true" : "false");
+        fprintf(f, "        \"logical_sha1\": \"%s\",\n", s->logical_sha1);
+        fprintf(f, "        \"raw_header_hex\": ");
+        xgd1_json_hex(f, s->raw_header, 12U);
+        fprintf(f, ",\n");
+        fprintf(f, "        \"raw_edc_hex\": ");
+        xgd1_json_hex(f, s->raw_edc, 4U);
+        fprintf(f, ",\n");
+        fprintf(f, "        \"logical_data_hex\": ");
+        xgd1_json_hex(f, s->logical, XGD1_LOGICAL_SECTOR_BYTES);
+        fprintf(f, "\n");
+    }
+    else
+    {
+        fprintf(f, "        \"sector_information\": null,\n");
+        fprintf(f, "        \"layer_number\": null,\n");
+        fprintf(f, "        \"physical_sector_number\": null,\n");
+        fprintf(f, "        \"normalized_physical_sector_number\": null,\n");
+        fprintf(f, "        \"output_lba\": null,\n");
+        fprintf(f, "        \"expected_layer_number\": null,\n");
+        fprintf(f, "        \"expected_physical_sector_number\": null,\n");
+        fprintf(f, "        \"expected_normalized_physical_sector_number\": null,\n");
+        fprintf(f, "        \"layer_matches_expected\": false,\n");
+        fprintf(f, "        \"psn_matches_expected\": false,\n");
+        fprintf(f, "        \"geometry_matches_expected\": false,\n");
+        fprintf(f, "        \"logical_sha1\": null,\n");
+        fprintf(f, "        \"raw_header_hex\": \"\",\n");
+        fprintf(f, "        \"raw_edc_hex\": \"\",\n");
+        fprintf(f, "        \"logical_data_hex\": \"\"\n");
+    }
+    fprintf(f, "      }%s\n", comma ? "," : "");
+}
+
+static void xgd1_write_state(FILE *f,
+                             const xgd1_raw_id_state *state,
+                             int comma)
+{
+    uint32_t i;
+
+    fprintf(f, "    {\n");
+    fprintf(f, "      \"name\": ");
+    xgd1_json_string(f, state->name ? state->name : "");
+    fprintf(f, ",\n");
+    fprintf(f, "      \"capacity_valid\": %s,\n",
+            state->capacity_valid ? "true" : "false");
+    fprintf(f, "      \"capacity_sectors\": %u,\n", state->capacity);
+    fprintf(f, "      \"state_verified\": %s,\n",
+            state->state_verified ? "true" : "false");
+    fprintf(f, "      \"geometry_validation_failures\": %u,\n",
+            state->geometry_validation_failures);
+    fprintf(f, "      \"geometry_verified\": %s,\n",
+            state->geometry_verified ? "true" : "false");
+    fprintf(f, "      \"samples\": [\n");
+    for (i = 0; i < state->sample_count; ++i)
+        xgd1_write_sample(f,
+                          &state->samples[i],
+                          (i + 1U < state->sample_count));
+    fprintf(f, "      ]\n");
+    fprintf(f, "    }%s\n", comma ? "," : "");
+}
+
+static int xgd1_write_report(const char *report_path,
+                             const char *device,
+                             uint32_t entry_capacity,
+                             int entry_game_view,
+                             int volume_lock_ever,
+                             const xgd1_raw_id_state *locked_state,
+                             const xgd1_raw_id_state *unlocked_state,
+                             int restore_attempted,
+                             uint32_t restored_capacity,
+                             int restore_verified)
+{
+    const char *final_path;
+    char tmp_path[MAX_PATH * 4];
+    FILE *f;
+    int fd;
+    BOOL moved;
+    uint32_t validation_failures;
+    int geometry_resolved;
+
+    final_path = (report_path && report_path[0])
+        ? report_path
+        : "xgd1_raw_id_probe.json";
+
+    validation_failures =
+        (locked_state ? locked_state->geometry_validation_failures : 0U) +
+        (unlocked_state ? unlocked_state->geometry_validation_failures : 0U);
+    geometry_resolved =
+        (locked_state && locked_state->geometry_verified &&
+         unlocked_state && unlocked_state->geometry_verified) ? 1 : 0;
+
+    if (strlen(final_path) + 32U >= sizeof(tmp_path))
+    {
+        printf("[XGD1-RAW-ID] Fatal: report path is too long.\n");
+        return 0;
+    }
+
+    sprintf(tmp_path,
+            "%s.tmp.%lu.%lu",
+            final_path,
+            (unsigned long)GetCurrentProcessId(),
+            (unsigned long)GetTickCount());
+
+    f = fopen(tmp_path, "wb");
+    if (!f)
+    {
+        printf("[XGD1-RAW-ID] Fatal: could not create temporary report %s.\n",
+               tmp_path);
+        return 0;
+    }
+
+    fprintf(f, "{\n");
+    fprintf(f, "  \"schema_version\": %u,\n",
+            XGD1_RAW_ID_PROBE_SCHEMA_VERSION);
+    fprintf(f, "  \"producer\": \"friidump-0.5.3.13\",\n");
+    fprintf(f, "  \"probe\": \"original_xbox_xgd1_cache_aligned_raw_id_geometry\",\n");
+    fprintf(f, "  \"device\": ");
+    xgd1_json_string(f, device ? device : "");
+    fprintf(f, ",\n");
+    fprintf(f, "  \"cache\": {\n");
+    fprintf(f, "    \"memdump_command\": \"E7 48 49 54 01\",\n");
+    fprintf(f, "    \"base_address\": \"0x%08x\",\n",
+            XGD1_RAW_ID_CACHE_BASE);
+    fprintf(f, "    \"raw_sector_stride\": %u,\n",
+            XGD1_RAW_SECTOR_BYTES);
+    fprintf(f, "    \"cache_flush_command\": \"READ(12), byte1=0x08, zero transfer length\",\n");
+    fprintf(f, "    \"cache_fill_command\": \"READ(12) streaming, 16 sectors, block-aligned LBA\",\n");
+    fprintf(f, "    \"memdump_regions\": \"12-byte ID/IED/CPR_MAI header plus 4-byte EDC only\",\n");
+    fprintf(f, "    \"raw_user_data_compared_to_logical\": false,\n");
+    fprintf(f, "    \"raw_user_data_note\": \"The proven Method 8 path replaces the drive-cache user field with READ(12) data before unscrambling; equality is not expected and is not a geometry test.\"\n");
+    fprintf(f, "  },\n");
+    fprintf(f, "  \"geometry_model\": {\n");
+    fprintf(f, "    \"dvd_start_psn\": 196608,\n");
+    fprintf(f, "    \"game_output_start_lba\": %u,\n", XGD1_GAME_OUTPUT_START_LBA);
+    fprintf(f, "    \"layer_break_lba\": %u,\n", XGD1_REDUMP_LAYER_BREAK_LBA);
+    fprintf(f, "    \"layer_zero_last_normalized_psn\": %u,\n",
+            0x30000U + XGD1_REDUMP_LAYER_BREAK_LBA - 1U);
+    fprintf(f, "    \"layer_one_raw_psn_is_24bit_complement\": true\n");
+    fprintf(f, "  },\n");
+    fprintf(f, "  \"safety\": {\n");
+    fprintf(f, "    \"read_only_sector_commands\": true,\n");
+    fprintf(f, "    \"authentication_handshake_used\": true,\n");
+    fprintf(f, "    \"tray_cycle_used\": true,\n");
+    fprintf(f, "    \"firmware_write_used\": false,\n");
+    fprintf(f, "    \"flash_command_used\": false\n");
+    fprintf(f, "  },\n");
+    fprintf(f, "  \"entry\": {\n");
+    fprintf(f, "    \"capacity_sectors\": %u,\n", entry_capacity);
+    fprintf(f, "    \"game_view\": %s,\n",
+            entry_game_view ? "true" : "false");
+    fprintf(f, "    \"classification\": \"%s\"\n",
+            entry_game_view ? "unlocked_game" : "locked_video_or_unknown");
+    fprintf(f, "  },\n");
+    fprintf(f, "  \"volume_lock_acquired_at_least_once\": %s,\n",
+            volume_lock_ever ? "true" : "false");
+    fprintf(f, "  \"required_states_verified\": %s,\n",
+            (locked_state && locked_state->state_verified &&
+             unlocked_state && unlocked_state->state_verified)
+                ? "true" : "false");
+    fprintf(f, "  \"geometry_validation_failures\": %u,\n",
+            validation_failures);
+    fprintf(f, "  \"physical_geometry_resolved\": %s,\n",
+            geometry_resolved ? "true" : "false");
+    fprintf(f, "  \"states\": [\n");
+    xgd1_write_state(f, locked_state, 1);
+    xgd1_write_state(f, unlocked_state, 0);
+    fprintf(f, "  ],\n");
+    fprintf(f, "  \"restoration\": {\n");
+    fprintf(f, "    \"attempted\": %s,\n",
+            restore_attempted ? "true" : "false");
+    fprintf(f, "    \"capacity_sectors\": %u,\n", restored_capacity);
+    fprintf(f, "    \"verified\": %s\n",
+            restore_verified ? "true" : "false");
+    fprintf(f, "  },\n");
+    fprintf(f, "  \"interpretation_boundary\": {\n");
+    fprintf(f, "    \"physical_geometry_resolved\": %s,\n",
+            geometry_resolved ? "true" : "false");
+    fprintf(f, "    \"pregame_filler_content_resolved\": false,\n");
+    fprintf(f, "    \"postgame_filler_content_resolved\": false,\n");
+    fprintf(f, "    \"note\": \"A complete cache-aligned raw-ID match can validate the logical-to-physical placement used by the current reconstruction. It does not make inaccessible filler sectors readable and does not establish exact filler bytes.\"\n");
+    fprintf(f, "  }\n");
+    fprintf(f, "}\n");
+
+    if (fflush(f) != 0)
+    {
+        fclose(f);
+        DeleteFileA(tmp_path);
+        return 0;
+    }
+
+    fd = _fileno(f);
+    if (fd < 0 || _commit(fd) != 0)
+    {
+        fclose(f);
+        DeleteFileA(tmp_path);
+        return 0;
+    }
+
+    if (fclose(f) != 0)
+    {
+        DeleteFileA(tmp_path);
+        return 0;
+    }
+
+    moved = MoveFileExA(tmp_path,
+                        final_path,
+                        MOVEFILE_REPLACE_EXISTING |
+                        MOVEFILE_WRITE_THROUGH);
+    if (!moved)
+    {
+        printf("[XGD1-RAW-ID] Fatal: could not atomically publish report %s (error %lu).\n",
+               final_path,
+               (unsigned long)GetLastError());
+        DeleteFileA(tmp_path);
+        return 0;
+    }
+
+    printf("[XGD1-RAW-ID] Atomic report written: %s\n", final_path);
+    return 1;
+}
+
+static int xgd1_raw_id_probe_core(HANDLE hDevice,
+                                  const char *device,
+                                  const char *report_path,
+                                  BOOL owns_handle)
+{
+    xgd1_raw_id_state locked_state;
+    xgd1_raw_id_state unlocked_state;
+    uint32_t entry_capacity;
+    uint32_t restored_capacity;
+    int entry_game_view;
+    int volume_lock_acquired;
+    int volume_lock_ever;
+    int restore_attempted;
+    int restore_verified;
+    int states_verified;
+    DWORD bytes_returned;
+    int report_ok;
+
+    memset(&locked_state, 0, sizeof(locked_state));
+    memset(&unlocked_state, 0, sizeof(unlocked_state));
+    locked_state.name = "locked_video";
+    locked_state.locked_view = 1;
+    unlocked_state.name = "unlocked_game";
+    unlocked_state.locked_view = 0;
+    entry_capacity = 0;
+    restored_capacity = 0;
+    entry_game_view = 0;
+    volume_lock_acquired = 0;
+    volume_lock_ever = 0;
+    restore_attempted = 0;
+    restore_verified = 0;
+    states_verified = 0;
+    bytes_returned = 0;
+
+    xbox_ref_log_open_for_target(report_path,
+                                 xgd1_drive_letter_from_device(device));
+    printf("[XGD1-RAW-ID] Starting cache-aligned raw-sector ID geometry probe.\n");
+    printf("[XGD1-RAW-ID] Requires modified GDR-8050L firmware with HIT 0xE7 memdump support.\n");
+    printf("[XGD1-RAW-ID] No ISO or firmware-write command will be issued.\n");
+
+    if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
+    {
+        printf("[XGD1-RAW-ID] Fatal: invalid drive handle.\n");
+        if (owns_handle)
+            xbox_ref_log_close();
+        return 1;
+    }
+
+    if (!EnsureDriveReady(hDevice, 30000))
+    {
+        printf("[XGD1-RAW-ID] Fatal: media did not become ready.\n");
+        if (owns_handle)
+            xbox_ref_log_close();
+        return 1;
+    }
+
+    entry_capacity = GetTotalSectors(hDevice);
+    entry_game_view = xgd1_capacity_is_game(entry_capacity) ? 1 : 0;
+    printf("[XGD1-RAW-ID] Entry READ CAPACITY: %u sectors (%s).\n",
+           entry_capacity,
+           entry_game_view ? "game view" : "locked/video or unknown view");
+
+    DeviceIoControl(hDevice,
+                    FSCTL_UNLOCK_VOLUME,
+                    NULL,
+                    0,
+                    NULL,
+                    0,
+                    &bytes_returned,
+                    NULL);
+
+    printf("[XGD1-RAW-ID] Cycling tray to establish locked/video view.\n");
+    AutomateTrayCycle(hDevice);
+    locked_state.capacity = xgd1_refresh_ready_capacity(hDevice);
+    locked_state.capacity_valid = locked_state.capacity != 0U;
+    locked_state.state_verified =
+        (locked_state.capacity_valid &&
+         xgd1_capacity_is_locked(locked_state.capacity)) ? 1 : 0;
+    printf("[XGD1-RAW-ID] Locked/video READ CAPACITY: %u sectors; verified=%s.\n",
+           locked_state.capacity,
+           locked_state.state_verified ? "yes" : "no");
+
+    if (DeviceIoControl(hDevice,
+                        FSCTL_LOCK_VOLUME,
+                        NULL,
+                        0,
+                        NULL,
+                        0,
+                        &bytes_returned,
+                        NULL))
+    {
+        volume_lock_acquired = 1;
+        volume_lock_ever = 1;
+        printf("[XGD1-RAW-ID] Windows volume lock acquired.\n");
+    }
+    else
+    {
+        printf("[XGD1-RAW-ID][WARN] Windows volume lock was not acquired.\n");
+    }
+
+    SetDriveSpeedMax(hDevice);
+    xgd1_capture_state(hDevice,
+                       &locked_state,
+                       xgd1_locked_points,
+                       (uint32_t)(sizeof(xgd1_locked_points) /
+                                  sizeof(xgd1_locked_points[0])));
+
+    printf("[XGD1-RAW-ID] Applying the full Xbox handshake to establish game view.\n");
+    UnlockDrive(hDevice);
+    unlocked_state.capacity = xgd1_refresh_ready_capacity(hDevice);
+    unlocked_state.capacity_valid = unlocked_state.capacity != 0U;
+    unlocked_state.state_verified =
+        (unlocked_state.capacity_valid &&
+         xgd1_capacity_is_game(unlocked_state.capacity)) ? 1 : 0;
+    printf("[XGD1-RAW-ID] Unlocked/game READ CAPACITY: %u sectors; verified=%s.\n",
+           unlocked_state.capacity,
+           unlocked_state.state_verified ? "yes" : "no");
+
+    xgd1_capture_state(hDevice,
+                       &unlocked_state,
+                       xgd1_unlocked_points,
+                       (uint32_t)(sizeof(xgd1_unlocked_points) /
+                                  sizeof(xgd1_unlocked_points[0])));
+
+    if (volume_lock_acquired)
+    {
+        DeviceIoControl(hDevice,
+                        FSCTL_UNLOCK_VOLUME,
+                        NULL,
+                        0,
+                        NULL,
+                        0,
+                        &bytes_returned,
+                        NULL);
+        volume_lock_acquired = 0;
+    }
+
+    if (!entry_game_view && entry_capacity != 0U)
+    {
+        restore_attempted = 1;
+        printf("[XGD1-RAW-ID] Restoring entry locked/video state with a final tray cycle.\n");
+        AutomateTrayCycle(hDevice);
+        restored_capacity = xgd1_refresh_ready_capacity(hDevice);
+        restore_verified =
+            (restored_capacity != 0U &&
+             xgd1_capacity_is_locked(restored_capacity)) ? 1 : 0;
+    }
+    else
+    {
+        restored_capacity = GetTotalSectors(hDevice);
+        restore_verified =
+            (entry_game_view &&
+             xgd1_capacity_is_game(restored_capacity)) ? 1 : 0;
+    }
+
+    states_verified =
+        (locked_state.state_verified &&
+         unlocked_state.state_verified) ? 1 : 0;
+
+    report_ok = xgd1_write_report(report_path,
+                                  device,
+                                  entry_capacity,
+                                  entry_game_view,
+                                  volume_lock_ever,
+                                  &locked_state,
+                                  &unlocked_state,
+                                  restore_attempted,
+                                  restored_capacity,
+                                  restore_verified);
+
+    if (owns_handle)
+    {
+        printf("[XGD1-RAW-ID] Issuing STOP UNIT / spin-down after probe... ");
+        printf("%s\n", StopDriveUnit(hDevice) ? "OK" : "FAILED");
+        CloseDrive(hDevice);
+        xbox_ref_log_close();
+    }
+
+    if (!report_ok)
+        return 1;
+    if (!states_verified)
+    {
+        printf("[XGD1-RAW-ID] Probe report is partial because one or more states were not verified.\n");
+        return 1;
+    }
+    if (!locked_state.geometry_verified || !unlocked_state.geometry_verified)
+    {
+        printf("[XGD1-RAW-ID] Geometry validation failed: locked failures=%u, unlocked failures=%u.\n",
+               locked_state.geometry_validation_failures,
+               unlocked_state.geometry_validation_failures);
+        return 1;
+    }
+
+    printf("[XGD1-RAW-ID] Probe complete. All selected raw IDs match the current XGD1 output geometry.\n");
+    return 0;
+}
+
+int xbox_ref_xgd1_raw_id_probe_with_handle(void *native_handle,
+                                           const char *device,
+                                           const char *report_path)
+{
+    return xgd1_raw_id_probe_core((HANDLE)native_handle,
+                                  device,
+                                  report_path,
+                                  FALSE);
+}
+
+int xbox_ref_xgd1_raw_id_probe(const char *device,
+                               const char *report_path)
+{
+    char drive_letter;
+    HANDLE hDevice;
+
+    drive_letter = xgd1_drive_letter_from_device(device);
+    if (!drive_letter)
+    {
+        printf("[XGD1-RAW-ID] Fatal: could not parse drive letter from device.\n");
+        return 1;
+    }
+
+    hDevice = OpenDrive(drive_letter);
+    if (hDevice == INVALID_HANDLE_VALUE)
+    {
+        printf("[XGD1-RAW-ID] Fatal: cannot open drive %c:. Run as Administrator.\n",
+               drive_letter);
+        return 1;
+    }
+
+    return xgd1_raw_id_probe_core(hDevice,
+                                  device,
+                                  report_path,
+                                  TRUE);
+}
+
+#else
+
+int xbox_ref_xgd1_raw_id_probe_with_handle(void *native_handle,
+                                           const char *device,
+                                           const char *report_path)
+{
+    (void)native_handle;
+    (void)device;
+    (void)report_path;
+    return 1;
+}
+
+int xbox_ref_xgd1_raw_id_probe(const char *device,
+                               const char *report_path)
+{
+    (void)device;
+    (void)report_path;
+    return 1;
+}
+
+#endif
index 71ee8e9f887f975864fa1a5241d22a14c2479501..ad9c2e3a84e50add0d11da70bed4d9fbaa20eff3 100644 (file)
@@ -36,6 +36,7 @@ libfriidump\vanilla_2064.c
 libfriidump\vanilla_2384.c
 libfriidump\win32compat.c
 libfriidump/xbox_ref_bridge.c
+libfriidump/xbox_xgd1_raw_id_probe.c
 libfriidump/xbox_ref/utils.c
 libfriidump/xbox_ref/unlock.c
 libfriidump/xbox_ref/rc4.c
index e30b553f8be6f56c8b5d645e8e887d20567f5cdb..292f658039302c82d08f63de712963ddb6ac91b8 100644 (file)
@@ -28,6 +28,7 @@
 #include "dumper.h"
 #include "unscrambler.h"
 #include "xbox_ref/xbox_ref_log.h"
+#include "xbox_ref_bridge.h"
 #include "redump_dat.h"
 
 #ifdef WIN32
@@ -77,6 +78,7 @@ static redump_verify_result g_redump_result;
 static bool g_redump_attempted = false;
 static bool g_operation_duration_override_valid = false;
 static double g_operation_duration_override = 0.0;
+static char g_executable_dir[1024];
 
 static void friidump_summary_copy(char *dst, size_t dst_size, const char *src) {
        if (!dst || dst_size == 0)
@@ -195,7 +197,7 @@ static void friidump_print_validation_summary(double duration, bool have_duratio
 #define PACKAGE_NAME "FriiDump"
 
 /* Define to the version of this package. */
-#define PACKAGE_VERSION "0.5.3.10"
+#define PACKAGE_VERSION "0.5.3.15"
 
 
 #ifdef WIN32
@@ -245,6 +247,10 @@ struct {
        char *redump_dat_dir;
        char *redump_report;
        bool no_redump_verify;
+       bool xgd1_layout_probe;
+       char *xgd1_layout_probe_report;
+       bool xgd1_raw_id_probe;
+       char *xgd1_raw_id_probe_report;
 } options;
 
 
@@ -267,6 +273,8 @@ static char friidump_drive_letter_from_device(const char *device) {
 
 
 static const char *friidump_requested_output_target(void) {
+       if (options.xgd1_raw_id_probe_report && options.xgd1_raw_id_probe_report[0]) return options.xgd1_raw_id_probe_report;
+       if (options.xgd1_layout_probe_report && options.xgd1_layout_probe_report[0]) return options.xgd1_layout_probe_report;
        if (options.iso_out && options.iso_out[0]) return options.iso_out;
        if (options.xiso_out && options.xiso_out[0]) return options.xiso_out;
        if (options.raw_out && options.raw_out[0]) return options.raw_out;
@@ -486,6 +494,59 @@ void welcome (void) {
 
 
 
+static void friidump_init_executable_dir(const char *argv0) {
+    char path[1024];
+    size_t length = 0;
+    char *slash;
+    char *backslash;
+    char *separator;
+
+    g_executable_dir[0] = '\0';
+    path[0] = '\0';
+
+#ifdef WIN32
+    {
+        DWORD result = GetModuleFileNameA(NULL, path, (DWORD)sizeof(path));
+        if (result > 0 && result < sizeof(path)) {
+            path[result] = '\0';
+            length = (size_t)result;
+        }
+    }
+#else
+#if defined(__linux__)
+    {
+        ssize_t result = readlink("/proc/self/exe", path, sizeof(path) - 1);
+        if (result > 0 && (size_t)result < sizeof(path)) {
+            path[result] = '\0';
+            length = (size_t)result;
+        }
+    }
+#endif
+    if (length == 0 && argv0 && argv0[0] &&
+        (strchr(argv0, '/') || strchr(argv0, '\\'))) {
+        char *resolved = realpath(argv0, path);
+        if (resolved)
+            length = strlen(path);
+    }
+#endif
+
+    if (length == 0)
+        return;
+
+    slash = strrchr(path, '/');
+    backslash = strrchr(path, '\\');
+    separator = slash;
+    if (backslash && (!separator || backslash > separator))
+        separator = backslash;
+
+    if (!separator)
+        return;
+
+    *separator = '\0';
+    if (path[0])
+        snprintf(g_executable_dir, sizeof(g_executable_dir), "%s", path);
+}
+
 static const char *friidump_redump_dat_basename(disc_type type_id) {
     switch (type_id) {
         case DISC_TYPE_GAMECUBE:
@@ -673,7 +734,7 @@ static void friidump_verify_redump_values(disc_type type_id,
     redump_verify_result_init(&g_redump_result);
     g_redump_attempted = false;
     representation_note = (type_id == DISC_TYPE_XBOX)
-        ? "XGD1 acquisition success and exact Redump hash identity are separate claims; documented synthetic reconstruction ranges may prevent an exact match."
+        ? "XGD1 acquisition success and exact Redump hash identity are separate claims; unresolved zero-filled pregame/postgame content may prevent an exact match."
         : "";
 
     if (options.no_redump_verify) {
@@ -699,8 +760,11 @@ static void friidump_verify_redump_values(disc_type type_id,
         return;
     }
 
-    dat_dir = (options.redump_dat_dir && options.redump_dat_dir[0]) ? options.redump_dat_dir : "redump_dat";
-    snprintf(dat_path, sizeof(dat_path), "%s/%s", dat_dir, basename);
+    dat_dir = (options.redump_dat_dir && options.redump_dat_dir[0])
+        ? options.redump_dat_dir
+        : NULL;
+    redump_resolve_dat_path(dat_dir, g_executable_dir, basename,
+                            dat_path, sizeof(dat_path));
     if (output_size == 0)
         output_size = friidump_file_size(output_path);
 
@@ -901,9 +965,13 @@ void help (void) {
                "     --scan-dump-prefix <prefix>       Optional raw 0xE7 window dump prefix for --hlds-e7-scan\n"
                "     --hlds-profile-report <file> Write selected HLDS profile/evidence JSON\n"
                "     --redump-dat-dir <dir> Directory containing canonical Redump DAT files\n"
-               "                              (default: redump_dat)\n"
+               "                              (default: executable-relative redump_dat, then current directory)\n"
                "     --redump-report <file>   Write atomic Redump evidence JSON\n"
                "     --no-redump-verify       Disable automatic post-dump DAT verification\n"
+               "     --xgd1-layout-probe <file> Read-only locked/unlocked XGD1 boundary probe;\n"
+               "                              writes atomic JSON and does not create an ISO\n"
+               "     --xgd1-raw-id-probe <file> Modified-firmware cache-flushed, block-aligned raw-ID probe;\n"
+               "                              maps logical LBAs to decoded physical sector IDs\n"
                " -A, --allmethods              Try all known command/method combinations until\n"
                "                               one works. Reopens the drive for each command so\n"
                "                               command-specific vendor handlers are rebound.\n"
@@ -962,6 +1030,8 @@ bool optparse (int argc, char **argv) {
                {"redump-dat-dir", 1, 0, 1006},
                {"redump-report", 1, 0, 1007},
                {"no-redump-verify", 0, 0, 1008},
+               {"xgd1-layout-probe", 1, 0, 1009},
+               {"xgd1-raw-id-probe", 1, 0, 1010},
 #ifdef DEBUG
                /* We don't want newbies to generate and put into circulation bad dumps, so this options are disabled for releases */
                {"donottunscramble", 0, 0, 'n'},
@@ -1010,6 +1080,10 @@ bool optparse (int argc, char **argv) {
        options.redump_dat_dir = NULL;
        options.redump_report = NULL;
        options.no_redump_verify = false;
+       options.xgd1_layout_probe = false;
+       options.xgd1_layout_probe_report = NULL;
+       options.xgd1_raw_id_probe = false;
+       options.xgd1_raw_id_probe_report = NULL;
 
        do {
 #ifdef DEBUG
@@ -1165,6 +1239,16 @@ bool optparse (int argc, char **argv) {
                        case 1008:
                                options.no_redump_verify = true;
                                break;
+                       case 1009:
+                               options.xgd1_layout_probe = true;
+                               my_strdup (options.xgd1_layout_probe_report, optarg);
+                               options.disctype = DISC_TYPE_XBOX;
+                               break;
+                       case 1010:
+                               options.xgd1_raw_id_probe = true;
+                               my_strdup (options.xgd1_raw_id_probe_report, optarg);
+                               options.disctype = DISC_TYPE_XBOX;
+                               break;
                        case -1:
                                break;
                        default:
@@ -1179,6 +1263,9 @@ bool optparse (int argc, char **argv) {
                fprintf (stderr, "WARNING: Extra parameters ignored\n");
        }
 
+       if (options.xgd1_layout_probe || options.xgd1_raw_id_probe)
+               options.disctype = DISC_TYPE_XBOX;
+
        /* Sanity checks... */
        out = false;
        if (!options.device && !options.raw_in) {
@@ -1192,6 +1279,13 @@ bool optparse (int argc, char **argv) {
                fprintf (stderr, "The -r, -i and -X options cannot be used together with -a.\n");
        } else if (options.xiso_requested && (options.raw_out || options.iso_requested)) {
                fprintf (stderr, "The -X/--xiso option is a separate Xbox output mode and cannot be combined with -r or -i.\n");
+       } else if ((options.xgd1_layout_probe || options.xgd1_raw_id_probe) &&
+                  (options.autodump || options.raw_in || options.raw_out ||
+                   options.iso_requested || options.xiso_requested ||
+                   options.allmethods || options.hlds_e7_scan ||
+                   options.hlds_e7_subcmd_sweep || options.hlds_e7_memrange_sweep ||
+                   (options.xgd1_layout_probe && options.xgd1_raw_id_probe))) {
+               fprintf (stderr, "XGD1 probe modes are mutually exclusive read-only diagnostics and cannot be combined with dump, conversion, all-methods, or HLDS 0xE7 probe options.\n");
        } else {
                /* Specified options seem to make sense */
                out = true;
@@ -1212,7 +1306,7 @@ int dologic (disc *d, progstats *stats) {
        u_int32_t current_sector = 0;
        
        xbox_forced = (options.disctype == DISC_TYPE_XBOX);
-       xbox_output_requested = xbox_forced || options.xiso_requested;
+       xbox_output_requested = xbox_forced || options.xiso_requested || options.xgd1_layout_probe || options.xgd1_raw_id_probe;
        dump_attempted = false;
        
        
@@ -1327,6 +1421,67 @@ int dologic (disc *d, progstats *stats) {
                                                fprintf (stderr, "OK\n");
                                }
 
+                               if (options.xgd1_layout_probe || options.xgd1_raw_id_probe) {
+#ifdef WIN32
+                                       int media_rc;
+                                       int media_sense_key;
+                                       int media_asc;
+                                       int media_ascq;
+                                       int probe_status;
+                                       bool stop_ok;
+                                       const char *probe_name;
+
+                                       probe_name = options.xgd1_raw_id_probe ? "XGD1 raw-sector ID" : "XGD1 logical-boundary";
+
+                                       if (!disc_is_xbox_challenge_drive (d)) {
+                                               fprintf (stderr,
+                                                        "%s probe currently supports only the GDR-8050L challenge-handshake profile.\n",
+                                                        probe_name);
+                                               return false;
+                                       }
+
+                                       if (options.xgd1_raw_id_probe &&
+                                           !(disc_get_hlds_e7_type (d) == 44 ||
+                                             disc_get_hlds_e7_type (d) == 45 ||
+                                             disc_get_hlds_e7_type (d) == 442 ||
+                                             disc_get_hlds_e7_type (d) == 443 ||
+                                             disc_get_hlds_e7_type (d) == 445)) {
+                                               fprintf (stderr,
+                                                        "--xgd1-raw-id-probe requires the modified GDR-8050L HIT 0xE7 memdump profile.\n");
+                                               return false;
+                                       }
+
+                                       fprintf (stderr, "\nChecking for ready Xbox media before %s probing... ", probe_name);
+                                       media_rc = disc_media_preflight (d, 15000, &media_sense_key, &media_asc, &media_ascq);
+                                       if (media_rc <= 0) {
+                                               fprintf (stderr,
+                                                        "Failed (sense %02X/%02X/%02X). Insert the disc, wait for spin-up, close AutoPlay/File Explorer, and retry.\n",
+                                                        media_sense_key, media_asc, media_ascq);
+                                               return false;
+                                       }
+                                       fprintf (stderr, "OK\n");
+
+                                       gettimeofday (&(stats -> start_time), NULL);
+                                       if (options.xgd1_raw_id_probe)
+                                               probe_status = xbox_ref_xgd1_raw_id_probe_with_handle (
+                                                               disc_get_native_handle (d), disc_get_device (d), options.xgd1_raw_id_probe_report);
+                                       else
+                                               probe_status = xbox_ref_xgd1_layout_probe_with_handle (
+                                                               disc_get_native_handle (d), disc_get_device (d), options.xgd1_layout_probe_report);
+
+                                       fprintf (stderr, "Issuing STOP UNIT / spin-down after %s probe... ", probe_name);
+                                       stop_ok = disc_stop_unit (d, false);
+                                       fprintf (stderr, "%s\n", stop_ok ? "OK" : "Failed");
+                                       gettimeofday (&(stats -> end_time), NULL);
+                                       fprintf (stderr, "%s probe status: %s\n", probe_name, probe_status == 0 ? "OK" : "FAILED");
+                                       friidump_summary_reset();
+                                       return probe_status == 0;
+#else
+                                       fprintf (stderr, "XGD1 probe modes are available only in the Windows build.\n");
+                                       return false;
+#endif
+                               }
+
                                if (options.hlds_e7_scan) {
                                        out = disc_hlds_e7_scan (d, options.hlds_e7_scan_log, options.hlds_e7_scan_dump_prefix);
                                        fprintf (stderr, "Issuing STOP UNIT / spin-down after HLDS 0xE7 scan... ");
@@ -1679,6 +1834,7 @@ int main (int argc, char *argv[]) {
 
        /* First of all... */
        drop_euid ();
+       friidump_init_executable_dir((argc > 0) ? argv[0] : NULL);
        xbox_ref_log_open_for_target(NULL, 0);
        
        welcome ();
@@ -1758,6 +1914,8 @@ int main (int argc, char *argv[]) {
                my_free (options.raw_out);
                my_free (options.raw_in);
                my_free (options.hlds_profile_report);
+               my_free (options.xgd1_layout_probe_report);
+               my_free (options.xgd1_raw_id_probe_report);
        }
 
        if (xbox_ref_log_path())
index 6231679b06a0964c0928c3c6e170770092881037..028aa1362938c89f25c67aed0b951b2e0b46bca0 100644 (file)
@@ -5,6 +5,93 @@
 #include <stdlib.h>
 #include <string.h>
 
+static int redump_path_exists(const char *path) {
+    FILE *f;
+    if (!path || !path[0])
+        return 0;
+    f = fopen(path, "rb");
+    if (!f)
+        return 0;
+    fclose(f);
+    return 1;
+}
+
+static int redump_join_path(
+    const char *dir,
+    const char *child_dir,
+    const char *basename,
+    char *out_path,
+    size_t out_path_size
+) {
+    int written;
+
+    if (!dir || !dir[0] || !basename || !basename[0] ||
+        !out_path || out_path_size == 0)
+        return 0;
+
+    if (child_dir && child_dir[0])
+        written = snprintf(out_path, out_path_size, "%s/%s/%s",
+                           dir, child_dir, basename);
+    else
+        written = snprintf(out_path, out_path_size, "%s/%s",
+                           dir, basename);
+
+    if (written < 0 || (size_t)written >= out_path_size) {
+        out_path[0] = '\0';
+        return 0;
+    }
+
+    return 1;
+}
+
+int redump_resolve_dat_path(
+    const char *explicit_dir,
+    const char *executable_dir,
+    const char *basename,
+    char *out_path,
+    size_t out_path_size
+) {
+    char candidate[1024];
+
+    if (!basename || !basename[0] || !out_path || out_path_size == 0)
+        return 0;
+
+    out_path[0] = '\0';
+
+    if (explicit_dir && explicit_dir[0]) {
+        if (!redump_join_path(explicit_dir, NULL, basename,
+                              out_path, out_path_size))
+            return 0;
+        return redump_path_exists(out_path);
+    }
+
+    if (executable_dir && executable_dir[0] &&
+        redump_join_path(executable_dir, "redump_dat", basename,
+                         candidate, sizeof(candidate))) {
+        if (redump_path_exists(candidate)) {
+            snprintf(out_path, out_path_size, "%s", candidate);
+            return 1;
+        }
+
+        /* Retain the executable-relative path as the preferred diagnostic
+         * candidate if neither automatic location exists. */
+        snprintf(out_path, out_path_size, "%s", candidate);
+    }
+
+    if (redump_join_path(".", "redump_dat", basename,
+                         candidate, sizeof(candidate))) {
+        if (redump_path_exists(candidate)) {
+            snprintf(out_path, out_path_size, "%s", candidate);
+            return 1;
+        }
+
+        if (!out_path[0])
+            snprintf(out_path, out_path_size, "%s", candidate);
+    }
+
+    return 0;
+}
+
 static int redump_ascii_equal(const char *a, const char *b) {
     unsigned char ca, cb;
     if (!a || !b)
index 39919edb00286c78d626753f7660487fa8251fde..475f5c28acaad550d35eab0006f3e8767f44cb55 100644 (file)
@@ -49,6 +49,26 @@ const char *redump_field_status_string(redump_field_status status);
 const char *redump_verify_overall_string(const redump_verify_result *result);
 const char *redump_verify_confidence_string(const redump_verify_result *result);
 const char *redump_verify_match_kind_string(const redump_verify_result *result);
+
+/*
+ * Resolve a DAT path predictably.
+ *
+ * Resolution order:
+ *   1. explicit_dir, when non-empty;
+ *   2. executable_dir/redump_dat;
+ *   3. current-working-directory redump_dat.
+ *
+ * The selected candidate is always written to out_path when arguments are
+ * valid. The return value is nonzero only when that candidate exists.
+ */
+int redump_resolve_dat_path(
+    const char *explicit_dir,
+    const char *executable_dir,
+    const char *basename,
+    char *out_path,
+    size_t out_path_size
+);
+
 int redump_verify_dat_file(
     const char *dat_path,
     uint64_t image_size,
diff --git a/tests/test_redump_dat_resolver.c b/tests/test_redump_dat_resolver.c
new file mode 100644 (file)
index 0000000..275c9b0
--- /dev/null
@@ -0,0 +1,61 @@
+#include "redump_dat.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static const char *optional_arg(const char *arg) {
+    if (!arg || strcmp(arg, "-") == 0)
+        return NULL;
+    return arg;
+}
+
+int main(int argc, char **argv) {
+    const char *explicit_dir;
+    const char *executable_dir;
+    const char *basename;
+    const char *expected_path;
+    int expected_found;
+    int found;
+    char resolved[1024];
+
+    if (argc != 6) {
+        fprintf(stderr,
+                "Usage: %s <explicit-dir|-> <executable-dir|-> "
+                "<basename> <expected-path> <expected-found-0-or-1>\n",
+                argc > 0 ? argv[0] : "test_redump_dat_resolver");
+        return 64;
+    }
+
+    explicit_dir = optional_arg(argv[1]);
+    executable_dir = optional_arg(argv[2]);
+    basename = argv[3];
+    expected_path = argv[4];
+    expected_found = atoi(argv[5]) ? 1 : 0;
+
+    memset(resolved, 0, sizeof(resolved));
+    found = redump_resolve_dat_path(
+        explicit_dir,
+        executable_dir,
+        basename,
+        resolved,
+        sizeof(resolved)
+    ) ? 1 : 0;
+
+    printf("resolved=%s\n", resolved);
+    printf("found=%d\n", found);
+
+    if (found != expected_found) {
+        fprintf(stderr, "Found-state mismatch: expected %d, observed %d\n",
+                expected_found, found);
+        return 1;
+    }
+
+    if (strcmp(resolved, expected_path) != 0) {
+        fprintf(stderr, "Path mismatch:\n  expected: %s\n  observed: %s\n",
+                expected_path, resolved);
+        return 2;
+    }
+
+    return 0;
+}