]> 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-*/
 /build/
 /build-*/
-/out/
-
-# Windows/MSVC output
-/.vs/
 /build-msvc32/
 /build-msvc32/
-/friidump.exe
-/friidump.pdb
-/friidump.ilk
-/friidump.exp
-/friidump.lib
-/friidump_msvc32_build_results.zip
+
+# Compiler and executable outputs
 *.obj
 *.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
 /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.
 
 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,
 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.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:
 
 
 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
 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:
 
 
 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.
 - `--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:
 
 
 Example:
 
@@ -568,5 +588,68 @@ Example:
 ```
 
 The DAT files are data inputs, not compiled into the executable. Replace the
 ```
 
 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 ->
 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 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;
 - 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.
 
 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
 
 
 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.
 
 - `<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
 
 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.
 
 `--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
 ## 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.
 
 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
 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.
 
 
 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
 ## 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.
 
 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.
 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.
                                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)
  -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
  -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)
  -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.
  -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
        win32compat.c
        xbox_ref_bridge.h
        xbox_ref_bridge.c
+       xbox_xgd1_raw_id_probe.c
        xbox_ref/xbox_ref_log.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, "    \"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);
        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, "  },\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, "    \"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");
        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, "  },\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);
        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 (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))
 
        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)
     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
 
 
 #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, "    \"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);
         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, "  \"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, "    \"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)
     {
     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, "      }\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");
     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, "      \"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);
         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);
             }
 
                 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;
 
             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;
             {
                 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>
 #define _WIN32_WINNT 0x0500
 #include <windows.h>
 #include <winioctl.h>
+#include <ntddscsi.h>
 #include <stdio.h>
 #include <stdio.h>
+#include <stddef.h>
+#include <io.h>
 #include "xbox_ref/utils.h"
 #include "xbox_ref/utils.h"
+#include "xbox_ref/sha1.h"
 #include "xbox_ref/unlock.h"
 #include "xbox_ref/xbox_ref_log.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;
 }
 
     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);
 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];
 }
 
     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;
 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;
 }
 
     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);
 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
     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;
 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);
                            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
 }
 
 #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\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
 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 "dumper.h"
 #include "unscrambler.h"
 #include "xbox_ref/xbox_ref_log.h"
+#include "xbox_ref_bridge.h"
 #include "redump_dat.h"
 
 #ifdef WIN32
 #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 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)
 
 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_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
 
 
 #ifdef WIN32
@@ -245,6 +247,10 @@ struct {
        char *redump_dat_dir;
        char *redump_report;
        bool no_redump_verify;
        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;
 
 
 } options;
 
 
@@ -267,6 +273,8 @@ static char friidump_drive_letter_from_device(const char *device) {
 
 
 static const char *friidump_requested_output_target(void) {
 
 
 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;
        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:
 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)
     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) {
         : "";
 
     if (options.no_redump_verify) {
@@ -699,8 +760,11 @@ static void friidump_verify_redump_values(disc_type type_id,
         return;
     }
 
         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);
 
     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"
                "     --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"
                "     --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"
                " -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},
                {"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'},
 #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.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
 
        do {
 #ifdef DEBUG
@@ -1165,6 +1239,16 @@ bool optparse (int argc, char **argv) {
                        case 1008:
                                options.no_redump_verify = true;
                                break;
                        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:
                        case -1:
                                break;
                        default:
@@ -1179,6 +1263,9 @@ bool optparse (int argc, char **argv) {
                fprintf (stderr, "WARNING: Extra parameters ignored\n");
        }
 
                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) {
        /* 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");
                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;
        } 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);
        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;
        
        
        dump_attempted = false;
        
        
@@ -1327,6 +1421,67 @@ int dologic (disc *d, progstats *stats) {
                                                fprintf (stderr, "OK\n");
                                }
 
                                                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... ");
                                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 ();
 
        /* First of all... */
        drop_euid ();
+       friidump_init_executable_dir((argc > 0) ? argv[0] : NULL);
        xbox_ref_log_open_for_target(NULL, 0);
        
        welcome ();
        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.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())
        }
 
        if (xbox_ref_log_path())
index 6231679b06a0964c0928c3c6e170770092881037..028aa1362938c89f25c67aed0b951b2e0b46bca0 100644 (file)
@@ -5,6 +5,93 @@
 #include <stdlib.h>
 #include <string.h>
 
 #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)
 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);
 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,
 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;
+}