]> FriiDump Source - friidump.git/blobdiff - libfriidump/xbox_ref_bridge.c
FriiDump 0.5.3.16: finalize release identity and documentation
[friidump.git] / libfriidump / xbox_ref_bridge.c
index 663805d10f0e04d8c842e2569124734a5c57382b..25e317ab1b7a49b3882d550552e716d32bc83f88 100644 (file)
@@ -1,4 +1,5 @@
 #include "xbox_ref_bridge.h"
 #include "xbox_ref_bridge.h"
+#include "xbox_region.h"
 
 #include <string.h>
 
 
 #include <string.h>
 
@@ -13,8 +14,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 +32,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,7 +58,752 @@ static char xbox_ref_drive_letter_from_device(const char *device)
     return device[0];
 }
 
     return device[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)
+#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_cancel_func cancel,
+                                          void *cancel_data,
+                                          xbox_ref_dump_result *result)
 {
     char driveLetter;
     XboxGameInfo game;
 {
     char driveLetter;
     XboxGameInfo game;
@@ -164,6 +924,13 @@ static int xbox_ref_gdr8050l_dump_core(HANDLE hDevice, const char *device, const
             strncpy(result->media_id, mediaId, sizeof(result->media_id) - 1);
             result->media_id[sizeof(result->media_id) - 1] = '\0';
         }
             strncpy(result->media_id, mediaId, sizeof(result->media_id) - 1);
             result->media_id[sizeof(result->media_id) - 1] = '\0';
         }
+        if (game.Success) {
+            result->have_game_region = 1;
+            result->game_region = game.GameRegion;
+            xbox_region_format(game.GameRegion,
+                               result->region,
+                               sizeof(result->region));
+        }
     }
 
     if (game.Success) {
     }
 
     if (game.Success) {
@@ -218,7 +985,7 @@ static int xbox_ref_gdr8050l_dump_core(HANDLE hDevice, const char *device, const
     }
 
     printf("Starting dump with copied DumpXboxGameDisc() mode %c...\n", mode);
     }
 
     printf("Starting dump with copied DumpXboxGameDisc() mode %c...\n", mode);
-    ok = DumpXboxGameDisc(hDevice, finalFilename, mode, totalSectors, isDualLayerBool, FALSE, result);
+    ok = DumpXboxGameDisc(hDevice, finalFilename, mode, totalSectors, isDualLayerBool, FALSE, cancel, cancel_data, result);
 
 cleanup:
     if (volumeLocked)
 
 cleanup:
     if (volumeLocked)
@@ -236,14 +1003,61 @@ cleanup:
     } else if (ok) {
         printf("[XBOX-REF] Returning finalized output evidence to FriiDump's shared Redump verifier.\n");
     } else {
     } else if (ok) {
         printf("[XBOX-REF] Returning finalized output evidence to FriiDump's shared Redump verifier.\n");
     } else {
-        printf("[XBOX-REF] No completed output evidence is available for shared Redump verification.\n");
+        printf("[XBOX-REF] No completed full-output evidence is available for shared Redump verification.\n");
     }
     return ok ? 0 : 1;
 }
 
     }
     return ok ? 0 : 1;
 }
 
-int xbox_ref_gdr8050l_dump_with_handle(void *native_handle, const char *device, const char *filename, char mode, xbox_ref_dump_result *result)
+int xbox_ref_xgd1_layout_probe_with_handle(void *native_handle,
+                                                const char *device,
+                                                const char *report_path)
 {
 {
-    return xbox_ref_gdr8050l_dump_core((HANDLE)native_handle, device, filename, mode, FALSE, result);
+    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_cancel_func cancel,
+                                         void *cancel_data,
+                                         xbox_ref_dump_result *result)
+{
+    return xbox_ref_gdr8050l_dump_core((HANDLE)native_handle, device, filename,
+                                      mode, FALSE, cancel, cancel_data, result);
 }
 
 int xbox_ref_gdr8050l_dump(const char *device, const char *filename, char mode, xbox_ref_dump_result *result)
 }
 
 int xbox_ref_gdr8050l_dump(const char *device, const char *filename, char mode, xbox_ref_dump_result *result)
@@ -267,12 +1081,37 @@ int xbox_ref_gdr8050l_dump(const char *device, const char *filename, char mode,
         return 1;
     }
 
         return 1;
     }
 
-    return xbox_ref_gdr8050l_dump_core(hDevice, device, filename, mode, TRUE, result);
+    return xbox_ref_gdr8050l_dump_core(hDevice, device, filename, mode, TRUE, NULL, NULL, result);
 }
 #else
 }
 #else
-int xbox_ref_gdr8050l_dump_with_handle(void *native_handle, const char *device, const char *filename, char mode, xbox_ref_dump_result *result)
+int xbox_ref_xgd1_layout_probe_with_handle(void *native_handle,
+                                                const char *device,
+                                                const char *report_path)
+{
+    (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_cancel_func cancel,
+                                         void *cancel_data,
+                                         xbox_ref_dump_result *result)
 {
 {
-    (void)native_handle; (void)device; (void)filename; (void)mode; xbox_ref_dump_result_init(result); return 1;
+    (void)native_handle; (void)device; (void)filename; (void)mode;
+    (void)cancel; (void)cancel_data;
+    xbox_ref_dump_result_init(result);
+    return 1;
 }
 
 int xbox_ref_gdr8050l_dump(const char *device, const char *filename, char mode, xbox_ref_dump_result *result)
 }
 
 int xbox_ref_gdr8050l_dump(const char *device, const char *filename, char mode, xbox_ref_dump_result *result)