diff --git a/.gitattributes b/.gitattributes
index 9e2b4945..1c544085 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -5,3 +5,17 @@ launcher-rust/fixtures/** text eol=lf
launcher-rust/assets/i18n.json text eol=lf
# Snapshot references are images compared pixel by pixel: never touched as text.
launcher-rust/snapshots/** binary
+# The runner fixtures: text outputs are LF, every input and output that is bytes is never touched as text.
+launcher-rust/fixtures/runner/**/*.json text eol=lf
+launcher-rust/fixtures/runner/**/*.xml text eol=lf
+launcher-rust/fixtures/runner/**/*.ini text eol=lf
+launcher-rust/fixtures/runner/**/*.bin binary
+launcher-rust/fixtures/runner/**/*.dds binary
+launcher-rust/fixtures/runner/**/*.dxt1 binary
+launcher-rust/fixtures/runner/**/*.dxt5 binary
+launcher-rust/fixtures/runner/**/*.png binary
+launcher-rust/fixtures/runner/**/*.bmp binary
+launcher-rust/fixtures/runner/**/*.WAD binary
+launcher-rust/fixtures/runner/**/*.prx binary
+launcher-rust/fixtures/runner/**/*.sfo binary
+launcher-rust/fixtures/runner/**/*.elf binary
diff --git a/README.md b/README.md
index a5fee56a..31c4390f 100644
--- a/README.md
+++ b/README.md
@@ -481,6 +481,7 @@ cargo test
The page snapshots under `launcher-rust/snapshots/` are compared pixel by pixel on Windows only.
`python -m unittest discover -s tests -p "test_launcher_parity_doc.py"` checks that every Python
launcher test has its row in `docs/specs/2026-10-21-rust-launcher-parity.md`.
+`tools/runner_fixtures.py` generates `launcher-rust/fixtures/runner/`, the inputs and the files the Python runner's modules write over them, for the Rust runner's parity tests.
### Windows C++ tests (MSYS2 CLANG64)
diff --git a/launcher-rust/fixtures/runner/content/expected/full/content-profile.json b/launcher-rust/fixtures/runner/content/expected/full/content-profile.json
new file mode 100644
index 00000000..72c1ed0a
--- /dev/null
+++ b/launcher-rust/fixtures/runner/content/expected/full/content-profile.json
@@ -0,0 +1,13 @@
+{
+ "title_id": "CUSA01623",
+ "sku": "full",
+ "sku_source": "explicit probe setting",
+ "user_params": [
+ 13,
+ 0,
+ 7,
+ 4294967295
+ ],
+ "mounted_addons": [],
+ "boot_attr": 0
+}
diff --git a/launcher-rust/fixtures/runner/content/expected/full/content.bin b/launcher-rust/fixtures/runner/content/expected/full/content.bin
new file mode 100644
index 00000000..9750054c
Binary files /dev/null and b/launcher-rust/fixtures/runner/content/expected/full/content.bin differ
diff --git a/launcher-rust/fixtures/runner/content/expected/trial/content-profile.json b/launcher-rust/fixtures/runner/content/expected/trial/content-profile.json
new file mode 100644
index 00000000..f46a8b72
--- /dev/null
+++ b/launcher-rust/fixtures/runner/content/expected/trial/content-profile.json
@@ -0,0 +1,13 @@
+{
+ "title_id": "CUSA01623",
+ "sku": "trial",
+ "sku_source": "explicit probe setting",
+ "user_params": [
+ 13,
+ 0,
+ 7,
+ 4294967295
+ ],
+ "mounted_addons": [],
+ "boot_attr": 0
+}
diff --git a/launcher-rust/fixtures/runner/content/expected/trial/content.bin b/launcher-rust/fixtures/runner/content/expected/trial/content.bin
new file mode 100644
index 00000000..e9b967d4
Binary files /dev/null and b/launcher-rust/fixtures/runner/content/expected/trial/content.bin differ
diff --git a/launcher-rust/fixtures/runner/content/input/param.sfo b/launcher-rust/fixtures/runner/content/input/param.sfo
new file mode 100644
index 00000000..34361ccc
Binary files /dev/null and b/launcher-rust/fixtures/runner/content/input/param.sfo differ
diff --git a/launcher-rust/fixtures/runner/dxt/expected/alpha.dxt1 b/launcher-rust/fixtures/runner/dxt/expected/alpha.dxt1
new file mode 100644
index 00000000..2447e4d3
Binary files /dev/null and b/launcher-rust/fixtures/runner/dxt/expected/alpha.dxt1 differ
diff --git a/launcher-rust/fixtures/runner/dxt/expected/alpha.dxt5 b/launcher-rust/fixtures/runner/dxt/expected/alpha.dxt5
new file mode 100644
index 00000000..34a4810d
Binary files /dev/null and b/launcher-rust/fixtures/runner/dxt/expected/alpha.dxt5 differ
diff --git a/launcher-rust/fixtures/runner/dxt/expected/edges.dxt1 b/launcher-rust/fixtures/runner/dxt/expected/edges.dxt1
new file mode 100644
index 00000000..3010c7eb
Binary files /dev/null and b/launcher-rust/fixtures/runner/dxt/expected/edges.dxt1 differ
diff --git a/launcher-rust/fixtures/runner/dxt/expected/edges.dxt5 b/launcher-rust/fixtures/runner/dxt/expected/edges.dxt5
new file mode 100644
index 00000000..661bad4e
Binary files /dev/null and b/launcher-rust/fixtures/runner/dxt/expected/edges.dxt5 differ
diff --git a/launcher-rust/fixtures/runner/dxt/expected/flat.dxt1 b/launcher-rust/fixtures/runner/dxt/expected/flat.dxt1
new file mode 100644
index 00000000..d6d23e4a
Binary files /dev/null and b/launcher-rust/fixtures/runner/dxt/expected/flat.dxt1 differ
diff --git a/launcher-rust/fixtures/runner/dxt/expected/flat.dxt5 b/launcher-rust/fixtures/runner/dxt/expected/flat.dxt5
new file mode 100644
index 00000000..1cf48edc
Binary files /dev/null and b/launcher-rust/fixtures/runner/dxt/expected/flat.dxt5 differ
diff --git a/launcher-rust/fixtures/runner/dxt/expected/gradient.dxt1 b/launcher-rust/fixtures/runner/dxt/expected/gradient.dxt1
new file mode 100644
index 00000000..603eef05
Binary files /dev/null and b/launcher-rust/fixtures/runner/dxt/expected/gradient.dxt1 differ
diff --git a/launcher-rust/fixtures/runner/dxt/expected/gradient.dxt5 b/launcher-rust/fixtures/runner/dxt/expected/gradient.dxt5
new file mode 100644
index 00000000..3a849cce
Binary files /dev/null and b/launcher-rust/fixtures/runner/dxt/expected/gradient.dxt5 differ
diff --git a/launcher-rust/fixtures/runner/dxt/expected/noise.dxt1 b/launcher-rust/fixtures/runner/dxt/expected/noise.dxt1
new file mode 100644
index 00000000..69f9779b
Binary files /dev/null and b/launcher-rust/fixtures/runner/dxt/expected/noise.dxt1 differ
diff --git a/launcher-rust/fixtures/runner/dxt/expected/noise.dxt5 b/launcher-rust/fixtures/runner/dxt/expected/noise.dxt5
new file mode 100644
index 00000000..fc5f455e
Binary files /dev/null and b/launcher-rust/fixtures/runner/dxt/expected/noise.dxt5 differ
diff --git a/launcher-rust/fixtures/runner/graphics/expected/good/environment.json b/launcher-rust/fixtures/runner/graphics/expected/good/environment.json
new file mode 100644
index 00000000..a2697dee
--- /dev/null
+++ b/launcher-rust/fixtures/runner/graphics/expected/good/environment.json
@@ -0,0 +1,5 @@
+{
+ "SPARTAN_DEFERRED_READBACK": "1",
+ "SPARTAN_STALE_READBACK": "1",
+ "SPARTAN_ASYNC_SHADERS": "1"
+}
diff --git a/launcher-rust/fixtures/runner/graphics/expected/good/graphics_state.json b/launcher-rust/fixtures/runner/graphics/expected/good/graphics_state.json
new file mode 100644
index 00000000..ed8feee9
--- /dev/null
+++ b/launcher-rust/fixtures/runner/graphics/expected/good/graphics_state.json
@@ -0,0 +1,10 @@
+{
+ "last_good": {
+ "render_resolution": "1440p",
+ "engine_fps": "120",
+ "deferred_readback": "1",
+ "stale_readback": "1",
+ "async_shaders": "1"
+ },
+ "attempt": false
+}
\ No newline at end of file
diff --git a/launcher-rust/fixtures/runner/graphics/expected/good/spartanport.ini b/launcher-rust/fixtures/runner/graphics/expected/good/spartanport.ini
new file mode 100644
index 00000000..1350879c
--- /dev/null
+++ b/launcher-rust/fixtures/runner/graphics/expected/good/spartanport.ini
@@ -0,0 +1,2 @@
+render_resolution=1440p
+engine_fps=120
diff --git a/launcher-rust/fixtures/runner/graphics/expected/reverted/environment.json b/launcher-rust/fixtures/runner/graphics/expected/reverted/environment.json
new file mode 100644
index 00000000..a2697dee
--- /dev/null
+++ b/launcher-rust/fixtures/runner/graphics/expected/reverted/environment.json
@@ -0,0 +1,5 @@
+{
+ "SPARTAN_DEFERRED_READBACK": "1",
+ "SPARTAN_STALE_READBACK": "1",
+ "SPARTAN_ASYNC_SHADERS": "1"
+}
diff --git a/launcher-rust/fixtures/runner/graphics/expected/reverted/graphics_state.json b/launcher-rust/fixtures/runner/graphics/expected/reverted/graphics_state.json
new file mode 100644
index 00000000..ed8feee9
--- /dev/null
+++ b/launcher-rust/fixtures/runner/graphics/expected/reverted/graphics_state.json
@@ -0,0 +1,10 @@
+{
+ "last_good": {
+ "render_resolution": "1440p",
+ "engine_fps": "120",
+ "deferred_readback": "1",
+ "stale_readback": "1",
+ "async_shaders": "1"
+ },
+ "attempt": false
+}
\ No newline at end of file
diff --git a/launcher-rust/fixtures/runner/graphics/expected/reverted/spartanport.ini b/launcher-rust/fixtures/runner/graphics/expected/reverted/spartanport.ini
new file mode 100644
index 00000000..8681e6b1
--- /dev/null
+++ b/launcher-rust/fixtures/runner/graphics/expected/reverted/spartanport.ini
@@ -0,0 +1,6 @@
+render_resolution=1440p
+engine_fps=120
+restart_unconfirmed=0
+deferred_readback=1
+stale_readback=1
+async_shaders=1
diff --git a/launcher-rust/fixtures/runner/graphics/expected/tried/environment.json b/launcher-rust/fixtures/runner/graphics/expected/tried/environment.json
new file mode 100644
index 00000000..a2697dee
--- /dev/null
+++ b/launcher-rust/fixtures/runner/graphics/expected/tried/environment.json
@@ -0,0 +1,5 @@
+{
+ "SPARTAN_DEFERRED_READBACK": "1",
+ "SPARTAN_STALE_READBACK": "1",
+ "SPARTAN_ASYNC_SHADERS": "1"
+}
diff --git a/launcher-rust/fixtures/runner/graphics/expected/tried/graphics_state.json b/launcher-rust/fixtures/runner/graphics/expected/tried/graphics_state.json
new file mode 100644
index 00000000..4d329057
--- /dev/null
+++ b/launcher-rust/fixtures/runner/graphics/expected/tried/graphics_state.json
@@ -0,0 +1,10 @@
+{
+ "last_good": {
+ "render_resolution": "1440p",
+ "engine_fps": "120",
+ "deferred_readback": "1",
+ "stale_readback": "1",
+ "async_shaders": "1"
+ },
+ "attempt": true
+}
\ No newline at end of file
diff --git a/launcher-rust/fixtures/runner/graphics/expected/tried/spartanport.ini b/launcher-rust/fixtures/runner/graphics/expected/tried/spartanport.ini
new file mode 100644
index 00000000..b23c9fbd
--- /dev/null
+++ b/launcher-rust/fixtures/runner/graphics/expected/tried/spartanport.ini
@@ -0,0 +1,3 @@
+render_resolution=4K
+engine_fps=240
+restart_unconfirmed=1
diff --git a/launcher-rust/fixtures/runner/graphics/input/edit.json b/launcher-rust/fixtures/runner/graphics/input/edit.json
new file mode 100644
index 00000000..b0ca0d4c
--- /dev/null
+++ b/launcher-rust/fixtures/runner/graphics/input/edit.json
@@ -0,0 +1,5 @@
+{
+ "render_resolution": "4K",
+ "engine_fps": "240",
+ "restart_unconfirmed": "1"
+}
diff --git a/launcher-rust/fixtures/runner/graphics/input/graphics_state.json b/launcher-rust/fixtures/runner/graphics/input/graphics_state.json
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/launcher-rust/fixtures/runner/graphics/input/graphics_state.json
@@ -0,0 +1 @@
+{}
diff --git a/launcher-rust/fixtures/runner/graphics/input/spartanport.ini b/launcher-rust/fixtures/runner/graphics/input/spartanport.ini
new file mode 100644
index 00000000..1350879c
--- /dev/null
+++ b/launcher-rust/fixtures/runner/graphics/input/spartanport.ini
@@ -0,0 +1,2 @@
+render_resolution=1440p
+engine_fps=120
diff --git a/launcher-rust/fixtures/runner/icon/expected/window_icon.bmp b/launcher-rust/fixtures/runner/icon/expected/window_icon.bmp
new file mode 100644
index 00000000..9349350d
Binary files /dev/null and b/launcher-rust/fixtures/runner/icon/expected/window_icon.bmp differ
diff --git a/launcher-rust/fixtures/runner/isa/expected/isa-rewrite.bin b/launcher-rust/fixtures/runner/isa/expected/isa-rewrite.bin
new file mode 100644
index 00000000..8edb6d5f
Binary files /dev/null and b/launcher-rust/fixtures/runner/isa/expected/isa-rewrite.bin differ
diff --git a/launcher-rust/fixtures/runner/isa/expected/sites.json b/launcher-rust/fixtures/runner/isa/expected/sites.json
new file mode 100644
index 00000000..62d3e890
--- /dev/null
+++ b/launcher-rust/fixtures/runner/isa/expected/sites.json
@@ -0,0 +1,122 @@
+[
+ {
+ "offset": 0,
+ "length": 5,
+ "kind": "BLSR",
+ "width": 32,
+ "mode": "JMP",
+ "registers": {
+ "dst": 1,
+ "src1": 1,
+ "src2": 255,
+ "base": 255,
+ "index": 255
+ },
+ "mem_slot": 0,
+ "scale": 0,
+ "mem_flags": 0,
+ "disp": 0,
+ "original": "c4e270f3c9",
+ "relocated": ""
+ },
+ {
+ "offset": 5,
+ "length": 4,
+ "kind": "MOVBE_STORE",
+ "width": 32,
+ "mode": "JMP_RELOCATE",
+ "registers": {
+ "dst": 255,
+ "src1": 1,
+ "src2": 255,
+ "base": 0,
+ "index": 255
+ },
+ "mem_slot": 1,
+ "scale": 1,
+ "mem_flags": 0,
+ "disp": 0,
+ "original": "0f38f108",
+ "relocated": "90"
+ },
+ {
+ "offset": 10,
+ "length": 9,
+ "kind": "BEXTR",
+ "width": 32,
+ "mode": "JMP",
+ "registers": {
+ "dst": 0,
+ "src1": 255,
+ "src2": 1,
+ "base": 255,
+ "index": 255
+ },
+ "mem_slot": 2,
+ "scale": 1,
+ "mem_flags": 1,
+ "disp": 31,
+ "original": "c4e270f7050c000000",
+ "relocated": ""
+ },
+ {
+ "offset": 19,
+ "length": 5,
+ "kind": "LZCNT",
+ "width": 16,
+ "mode": "JMP",
+ "registers": {
+ "dst": 0,
+ "src1": 1,
+ "src2": 255,
+ "base": 255,
+ "index": 255
+ },
+ "mem_slot": 0,
+ "scale": 0,
+ "mem_flags": 0,
+ "disp": 0,
+ "original": "66f30fbdc1",
+ "relocated": ""
+ },
+ {
+ "offset": 24,
+ "length": 5,
+ "kind": "TZCNT",
+ "width": 64,
+ "mode": "JMP",
+ "registers": {
+ "dst": 0,
+ "src1": 1,
+ "src2": 255,
+ "base": 255,
+ "index": 255
+ },
+ "mem_slot": 0,
+ "scale": 0,
+ "mem_flags": 0,
+ "disp": 0,
+ "original": "f3480fbcc1",
+ "relocated": ""
+ },
+ {
+ "offset": 29,
+ "length": 4,
+ "kind": "MOVBE_STORE",
+ "width": 32,
+ "mode": "INT3",
+ "registers": {
+ "dst": 255,
+ "src1": 1,
+ "src2": 255,
+ "base": 0,
+ "index": 255
+ },
+ "mem_slot": 1,
+ "scale": 1,
+ "mem_flags": 0,
+ "disp": 0,
+ "original": "0f38f108",
+ "relocated": ""
+ }
+]
diff --git a/launcher-rust/fixtures/runner/isa/input/boot-linked.bin b/launcher-rust/fixtures/runner/isa/input/boot-linked.bin
new file mode 100644
index 00000000..4d2b8da2
Binary files /dev/null and b/launcher-rust/fixtures/runner/isa/input/boot-linked.bin differ
diff --git a/launcher-rust/fixtures/runner/link/expected/boot-libc.bin b/launcher-rust/fixtures/runner/link/expected/boot-libc.bin
new file mode 100644
index 00000000..22e7fa6f
Binary files /dev/null and b/launcher-rust/fixtures/runner/link/expected/boot-libc.bin differ
diff --git a/launcher-rust/fixtures/runner/link/expected/boot-linked.bin b/launcher-rust/fixtures/runner/link/expected/boot-linked.bin
new file mode 100644
index 00000000..32752dbc
Binary files /dev/null and b/launcher-rust/fixtures/runner/link/expected/boot-linked.bin differ
diff --git a/launcher-rust/fixtures/runner/link/expected/libc-link.json b/launcher-rust/fixtures/runner/link/expected/libc-link.json
new file mode 100644
index 00000000..b55954ee
--- /dev/null
+++ b/launcher-rust/fixtures/runner/link/expected/libc-link.json
@@ -0,0 +1,37 @@
+{
+ "base": "0x10000",
+ "size": 4096,
+ "sha256": "16bcdb5c0a28f581da21dc76c9e73ed1e8ec35398b85be38352691f4ff4cba33",
+ "init": "0x10010",
+ "bindings": 1,
+ "tls_module_id": 2,
+ "fs_loads_patched": 2,
+ "main_tls": {
+ "vaddr": 3072,
+ "filesz": 16,
+ "memsz": 48,
+ "align": 16
+ },
+ "tls_relocations": 1,
+ "tls_template_bytes": 16,
+ "tls_memory_bytes": 48,
+ "imports": [
+ "bzQExy189ZI#q#q",
+ "AAAAAAAAAAA#q#q",
+ "DDDDDDDDDDD#q#q"
+ ],
+ "relocation_counts": {
+ "8": 1,
+ "16": 1,
+ "1": 1,
+ "6": 1,
+ "7": 1
+ },
+ "symbol_bindings": [
+ {
+ "import_name": "bzQExy189ZI#q#q",
+ "address": "0x10010",
+ "kind": 1
+ }
+ ]
+}
diff --git a/launcher-rust/fixtures/runner/link/expected/link.json b/launcher-rust/fixtures/runner/link/expected/link.json
new file mode 100644
index 00000000..ac24c23a
--- /dev/null
+++ b/launcher-rust/fixtures/runner/link/expected/link.json
@@ -0,0 +1,43 @@
+{
+ "modules": [
+ {
+ "file": "libc.prx",
+ "base": "0x10000",
+ "size": 4096,
+ "init": "0x10010",
+ "tls_address": "0x10500",
+ "tls_memsz": 48,
+ "tls_filesz": 16,
+ "tls_module": 2,
+ "tls_relocations": 1,
+ "exports": 3,
+ "sha256": "16bcdb5c0a28f581da21dc76c9e73ed1e8ec35398b85be38352691f4ff4cba33"
+ },
+ {
+ "file": "libSceFios2.prx",
+ "base": "0x20000",
+ "size": 2048,
+ "init": "0x20000",
+ "tls_address": "0x0",
+ "tls_memsz": 0,
+ "tls_filesz": 0,
+ "tls_module": 0,
+ "tls_relocations": 0,
+ "exports": 1,
+ "sha256": "a7ae78118c4fcb8dd27129c2b8059fbcf2e0d685b4ad8b40038fa43f4d93e4f2"
+ }
+ ],
+ "bindings": 2,
+ "imports": 4,
+ "fs_loads_patched": 4,
+ "main_tls": {
+ "vaddr": 3072,
+ "filesz": 16,
+ "memsz": 48,
+ "align": 16
+ },
+ "unresolved_imports": [
+ "AAAAAAAAAAA#libc",
+ "DDDDDDDDDDD#libc"
+ ]
+}
diff --git a/launcher-rust/fixtures/runner/link/input/boot.bin b/launcher-rust/fixtures/runner/link/input/boot.bin
new file mode 100644
index 00000000..52118313
Binary files /dev/null and b/launcher-rust/fixtures/runner/link/input/boot.bin differ
diff --git a/launcher-rust/fixtures/runner/link/input/eboot.bin b/launcher-rust/fixtures/runner/link/input/eboot.bin
new file mode 100644
index 00000000..e1c1d5ed
Binary files /dev/null and b/launcher-rust/fixtures/runner/link/input/eboot.bin differ
diff --git a/launcher-rust/fixtures/runner/link/input/sce_module/libSceFios2.prx b/launcher-rust/fixtures/runner/link/input/sce_module/libSceFios2.prx
new file mode 100644
index 00000000..c7cafc1e
Binary files /dev/null and b/launcher-rust/fixtures/runner/link/input/sce_module/libSceFios2.prx differ
diff --git a/launcher-rust/fixtures/runner/link/input/sce_module/libc.prx b/launcher-rust/fixtures/runner/link/input/sce_module/libc.prx
new file mode 100644
index 00000000..d1ab1bb0
Binary files /dev/null and b/launcher-rust/fixtures/runner/link/input/sce_module/libc.prx differ
diff --git a/launcher-rust/fixtures/runner/patches/expected/patch_status.json b/launcher-rust/fixtures/runner/patches/expected/patch_status.json
new file mode 100644
index 00000000..f1652f37
--- /dev/null
+++ b/launcher-rust/fixtures/runner/patches/expected/patch_status.json
@@ -0,0 +1 @@
+{"sha256": "ffed585c6a2b513546eba7f283c45ad27382a8e31cf54fe03b4e1c0ca63195d5", "applied": ["Found", "Call", "Data"], "skipped": {"Missing": "signature not found"}}
\ No newline at end of file
diff --git a/launcher-rust/fixtures/runner/patches/expected/patches-merged.xml b/launcher-rust/fixtures/runner/patches/expected/patches-merged.xml
new file mode 100644
index 00000000..0366f3d0
--- /dev/null
+++ b/launcher-rust/fixtures/runner/patches/expected/patches-merged.xml
@@ -0,0 +1,44 @@
+
+
+
+ CUSA01623
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/launcher-rust/fixtures/runner/patches/expected/patches.bin b/launcher-rust/fixtures/runner/patches/expected/patches.bin
new file mode 100644
index 00000000..bdf3d8d6
Binary files /dev/null and b/launcher-rust/fixtures/runner/patches/expected/patches.bin differ
diff --git a/launcher-rust/fixtures/runner/patches/input/eboot.bin b/launcher-rust/fixtures/runner/patches/input/eboot.bin
new file mode 100644
index 00000000..9895ccd9
--- /dev/null
+++ b/launcher-rust/fixtures/runner/patches/input/eboot.bin
@@ -0,0 +1 @@
+synthetic eboot.bin of the patches case
diff --git a/launcher-rust/fixtures/runner/patches/input/eboot.elf b/launcher-rust/fixtures/runner/patches/input/eboot.elf
new file mode 100644
index 00000000..36bb9e2f
Binary files /dev/null and b/launcher-rust/fixtures/runner/patches/input/eboot.elf differ
diff --git a/launcher-rust/fixtures/runner/patches/input/mod.xml b/launcher-rust/fixtures/runner/patches/input/mod.xml
new file mode 100644
index 00000000..087b4410
--- /dev/null
+++ b/launcher-rust/fixtures/runner/patches/input/mod.xml
@@ -0,0 +1,13 @@
+
+
+ CUSA01623
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/launcher-rust/fixtures/runner/patches/input/param.sfo b/launcher-rust/fixtures/runner/patches/input/param.sfo
new file mode 100644
index 00000000..331ffa86
Binary files /dev/null and b/launcher-rust/fixtures/runner/patches/input/param.sfo differ
diff --git a/launcher-rust/fixtures/runner/patches/input/patches.xml b/launcher-rust/fixtures/runner/patches/input/patches.xml
new file mode 100644
index 00000000..1d67a4be
--- /dev/null
+++ b/launcher-rust/fixtures/runner/patches/input/patches.xml
@@ -0,0 +1,21 @@
+
+
+ CUSA01623
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/launcher-rust/fixtures/runner/prepare/expected/analysis.json b/launcher-rust/fixtures/runner/prepare/expected/analysis.json
new file mode 100644
index 00000000..4d56ea28
--- /dev/null
+++ b/launcher-rust/fixtures/runner/prepare/expected/analysis.json
@@ -0,0 +1,142 @@
+{
+ "source_sha256": "138b11e351ce338943e47f76805cfba5380996355ea098c97be4fd586002d96c",
+ "source_bytes": 5400,
+ "sfo": {
+ "APP_VER": "01.02",
+ "CONTENT_ID": "EP9000-CUSA01623_00-GOW3REMASTERED00",
+ "TITLE": "ゴッド・オブ・ウォーIII リマスタード",
+ "TITLE_ID": "CUSA01623",
+ "USER_DEFINED_PARAM_1": 13,
+ "USER_DEFINED_PARAM_2": 0,
+ "USER_DEFINED_PARAM_3": 7,
+ "USER_DEFINED_PARAM_4": 4294967295
+ },
+ "entry": "0x10",
+ "image_bytes": 4096,
+ "program_headers": [
+ {
+ "type": 1,
+ "flags": 5,
+ "offset": 4096,
+ "vaddr": 0,
+ "paddr": 0,
+ "filesz": 2048,
+ "memsz": 2048,
+ "align": 16384
+ },
+ {
+ "type": 1,
+ "flags": 6,
+ "offset": 6144,
+ "vaddr": 2048,
+ "paddr": 2048,
+ "filesz": 2048,
+ "memsz": 2048,
+ "align": 16384
+ },
+ {
+ "type": 2,
+ "flags": 4,
+ "offset": 8192,
+ "vaddr": 0,
+ "paddr": 0,
+ "filesz": 192,
+ "memsz": 0,
+ "align": 8
+ },
+ {
+ "type": 1627389952,
+ "flags": 4,
+ "offset": 8448,
+ "vaddr": 0,
+ "paddr": 0,
+ "filesz": 344,
+ "memsz": 0,
+ "align": 8
+ },
+ {
+ "type": 1627389953,
+ "flags": 4,
+ "offset": 8960,
+ "vaddr": 128,
+ "paddr": 128,
+ "filesz": 0,
+ "memsz": 64,
+ "align": 8
+ },
+ {
+ "type": 7,
+ "flags": 4,
+ "offset": 7168,
+ "vaddr": 3072,
+ "paddr": 3072,
+ "filesz": 16,
+ "memsz": 48,
+ "align": 16
+ }
+ ],
+ "self_segments": [
+ {
+ "flags": "0x800",
+ "offset": 768,
+ "size": 2048
+ },
+ {
+ "flags": "0x100800",
+ "offset": 2816,
+ "size": 2048
+ },
+ {
+ "flags": "0x200800",
+ "offset": 4864,
+ "size": 192
+ },
+ {
+ "flags": "0x300800",
+ "offset": 5056,
+ "size": 344
+ }
+ ],
+ "unavailable_metadata_headers": [],
+ "needed": [
+ "libc.prx"
+ ],
+ "relocation_counts": {
+ "8": 1,
+ "6": 2,
+ "1": 1,
+ "7": 2
+ },
+ "import_count": 2,
+ "imports": [
+ "bzQExy189ZI#q#q",
+ "AAAAAAAAAAA#q#q"
+ ],
+ "import_name_hints": {
+ "bzQExy189ZI#q#q": "_init_env"
+ },
+ "libc_evidence": {
+ "sha256": "16bcdb5c0a28f581da21dc76c9e73ed1e8ec35398b85be38352691f4ff4cba33",
+ "init_env_is_ret": true,
+ "symbol": "bzQExy189ZI#C#A",
+ "address": "0x10",
+ "size": 1,
+ "bytes": "c3",
+ "ios_base_init": {
+ "symbol": "sqWytnhYdEg#C#A",
+ "address": "0x20",
+ "size": 3,
+ "code_sha256": "251447ee91a9067dcd6ab96703133f617565974cd6c4819021760c4688c91abf",
+ "name": "_ZNSt8ios_base4InitC1Ev"
+ }
+ },
+ "bundled_modules": [
+ "libc.prx"
+ ],
+ "resources": {
+ "PART1.WAD": 1,
+ "sound": 1
+ },
+ "resource_bytes": 132,
+ "status": "Prepared only; execution and Vulkan are tested separately."
+}
diff --git a/launcher-rust/fixtures/runner/prepare/expected/boot.bin b/launcher-rust/fixtures/runner/prepare/expected/boot.bin
new file mode 100644
index 00000000..2f5242ca
Binary files /dev/null and b/launcher-rust/fixtures/runner/prepare/expected/boot.bin differ
diff --git a/launcher-rust/fixtures/runner/prepare/expected/eboot.elf b/launcher-rust/fixtures/runner/prepare/expected/eboot.elf
new file mode 100644
index 00000000..4e828a14
Binary files /dev/null and b/launcher-rust/fixtures/runner/prepare/expected/eboot.elf differ
diff --git a/launcher-rust/fixtures/runner/prepare/expected/entry.bin b/launcher-rust/fixtures/runner/prepare/expected/entry.bin
new file mode 100644
index 00000000..7a13f328
Binary files /dev/null and b/launcher-rust/fixtures/runner/prepare/expected/entry.bin differ
diff --git a/launcher-rust/fixtures/runner/prepare/input/dvdroot_ps4/PART1.WAD b/launcher-rust/fixtures/runner/prepare/input/dvdroot_ps4/PART1.WAD
new file mode 100644
index 00000000..edf1f4a8
Binary files /dev/null and b/launcher-rust/fixtures/runner/prepare/input/dvdroot_ps4/PART1.WAD differ
diff --git a/launcher-rust/fixtures/runner/prepare/input/dvdroot_ps4/sound/music.bin b/launcher-rust/fixtures/runner/prepare/input/dvdroot_ps4/sound/music.bin
new file mode 100644
index 00000000..eeb57607
Binary files /dev/null and b/launcher-rust/fixtures/runner/prepare/input/dvdroot_ps4/sound/music.bin differ
diff --git a/launcher-rust/fixtures/runner/prepare/input/eboot.bin b/launcher-rust/fixtures/runner/prepare/input/eboot.bin
new file mode 100644
index 00000000..e1c1d5ed
Binary files /dev/null and b/launcher-rust/fixtures/runner/prepare/input/eboot.bin differ
diff --git a/launcher-rust/fixtures/runner/prepare/input/sce_module/libc.prx b/launcher-rust/fixtures/runner/prepare/input/sce_module/libc.prx
new file mode 100644
index 00000000..d1ab1bb0
Binary files /dev/null and b/launcher-rust/fixtures/runner/prepare/input/sce_module/libc.prx differ
diff --git a/launcher-rust/fixtures/runner/prepare/input/sce_sys/param.sfo b/launcher-rust/fixtures/runner/prepare/input/sce_sys/param.sfo
new file mode 100644
index 00000000..d6c3bc74
Binary files /dev/null and b/launcher-rust/fixtures/runner/prepare/input/sce_sys/param.sfo differ
diff --git a/tests/test_runner_generators.py b/tests/test_runner_generators.py
new file mode 100644
index 00000000..23ff3100
--- /dev/null
+++ b/tests/test_runner_generators.py
@@ -0,0 +1,58 @@
+"""The Rust runner's parity fixtures: what tools/runner_fixtures.py wrote from the Python modules."""
+import unittest
+
+from paths import ROOT
+
+FIXTURES = ROOT / 'launcher-rust/fixtures/runner'
+DXT_NAMES = ('gradient', 'edges', 'alpha', 'noise', 'flat')
+
+# case -> (input files, expected files), relative to the case folder's input/ and expected/.
+CASES = {
+ 'prepare': (['eboot.bin', 'sce_module/libc.prx', 'sce_sys/param.sfo', 'dvdroot_ps4/PART1.WAD'],
+ ['boot.bin', 'eboot.elf', 'entry.bin', 'analysis.json']),
+ 'link': (['boot.bin', 'eboot.bin', 'sce_module/libc.prx', 'sce_module/libSceFios2.prx'],
+ ['boot-libc.bin', 'libc-link.json', 'boot-linked.bin', 'link.json']),
+ 'isa': (['boot-linked.bin'], ['isa-rewrite.bin', 'sites.json']),
+ 'content': (['param.sfo'], ['full/content.bin', 'full/content-profile.json',
+ 'trial/content.bin', 'trial/content-profile.json']),
+ 'patches': (['eboot.elf', 'eboot.bin', 'param.sfo', 'patches.xml', 'mod.xml'],
+ ['patches-merged.xml', 'patches.bin', 'patch_status.json']),
+ 'graphics': (['spartanport.ini', 'graphics_state.json', 'edit.json'],
+ [f'{state}/{name}' for state in ('good', 'tried', 'reverted')
+ for name in ('spartanport.ini', 'graphics_state.json', 'environment.json')]),
+ 'dxt': ([f'{name}.png' for name in DXT_NAMES],
+ [f'{name}.{fmt}' for name in DXT_NAMES for fmt in ('dxt1', 'dxt5')]),
+ 'icon': ([], ['window_icon.bmp']),
+}
+
+
+class RunnerFixtures(unittest.TestCase):
+ def test_every_case_has_its_files(self):
+ for case, (inputs, expected) in CASES.items():
+ for folder, names in (('input', inputs), ('expected', expected)):
+ for name in names:
+ path = FIXTURES / case / folder / name
+ self.assertTrue(path.is_file(), f'{case}/{folder}/{name}')
+
+ def test_dxt_strips_are_whole_blocks(self):
+ for name in DXT_NAMES:
+ # 64 x 64 pixels are 16 x 16 blocks of 8 (DXT1) and 16 (DXT5) bytes.
+ dxt1 = (FIXTURES / 'dxt/expected' / f'{name}.dxt1').read_bytes()
+ dxt5 = (FIXTURES / 'dxt/expected' / f'{name}.dxt5').read_bytes()
+ self.assertEqual((len(dxt1) % 8, len(dxt1)), (0, 16 * 16 * 8), name)
+ self.assertEqual((len(dxt5) % 16, len(dxt5)), (0, 16 * 16 * 16), name)
+
+ def test_content_profile_is_twenty_eight_bytes(self):
+ for sku in ('full', 'trial'):
+ data = (FIXTURES / 'content/expected' / sku / 'content.bin').read_bytes()
+ self.assertEqual(len(data), 28, sku)
+ self.assertEqual(data[:8], b'G3CONT01', sku)
+
+ def test_isa_table_of_the_c_loader_fixture_is_reproduced(self):
+ # The isa input is a G3PROBE5 image holding isa_fixture.bin's code: its table is that file.
+ table = (FIXTURES / 'isa/expected/isa-rewrite.bin').read_bytes()
+ self.assertEqual(table, (ROOT / 'tests/data/isa_fixture.bin').read_bytes())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/runner_fixtures.py b/tools/runner_fixtures.py
new file mode 100644
index 00000000..61a60734
--- /dev/null
+++ b/tools/runner_fixtures.py
@@ -0,0 +1,492 @@
+"""One-off: runs the Python runner's own modules (scripts/*.py) over synthetic inputs and stores what they
+write in launcher-rust/fixtures/runner//{input,expected}/, for the Rust runner's byte-for-byte parity tests.
+
+Run from anywhere: python tools/runner_fixtures.py (needs Pillow and iced_x86)
+
+Every case folder holds input/ (what the module reads) and expected/ (what it wrote). A game folder keeps its
+own layout inside input/ (eboot.bin, sce_module/*.prx, sce_sys/param.sfo, dvdroot_ps4/...), so a test can
+copy input/ as it is. Text outputs are written with LF; no expected file holds the temporary folder (the
+generator stops when one does).
+
+ prepare input: a synthetic eboot.bin SELF (two PT_LOAD, dynamic, library blob with imports and the four
+ relocation kinds, TLS) with sce_module/libc.prx (its _init_env is RET), sce_sys/param.sfo and a
+ dvdroot_ps4 folder. expected: boot.bin, eboot.elf, entry.bin, analysis.json (scripts/prepare.py).
+ link input: the same eboot.bin, libc.prx and libSceFios2.prx, and boot.bin from tests/test_probe.package
+ (a freshly prepared G3PROBE2 image with the eboot's imports). expected: boot-libc.bin and
+ libc-link.json (link_libc.py), boot-linked.bin and link.json (link_modules.py).
+ isa input: boot-linked.bin, a G3PROBE5 image (tests/test_isa_rewrite.boot_linked) whose one executable
+ segment is isa_rewrite.fixture_code(). expected: isa-rewrite.bin (equal to tests/data/isa_fixture.bin)
+ and sites.json, one entry per site: offset, length, kind and mode names, width, registers
+ (dst, src1, src2, base, index; 255 is none), mem_slot, scale, mem_flags, disp, original and
+ relocated bytes as hex.
+ content input: param.sfo with the four user-defined parameters. expected: full/ and trial/ with
+ content.bin and content-profile.json (content_profile.py).
+ patches input: eboot.elf holding the signatures of tests/test_patches.SignatureTests in its one PT_LOAD,
+ eboot.bin and param.sfo (CUSA01623, 01.02: the game folder patches.py looks the profile up in),
+ patches.xml (SignatureTests.XML with a TitleID so the mod file merges in) and mod.xml
+ (ModPatchTests.MOD). patches.py main() runs with --mod-patches mod.xml --extra "Found;Call;Missing;Data"
+ and SPARTAN_PATCHES_ONLY=1. expected: patches-merged.xml, patches.bin, patch_status.json (Missing is
+ skipped: its second signature is absent).
+ graphics input: spartanport.ini, graphics_state.json ({}) and edit.json, the keys written between the
+ launches. graphics_options.prepare runs three times, as in
+ test_unconfirmed_restart_is_tried_once_then_reverted; expected// holds the ini,
+ the state file and environment.json after each launch.
+ dxt input: gradient, edges, alpha, noise and flat, 64 x 64 RGBA PNGs (noise from a fixed seed).
+ expected: .dxt1 and .dxt5, the 4-row strips of glyphs.encode_region concatenated with
+ each DDS header (128 bytes) stripped (RGB for DXT1, RGBA for DXT5).
+ icon expected: window_icon.bmp, launcher/spartanport.png as run.py makes it (RGBA, 64 x 64 LANCZOS, BMP)."""
+import contextlib
+import io
+import json
+import os
+import random
+import shutil
+import struct
+import sys
+import tempfile
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+FIXTURES = ROOT / 'launcher-rust' / 'fixtures' / 'runner'
+WORK = Path(tempfile.mkdtemp(prefix='runner_fixtures_'))
+
+sys.path.insert(0, str(ROOT / 'tests'))
+sys.path.insert(0, str(ROOT / 'scripts'))
+import content_profile # noqa: E402
+import glyphs # noqa: E402
+import graphics_options # noqa: E402
+import isa_rewrite # noqa: E402
+import link_libc # noqa: E402
+import link_modules # noqa: E402
+import patches # noqa: E402
+import prepare as prepare_module # noqa: E402
+from PIL import Image # noqa: E402
+from test_isa_rewrite import boot_linked # noqa: E402
+from test_patches import ModPatchTests, SignatureTests # noqa: E402
+from test_probe import package # noqa: E402
+
+FS_LOAD = bytes.fromhex('64488b042500000000') # mov rax, fs:[0]: what the linkers rewrite to GS
+PARAMS = {'USER_DEFINED_PARAM_1': 13, 'USER_DEFINED_PARAM_2': 0, 'USER_DEFINED_PARAM_3': 7,
+ 'USER_DEFINED_PARAM_4': 0xffffffff}
+EDIT = {'render_resolution': '4K', 'engine_fps': '240', 'restart_unconfirmed': '1'}
+
+
+# -- Synthetic files ----------------------------------------------------------------------------------------
+
+def sfo_bytes(values):
+ """A param.sfo (PSF 1.1): strings (0x204) and unsigned integers (0x404), keys sorted."""
+ keys, data, entries = b'', b'', []
+ for key, value in sorted(values.items()):
+ if isinstance(value, int):
+ fmt, raw, capacity = 0x404, struct.pack('', '\n CUSA01623', 1)
+ put(inputs / 'patches.xml', xml.encode())
+ put(inputs / 'mod.xml', ModPatchTests.MOD.encode())
+
+ game = stage(WORK / 'patches-game', {'eboot.bin': (inputs / 'eboot.bin').read_bytes(),
+ 'sce_sys/param.sfo': (inputs / 'param.sfo').read_bytes()})
+ out = WORK / 'patches-out'
+ put(out / 'eboot.elf', (inputs / 'eboot.elf').read_bytes())
+ saved = patches.GAMES, sys.argv, os.environ.get('SPARTAN_PATCHES_ONLY')
+ patches.GAMES = [({'CUSA01623'}, WORK / 'patches.xml', '01.02', 'God of War III')]
+ put(WORK / 'patches.xml', xml.encode())
+ os.environ['SPARTAN_PATCHES_ONLY'] = '1'
+ sys.argv = ['patches.py', '--out', str(out), '--game-dir', str(game), '--mod-patches', str(inputs / 'mod.xml'),
+ '--extra', 'Found;Call;Missing;Data']
+ try:
+ quiet(patches.main)
+ finally:
+ patches.GAMES, sys.argv = saved[0], saved[1]
+ if saved[2] is None:
+ del os.environ['SPARTAN_PATCHES_ONLY']
+ else:
+ os.environ['SPARTAN_PATCHES_ONLY'] = saved[2]
+ put(expected / 'patches-merged.xml', lf(out / 'patches-merged.xml'))
+ put(expected / 'patches.bin', (out / 'patches.bin').read_bytes())
+ put(expected / 'patch_status.json', lf(out / 'patch_status.json'))
+
+
+def case_graphics():
+ inputs, expected = case_folder('graphics')
+ put(inputs / 'spartanport.ini', b'render_resolution=1440p\nengine_fps=120\n')
+ put(inputs / 'graphics_state.json', b'{}\n')
+ put(inputs / 'edit.json', text(EDIT).encode())
+ work = WORK / 'graphics'
+ work.mkdir()
+ ini, state = work / 'spartanport.ini', work / 'graphics_state.json'
+ shutil.copyfile(inputs / 'spartanport.ini', ini)
+ shutil.copyfile(inputs / 'graphics_state.json', state)
+ for step in ('good', 'tried', 'reverted'):
+ if step == 'tried':
+ graphics_options.write_keys(ini, EDIT) # the in-game menu asking for a restart
+ options, _ = graphics_options.prepare(ini, state)
+ put(expected / step / 'spartanport.ini', lf(ini))
+ put(expected / step / 'graphics_state.json', lf(state))
+ put(expected / step / 'environment.json', text(graphics_options.environment(options)).encode())
+
+
+def test_images():
+ """The five 64 x 64 RGBA test images."""
+ size = 64
+ noise = random.Random(20260101)
+ drawings = {
+ 'gradient': lambda x, y: (x * 4, y * 4, (x + y) * 2, 255),
+ 'edges': lambda x, y: ((255, 255, 255, 255) if ((x + 1) // 5 + (y + 3) // 7 + (x == y) * 2) % 2 == 0
+ else (20, 30, 40, 255)),
+ 'alpha': lambda x, y: (230, 90 + y * 2, 40, x * 4),
+ 'noise': lambda x, y: tuple(noise.randrange(256) for _ in range(4)),
+ 'flat': lambda x, y: (200, 100, 50, 255),
+ }
+ images = {}
+ for name, draw in drawings.items():
+ image = Image.new('RGBA', (size, size))
+ image.putdata([draw(x, y) for y in range(size) for x in range(size)])
+ images[name] = image
+ return images
+
+
+def strips(image, fourcc):
+ """glyphs.encode_region's 4-row strips of the whole picture, DDS headers stripped, concatenated."""
+ image = image.convert('RGBA' if fourcc == 'DXT5' else 'RGB')
+ width, height = image.size
+ encoded = b''
+ for y in range(0, height, 4):
+ strip = io.BytesIO()
+ image.crop((0, y, width, min(y + 4, height))).save(strip, 'DDS', pixel_format=fourcc)
+ encoded += strip.getvalue()[128:]
+ return encoded
+
+
+def encode_region_blocks(image, fourcc):
+ """The same bytes through glyphs.encode_region itself, over a texture holding just the top level."""
+ block = 16 if fourcc == 'DXT5' else 8
+ width, height = image.size
+ data = bytearray(128 + (width // 4) * (height // 4) * block)
+ struct.pack_into('