From c694bb3f43aa47fe34896ec2d08bbeae205c3ba7 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 16 Apr 2026 23:11:49 +0200 Subject: [PATCH] Initial commit: xenia-rs workspace for Xbox 360 RE Rust reimplementation of the xenia Xbox 360 emulator targeting reverse- engineering and preservation, initially scoped to Project Sylpheed. Includes: - XEX2 loader (LZX decompression, AES decryption, PE parsing) - XISO / XGD2 disc image VFS - PPC interpreter with 200+ opcodes and VMX128 decoding - Static analyzer: functions, cross-references, labels, asm + SQLite output - HLE kernel covering the xboxkrnl/xam subset used by Sylpheed init - Debugger with in-memory and SQLite-backed execution tracing - `xenia-rs` CLI with extract/dis/exec commands that produce cumulative, superset SQLite databases and opt-in instruction/import/branch traces Co-Authored-By: Claude Opus 4.7 --- .gitignore | 4 + Cargo.lock | 861 +++++++ Cargo.toml | 47 + README.md | 132 ++ crates/xenia-analysis/Cargo.toml | 13 + crates/xenia-analysis/build.rs | 87 + crates/xenia-analysis/src/db.rs | 727 ++++++ crates/xenia-analysis/src/formatter.rs | 318 +++ crates/xenia-analysis/src/func.rs | 444 ++++ crates/xenia-analysis/src/lib.rs | 10 + crates/xenia-analysis/src/ordinals.rs | 1 + crates/xenia-analysis/src/ppc.rs | 1376 +++++++++++ crates/xenia-analysis/src/xref.rs | 296 +++ crates/xenia-app/Cargo.toml | 28 + crates/xenia-app/src/main.rs | 812 +++++++ crates/xenia-apu/Cargo.toml | 10 + crates/xenia-apu/src/lib.rs | 16 + crates/xenia-cpu/Cargo.toml | 12 + crates/xenia-cpu/src/context.rs | 191 ++ crates/xenia-cpu/src/decoder.rs | 819 +++++++ crates/xenia-cpu/src/disasm.rs | 276 +++ crates/xenia-cpu/src/interpreter.rs | 2529 +++++++++++++++++++++ crates/xenia-cpu/src/lib.rs | 9 + crates/xenia-cpu/src/opcode.rs | 196 ++ crates/xenia-debugger/Cargo.toml | 13 + crates/xenia-debugger/src/breakpoint.rs | 7 + crates/xenia-debugger/src/lib.rs | 125 + crates/xenia-debugger/src/trace.rs | 9 + crates/xenia-gpu/Cargo.toml | 13 + crates/xenia-gpu/src/command_processor.rs | 17 + crates/xenia-gpu/src/lib.rs | 21 + crates/xenia-gpu/src/register_file.rs | 28 + crates/xenia-hid/Cargo.toml | 10 + crates/xenia-hid/src/lib.rs | 47 + crates/xenia-kernel/Cargo.toml | 13 + crates/xenia-kernel/src/exports.rs | 763 +++++++ crates/xenia-kernel/src/lib.rs | 6 + crates/xenia-kernel/src/objects.rs | 12 + crates/xenia-kernel/src/state.rs | 159 ++ crates/xenia-kernel/src/xam.rs | 253 +++ crates/xenia-memory/Cargo.toml | 17 + crates/xenia-memory/src/access.rs | 47 + crates/xenia-memory/src/heap.rs | 265 +++ crates/xenia-memory/src/lib.rs | 31 + crates/xenia-memory/src/mmio.rs | 27 + crates/xenia-memory/src/page_table.rs | 122 + crates/xenia-memory/src/platform.rs | 98 + crates/xenia-types/Cargo.toml | 11 + crates/xenia-types/src/endian.rs | 122 + crates/xenia-types/src/error.rs | 27 + crates/xenia-types/src/lib.rs | 6 + crates/xenia-types/src/vec128.rs | 206 ++ crates/xenia-vfs/Cargo.toml | 12 + crates/xenia-vfs/src/device.rs | 54 + crates/xenia-vfs/src/disc_image.rs | 185 ++ crates/xenia-vfs/src/lib.rs | 33 + crates/xenia-xex/Cargo.toml | 17 + crates/xenia-xex/build.rs | 1 + crates/xenia-xex/src/header.rs | 128 ++ crates/xenia-xex/src/lib.rs | 6 + crates/xenia-xex/src/loader.rs | 571 +++++ crates/xenia-xex/src/lzx.rs | 692 ++++++ crates/xenia-xex/src/pe.rs | 68 + 63 files changed, 13456 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 crates/xenia-analysis/Cargo.toml create mode 100644 crates/xenia-analysis/build.rs create mode 100644 crates/xenia-analysis/src/db.rs create mode 100644 crates/xenia-analysis/src/formatter.rs create mode 100644 crates/xenia-analysis/src/func.rs create mode 100644 crates/xenia-analysis/src/lib.rs create mode 100644 crates/xenia-analysis/src/ordinals.rs create mode 100644 crates/xenia-analysis/src/ppc.rs create mode 100644 crates/xenia-analysis/src/xref.rs create mode 100644 crates/xenia-app/Cargo.toml create mode 100644 crates/xenia-app/src/main.rs create mode 100644 crates/xenia-apu/Cargo.toml create mode 100644 crates/xenia-apu/src/lib.rs create mode 100644 crates/xenia-cpu/Cargo.toml create mode 100644 crates/xenia-cpu/src/context.rs create mode 100644 crates/xenia-cpu/src/decoder.rs create mode 100644 crates/xenia-cpu/src/disasm.rs create mode 100644 crates/xenia-cpu/src/interpreter.rs create mode 100644 crates/xenia-cpu/src/lib.rs create mode 100644 crates/xenia-cpu/src/opcode.rs create mode 100644 crates/xenia-debugger/Cargo.toml create mode 100644 crates/xenia-debugger/src/breakpoint.rs create mode 100644 crates/xenia-debugger/src/lib.rs create mode 100644 crates/xenia-debugger/src/trace.rs create mode 100644 crates/xenia-gpu/Cargo.toml create mode 100644 crates/xenia-gpu/src/command_processor.rs create mode 100644 crates/xenia-gpu/src/lib.rs create mode 100644 crates/xenia-gpu/src/register_file.rs create mode 100644 crates/xenia-hid/Cargo.toml create mode 100644 crates/xenia-hid/src/lib.rs create mode 100644 crates/xenia-kernel/Cargo.toml create mode 100644 crates/xenia-kernel/src/exports.rs create mode 100644 crates/xenia-kernel/src/lib.rs create mode 100644 crates/xenia-kernel/src/objects.rs create mode 100644 crates/xenia-kernel/src/state.rs create mode 100644 crates/xenia-kernel/src/xam.rs create mode 100644 crates/xenia-memory/Cargo.toml create mode 100644 crates/xenia-memory/src/access.rs create mode 100644 crates/xenia-memory/src/heap.rs create mode 100644 crates/xenia-memory/src/lib.rs create mode 100644 crates/xenia-memory/src/mmio.rs create mode 100644 crates/xenia-memory/src/page_table.rs create mode 100644 crates/xenia-memory/src/platform.rs create mode 100644 crates/xenia-types/Cargo.toml create mode 100644 crates/xenia-types/src/endian.rs create mode 100644 crates/xenia-types/src/error.rs create mode 100644 crates/xenia-types/src/lib.rs create mode 100644 crates/xenia-types/src/vec128.rs create mode 100644 crates/xenia-vfs/Cargo.toml create mode 100644 crates/xenia-vfs/src/device.rs create mode 100644 crates/xenia-vfs/src/disc_image.rs create mode 100644 crates/xenia-vfs/src/lib.rs create mode 100644 crates/xenia-xex/Cargo.toml create mode 100644 crates/xenia-xex/build.rs create mode 100644 crates/xenia-xex/src/header.rs create mode 100644 crates/xenia-xex/src/lib.rs create mode 100644 crates/xenia-xex/src/loader.rs create mode 100644 crates/xenia-xex/src/lzx.rs create mode 100644 crates/xenia-xex/src/pe.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1eab3d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target/ +*.iso +*.xiso +*.db \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..45e3250 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,861 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xenia-analysis" +version = "0.1.0" +dependencies = [ + "anyhow", + "rusqlite", + "serde", + "tracing", + "xenia-xex", +] + +[[package]] +name = "xenia-app" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", + "xenia-analysis", + "xenia-apu", + "xenia-cpu", + "xenia-debugger", + "xenia-gpu", + "xenia-hid", + "xenia-kernel", + "xenia-memory", + "xenia-types", + "xenia-vfs", + "xenia-xex", +] + +[[package]] +name = "xenia-apu" +version = "0.1.0" +dependencies = [ + "thiserror", + "tracing", + "xenia-types", +] + +[[package]] +name = "xenia-cpu" +version = "0.1.0" +dependencies = [ + "bitflags", + "thiserror", + "tracing", + "xenia-memory", + "xenia-types", +] + +[[package]] +name = "xenia-debugger" +version = "0.1.0" +dependencies = [ + "anyhow", + "thiserror", + "tracing", + "xenia-cpu", + "xenia-memory", + "xenia-types", +] + +[[package]] +name = "xenia-gpu" +version = "0.1.0" +dependencies = [ + "anyhow", + "byteorder", + "thiserror", + "tracing", + "xenia-memory", + "xenia-types", +] + +[[package]] +name = "xenia-hid" +version = "0.1.0" +dependencies = [ + "thiserror", + "tracing", + "xenia-types", +] + +[[package]] +name = "xenia-kernel" +version = "0.1.0" +dependencies = [ + "anyhow", + "thiserror", + "tracing", + "xenia-cpu", + "xenia-memory", + "xenia-types", +] + +[[package]] +name = "xenia-memory" +version = "0.1.0" +dependencies = [ + "bitflags", + "libc", + "thiserror", + "tracing", + "windows-sys 0.59.0", + "xenia-types", +] + +[[package]] +name = "xenia-types" +version = "0.1.0" +dependencies = [ + "bitflags", + "byteorder", + "serde", + "thiserror", +] + +[[package]] +name = "xenia-vfs" +version = "0.1.0" +dependencies = [ + "anyhow", + "byteorder", + "thiserror", + "tracing", + "xenia-types", +] + +[[package]] +name = "xenia-xex" +version = "0.1.0" +dependencies = [ + "aes", + "anyhow", + "byteorder", + "serde", + "serde_json", + "thiserror", + "tracing", + "xenia-memory", + "xenia-types", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3510aa2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,47 @@ +[workspace] +resolver = "2" +members = [ + "crates/xenia-types", + "crates/xenia-memory", + "crates/xenia-cpu", + "crates/xenia-xex", + "crates/xenia-vfs", + "crates/xenia-kernel", + "crates/xenia-gpu", + "crates/xenia-apu", + "crates/xenia-hid", + "crates/xenia-debugger", + "crates/xenia-analysis", + "crates/xenia-app", +] + +[workspace.package] +version = "0.1.0" +edition = "2024" +license = "BSD-3-Clause" + +[workspace.dependencies] +# Shared types +xenia-types = { path = "crates/xenia-types" } +xenia-memory = { path = "crates/xenia-memory" } +xenia-cpu = { path = "crates/xenia-cpu" } +xenia-xex = { path = "crates/xenia-xex" } +xenia-vfs = { path = "crates/xenia-vfs" } +xenia-kernel = { path = "crates/xenia-kernel" } +xenia-gpu = { path = "crates/xenia-gpu" } +xenia-apu = { path = "crates/xenia-apu" } +xenia-hid = { path = "crates/xenia-hid" } +xenia-debugger = { path = "crates/xenia-debugger" } +xenia-analysis = { path = "crates/xenia-analysis" } + +# External dependencies +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +bitflags = "2" +byteorder = "1" +thiserror = "2" +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +aes = "0.8" +rusqlite = { version = "0.31", features = ["bundled"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ecd809 --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# xenia-rs + +Rust reimplementation of the Xbox 360 emulator [xenia](https://github.com/xenia-project/xenia), +focused on **reverse-engineering and preservation** rather than full-speed play. +The initial target is *Project Sylpheed — Arc of Deception*; getting the title +disassembled, traced, and far enough into its init path to understand its engine. + +Heavy cross-reference to [xenia-canary](https://github.com/xenia-canary/xenia-canary) +for CPU context setup, kernel export behavior, and XEX loading semantics. + +## Status + +- **XEX loader** — XEX2 header parsing, LZX decompression, AES decryption, PE section parsing. +- **VFS / XISO** — XGD2 dual-layer disc images (with the 0x0FD90000 partition offset). +- **PPC interpreter** — 200+ opcodes, PowerPC 32/64-bit GPR/FPR, VMX128 decoding. +- **Static analyzer** — function discovery (prolog/epilog heuristics), cross-references, labels, + save/restore helper detection, assembly text + SQLite database output. +- **Kernel HLE** — minimal subset driving Project Sylpheed: ~170 xboxkrnl + xam exports + (critical sections, events, TLS, virtual memory, Vd stubs, XAM input/user/content). +- **Debugger** — in-memory step/break, SQLite execution + import-call + branch tracing. + +Not yet: GPU (xenos/xe-shader), APU audio, HID, kernel scheduler, full threading, +exception delivery. + +## Workspace + +``` +crates/ + xenia-types # shared primitive types, bitflags + xenia-memory # guest memory, paged allocator, page table + xenia-cpu # PPC decoder, interpreter, context + xenia-xex # XEX2 loader, PE parser, LZX, AES + xenia-vfs # XISO / disc-image reader + xenia-kernel # HLE kernel state, exports, XAM + xenia-gpu # (stub) Xenos command processor + xenia-apu # (stub) XAudio + xenia-hid # (stub) XInput + xenia-debugger # in-memory trace, breakpoints, step modes + xenia-analysis # function/xref analysis, assembly formatter, SQLite DbWriter + xenia-app # `xenia-rs` CLI binary +``` + +## CLI + +Build: +```sh +cargo build --release +``` + +The binary `xenia-rs` accepts XEX2 files or ISO / XISO disc images as input +(the loader auto-detects discs and extracts `default.xex`). + +### `info` / `browse` / `disasm` + +Quick header / disc / first-N-instructions inspection. See `--help`. + +### `extract` — unpack PE + metadata + +```sh +xenia-rs extract [-o ] [--db ] +``` + +Writes `.pe` (decompressed/decrypted PE image) and `.xex.json` +(header metadata). With `--db`, also emits a SQLite database containing the +**base tables**: `metadata`, `sections`, `imports`. + +### `dis` — full disassembly + +```sh +xenia-rs dis [-o ] [--db ] [--quiet] +``` + +Runs function + cross-reference analysis and produces: +- assembly text to stdout or `-o ` (unless `--quiet`) +- optional SQLite DB with the **base tables + disasm tables**: + `functions`, `labels`, `instructions`, `xrefs` + +### `exec` — interpret with tracing + +```sh +xenia-rs exec [-n ] [--db ] + [--trace-instructions] [--trace-imports] [--trace-branches] +``` + +Loads the title, initializes CPU state per xenia-canary, intercepts import +thunks with HLE kernel calls, and interprets from the entry point. Without +`-n`, runs until halt/fault. With `--db`, produces a DB that is a **superset +of `dis --db`** plus opt-in trace tables: + +| flag | table | rows | +|-------------------------|----------------|---------------------------------------------------------| +| `--trace-instructions` | `exec_trace` | one row per interpreted instruction (PC, r3/r4, LR, SP) | +| `--trace-imports` | `import_calls` | one row per kernel/XAM call (module, ordinal, args) | +| `--trace-branches` | `branch_trace` | taken branches classified as `call`/`return`/`jump`/`branch` | + +### Cumulative DB layering + +Each command's DB is a superset of the previous. A single +`xenia-rs exec --db full.db --trace-instructions --trace-imports --trace-branches` +produces the full picture in one pass — base tables, complete static +disassembly, and runtime traces correlatable by address/cycle. + +## Performance knobs + +- **`XENIA_DB_BATCH_SIZE`** — rows per streaming commit / trace-buffer flush + (default `100_000`). Lower values reduce memory use; higher values reduce + fsync overhead on slow disks. + +The DB writer uses `journal_mode=OFF`, `synchronous=OFF`, `locking_mode=EXCLUSIVE` +and commits in batches; no `ANALYZE` is run at finalize. Indices are created +after bulk insertion with progress messages. + +## Example queries + +```sql +-- Top 20 kernel functions called during early init +SELECT name, COUNT(*) FROM import_calls GROUP BY name ORDER BY 2 DESC LIMIT 20; + +-- All basic-block leaders (targets of taken branches) not already labelled +SELECT DISTINCT bt.target +FROM branch_trace bt LEFT JOIN labels l ON l.address = bt.target +WHERE l.address IS NULL; + +-- Correlate a traced call site with its static disassembly +SELECT et.cycle, i.disasm, i.ext_disasm +FROM exec_trace et JOIN instructions i ON i.address = et.address +WHERE et.address = 0x824AB748 ORDER BY et.cycle; +``` + +## License + +BSD-3-Clause, matching upstream xenia. diff --git a/crates/xenia-analysis/Cargo.toml b/crates/xenia-analysis/Cargo.toml new file mode 100644 index 0000000..942542a --- /dev/null +++ b/crates/xenia-analysis/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "xenia-analysis" +version.workspace = true +edition.workspace = true +license.workspace = true +build = "build.rs" + +[dependencies] +xenia-xex = { workspace = true } +serde = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +rusqlite = { workspace = true } diff --git a/crates/xenia-analysis/build.rs b/crates/xenia-analysis/build.rs new file mode 100644 index 0000000..0a8ef34 --- /dev/null +++ b/crates/xenia-analysis/build.rs @@ -0,0 +1,87 @@ +//! Build script: parse xenia's xboxkrnl_table.inc and xam_table.inc to generate +//! ordinal->name lookup tables at compile time. + +use std::env; +use std::fs; +use std::io::Write; +use std::path::Path; + +fn parse_table(path: &Path) -> Vec<(u32, String, String)> { + let content = match fs::read_to_string(path) { + Ok(c) => c, + Err(e) => { + eprintln!("cargo:warning=could not read {}: {}", path.display(), e); + return Vec::new(); + } + }; + + let mut entries = Vec::new(); + for line in content.lines() { + let line = line.trim(); + // XE_EXPORT(module, 0xNNNNNNNN, Name, kType), + if !line.starts_with("XE_EXPORT(") { continue; } + let inner = match line.strip_prefix("XE_EXPORT(").and_then(|s| s.strip_suffix("),")) { + Some(s) => s, + None => continue, + }; + let parts: Vec<&str> = inner.splitn(4, ',').map(|s| s.trim()).collect(); + if parts.len() < 4 { continue; } + let module = parts[0].to_string(); + let ordinal = match u32::from_str_radix(parts[1].trim_start_matches("0x").trim_start_matches("0X"), 16) { + Ok(n) => n, + Err(_) => continue, + }; + let name = parts[2].to_string(); + entries.push((ordinal, name, module)); + } + entries +} + +fn main() { + let out_dir = env::var("OUT_DIR").unwrap(); + let dest = Path::new(&out_dir).join("ordinals.rs"); + let mut f = fs::File::create(&dest).unwrap(); + + // Locate xenia tables relative to the workspace root + // crates/xenia-analysis/ -> ../../ -> workspace root -> ../xenia-canary/ + let manifest = env::var("CARGO_MANIFEST_DIR").unwrap(); + let workspace_root = Path::new(&manifest).parent().unwrap().parent().unwrap(); + let project_root = workspace_root.parent().unwrap(); + + let krnl_path = project_root + .join("xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_table.inc"); + let xam_path = project_root + .join("xenia-canary/src/xenia/kernel/xam/xam_table.inc"); + + println!("cargo:rerun-if-changed={}", krnl_path.display()); + println!("cargo:rerun-if-changed={}", xam_path.display()); + + let krnl = parse_table(&krnl_path); + let xam = parse_table(&xam_path); + + writeln!(f, "/// Auto-generated from xenia's export tables.").unwrap(); + writeln!(f, "pub fn resolve_ordinal(lib: &str, ordinal: u16) -> Option<&'static str> {{").unwrap(); + writeln!(f, " match lib {{").unwrap(); + + // xboxkrnl.exe + writeln!(f, " \"xboxkrnl.exe\" => match ordinal {{").unwrap(); + for (ord, name, _) in &krnl { + writeln!(f, " 0x{ord:04X} => Some(\"{name}\"),").unwrap(); + } + writeln!(f, " _ => None,").unwrap(); + writeln!(f, " }},").unwrap(); + + // xam.xex + writeln!(f, " \"xam.xex\" => match ordinal {{").unwrap(); + for (ord, name, _) in &xam { + writeln!(f, " 0x{ord:04X} => Some(\"{name}\"),").unwrap(); + } + writeln!(f, " _ => None,").unwrap(); + writeln!(f, " }},").unwrap(); + + writeln!(f, " _ => None,").unwrap(); + writeln!(f, " }}").unwrap(); + writeln!(f, "}}").unwrap(); + + eprintln!("ordinals.rs: {} xboxkrnl + {} xam entries", krnl.len(), xam.len()); +} diff --git a/crates/xenia-analysis/src/db.rs b/crates/xenia-analysis/src/db.rs new file mode 100644 index 0000000..5a6c464 --- /dev/null +++ b/crates/xenia-analysis/src/db.rs @@ -0,0 +1,727 @@ +//! SQLite database writer for xenia-rs. +//! +//! Layered, streaming writes shared by `extract`, `dis`, and `exec`. +//! Each command's output is a superset of the previous: +//! - `extract --db` -> base tables (metadata, sections, imports) +//! - `dis --db` -> base + disasm tables (functions, labels, instructions, xrefs) +//! - `exec --db` -> base + disasm + opt-in trace tables (exec_trace, import_calls, branch_trace) +//! +//! Performance: streaming commits every 100k rows, no end-of-run ANALYZE, +//! progress messages before each index build. +//! +//! Trace kind values for `branch_trace.kind`: +//! - "call" : any branch with LK set (raw & 1 == 1) +//! - "return" : bclrx without LK +//! - "jump" : bcctrx without LK +//! - "branch" : bx/bcx without LK + +use std::collections::HashMap; +use std::path::Path; + +use rusqlite::{Connection, params}; + +use crate::func::FuncAnalysis; +use crate::xref::{XrefMap, resolve_source_label}; +use crate::formatter::DisasmInfo; + +const DEFAULT_BATCH_SIZE: u64 = 100_000; + +/// Number of rows per DB commit / trace buffer flush. +/// Configurable via the `XENIA_DB_BATCH_SIZE` env var (default 100_000). +/// Used for: +/// - `instructions` and `xrefs` streaming commits in `write_disasm` +/// - `exec_trace` and `branch_trace` buffer thresholds during exec +/// (`import_calls` always flushes at 1000 — low volume, not worth scaling.) +fn batch_size() -> u64 { + use std::sync::OnceLock; + static CACHED: OnceLock = OnceLock::new(); + *CACHED.get_or_init(|| { + std::env::var("XENIA_DB_BATCH_SIZE") + .ok() + .and_then(|s| s.parse::().ok()) + .filter(|&n| n > 0) + .unwrap_or(DEFAULT_BATCH_SIZE) + }) +} + +pub struct ExecTraceEntry { + pub address: u32, + pub cycle: u64, + pub r3: u64, + pub r4: u64, + pub lr: u64, + pub sp: u64, +} + +pub struct ImportCallEntry { + pub address: u32, + pub cycle: u64, + pub module: String, + pub ordinal: u16, + pub name: String, + pub arg_r3: u64, + pub arg_r4: u64, + pub arg_r5: u64, + pub arg_r6: u64, + pub return_value: u64, +} + +pub struct BranchTraceEntry { + pub source: u32, + pub target: u32, + pub cycle: u64, + pub kind: &'static str, + pub lr: u64, +} + +pub struct DbWriter { + conn: Connection, + exec_buffer: Vec, + import_buffer: Vec, + branch_buffer: Vec, + exec_count: u64, + import_count: u64, + branch_count: u64, + trace_instructions: bool, + trace_imports: bool, + trace_branches: bool, +} + +impl DbWriter { + /// Open a fresh database at `path`, removing any existing file first. + pub fn open_fresh(path: &Path) -> anyhow::Result { + if path.exists() { + std::fs::remove_file(path)?; + } + let conn = Connection::open(path)?; + conn.execute_batch(" + PRAGMA journal_mode = OFF; + PRAGMA synchronous = OFF; + PRAGMA locking_mode = EXCLUSIVE; + PRAGMA temp_store = MEMORY; + ")?; + let cap = batch_size() as usize; + Ok(Self { + conn, + exec_buffer: Vec::with_capacity(cap), + import_buffer: Vec::with_capacity(1024), + branch_buffer: Vec::with_capacity(cap), + exec_count: 0, + import_count: 0, + branch_count: 0, + trace_instructions: false, + trace_imports: false, + trace_branches: false, + }) + } + + // ── Base layer (written by extract/dis/exec) ───────────────────────────── + + /// Write metadata, sections, imports tables and their indices. + pub fn write_base(&mut self, info: &DisasmInfo) -> anyhow::Result<()> { + self.conn.execute_batch(" + CREATE TABLE metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + + CREATE TABLE sections ( + name TEXT NOT NULL, + virtual_address INTEGER NOT NULL, + virtual_size INTEGER NOT NULL, + raw_offset INTEGER NOT NULL, + raw_size INTEGER NOT NULL, + flags INTEGER NOT NULL, + is_code BOOLEAN NOT NULL + ); + + CREATE TABLE imports ( + library TEXT NOT NULL, + ordinal INTEGER NOT NULL, + name TEXT, + record_type INTEGER NOT NULL, + address INTEGER NOT NULL + ); + ")?; + + insert_metadata(&self.conn, info)?; + insert_sections(&self.conn, info.sections)?; + insert_imports(&self.conn, info)?; + + self.conn.execute_batch(" + CREATE INDEX idx_imports_library ON imports(library); + CREATE INDEX idx_imports_name ON imports(name); + ")?; + Ok(()) + } + + // ── Disasm layer (written by dis/exec) ─────────────────────────────────── + + /// Write functions, labels, instructions, xrefs tables and indices. + pub fn write_disasm( + &mut self, + pe: &[u8], + info: &DisasmInfo, + func_analysis: &FuncAnalysis, + labels: &HashMap, + xrefs: &XrefMap, + ) -> anyhow::Result<()> { + self.conn.execute_batch(" + CREATE TABLE functions ( + address INTEGER PRIMARY KEY, + name TEXT NOT NULL, + end_address INTEGER NOT NULL, + frame_size INTEGER NOT NULL, + saved_gprs INTEGER NOT NULL, + is_leaf BOOLEAN NOT NULL, + is_saverestore BOOLEAN NOT NULL + ); + + CREATE TABLE labels ( + address INTEGER PRIMARY KEY, + name TEXT NOT NULL, + kind TEXT NOT NULL + ); + + CREATE TABLE instructions ( + address INTEGER PRIMARY KEY, + raw INTEGER NOT NULL, + mnemonic TEXT NOT NULL, + operands TEXT NOT NULL, + disasm TEXT NOT NULL, + ext_mnemonic TEXT, + ext_operands TEXT, + ext_disasm TEXT, + section TEXT NOT NULL, + function INTEGER, + label TEXT + ); + + CREATE TABLE xrefs ( + source INTEGER NOT NULL, + target INTEGER NOT NULL, + kind TEXT NOT NULL, + instruction TEXT, + source_func INTEGER, + source_label TEXT, + target_label TEXT + ); + ")?; + + insert_functions(&self.conn, func_analysis, labels)?; + insert_labels(&self.conn, labels)?; + insert_instructions_streaming(&self.conn, pe, info, func_analysis, labels)?; + insert_xrefs_streaming(&self.conn, xrefs, pe, info.image_base, func_analysis, labels)?; + + let indices = [ + ("idx_functions_name", "CREATE INDEX idx_functions_name ON functions(name)"), + ("idx_labels_kind", "CREATE INDEX idx_labels_kind ON labels(kind)"), + ("idx_labels_name", "CREATE INDEX idx_labels_name ON labels(name)"), + ("idx_instructions_function", "CREATE INDEX idx_instructions_function ON instructions(function)"), + ("idx_instructions_mnemonic", "CREATE INDEX idx_instructions_mnemonic ON instructions(mnemonic)"), + ("idx_instructions_ext_mnemonic","CREATE INDEX idx_instructions_ext_mnemonic ON instructions(ext_mnemonic)"), + ("idx_instructions_section", "CREATE INDEX idx_instructions_section ON instructions(section)"), + ("idx_instructions_label", "CREATE INDEX idx_instructions_label ON instructions(label)"), + ("idx_xrefs_target", "CREATE INDEX idx_xrefs_target ON xrefs(target)"), + ("idx_xrefs_source", "CREATE INDEX idx_xrefs_source ON xrefs(source)"), + ("idx_xrefs_source_func", "CREATE INDEX idx_xrefs_source_func ON xrefs(source_func)"), + ("idx_xrefs_kind", "CREATE INDEX idx_xrefs_kind ON xrefs(kind)"), + ("idx_xrefs_instruction", "CREATE INDEX idx_xrefs_instruction ON xrefs(instruction)"), + ("idx_xrefs_target_label", "CREATE INDEX idx_xrefs_target_label ON xrefs(target_label)"), + ]; + for (name, sql) in indices { + eprintln!("[db] creating {name}..."); + self.conn.execute_batch(sql)?; + } + Ok(()) + } + + // ── Trace layer (written by exec when flags enabled) ───────────────────── + + /// Create the opt-in trace tables. No-op if all flags are false. + pub fn prepare_trace_tables( + &mut self, + trace_instructions: bool, + trace_imports: bool, + trace_branches: bool, + ) -> anyhow::Result<()> { + self.trace_instructions = trace_instructions; + self.trace_imports = trace_imports; + self.trace_branches = trace_branches; + + if trace_instructions { + self.conn.execute_batch(" + CREATE TABLE IF NOT EXISTS exec_trace ( + id INTEGER PRIMARY KEY, + address INTEGER NOT NULL, + cycle INTEGER NOT NULL, + r3 INTEGER NOT NULL, + r4 INTEGER NOT NULL, + lr INTEGER NOT NULL, + sp INTEGER NOT NULL + ); + DELETE FROM exec_trace; + ")?; + } + + if trace_imports { + self.conn.execute_batch(" + CREATE TABLE IF NOT EXISTS import_calls ( + id INTEGER PRIMARY KEY, + address INTEGER NOT NULL, + cycle INTEGER NOT NULL, + module TEXT NOT NULL, + ordinal INTEGER NOT NULL, + name TEXT NOT NULL, + arg_r3 INTEGER NOT NULL, + arg_r4 INTEGER NOT NULL, + arg_r5 INTEGER NOT NULL, + arg_r6 INTEGER NOT NULL, + return_value INTEGER NOT NULL + ); + DELETE FROM import_calls; + ")?; + } + + if trace_branches { + self.conn.execute_batch(" + CREATE TABLE IF NOT EXISTS branch_trace ( + id INTEGER PRIMARY KEY, + cycle INTEGER NOT NULL, + source INTEGER NOT NULL, + target INTEGER NOT NULL, + kind TEXT NOT NULL, + lr INTEGER NOT NULL + ); + DELETE FROM branch_trace; + ")?; + } + + Ok(()) + } + + pub fn log_instruction(&mut self, entry: ExecTraceEntry) { + if !self.trace_instructions { return; } + self.exec_buffer.push(entry); + if self.exec_buffer.len() as u64 >= batch_size() { + self.flush_exec(); + } + } + + pub fn log_import_call(&mut self, entry: ImportCallEntry) { + if !self.trace_imports { return; } + self.import_buffer.push(entry); + if self.import_buffer.len() >= 1000 { + self.flush_imports(); + } + } + + pub fn log_branch(&mut self, entry: BranchTraceEntry) { + if !self.trace_branches { return; } + self.branch_buffer.push(entry); + if self.branch_buffer.len() as u64 >= batch_size() { + self.flush_branches(); + } + } + + fn flush_exec(&mut self) { + if self.exec_buffer.is_empty() { return; } + let tx = self.conn.unchecked_transaction().unwrap(); + { + let mut stmt = tx.prepare_cached( + "INSERT INTO exec_trace (address, cycle, r3, r4, lr, sp) VALUES (?1, ?2, ?3, ?4, ?5, ?6)" + ).unwrap(); + for e in &self.exec_buffer { + stmt.execute(params![ + e.address as i64, + e.cycle as i64, + e.r3 as i64, + e.r4 as i64, + e.lr as i64, + e.sp as i64, + ]).ok(); + } + } + tx.commit().ok(); + self.exec_count += self.exec_buffer.len() as u64; + self.exec_buffer.clear(); + } + + fn flush_imports(&mut self) { + if self.import_buffer.is_empty() { return; } + let tx = self.conn.unchecked_transaction().unwrap(); + { + let mut stmt = tx.prepare_cached( + "INSERT INTO import_calls (address, cycle, module, ordinal, name, arg_r3, arg_r4, arg_r5, arg_r6, return_value) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)" + ).unwrap(); + for e in &self.import_buffer { + stmt.execute(params![ + e.address as i64, + e.cycle as i64, + e.module, + e.ordinal as i64, + e.name, + e.arg_r3 as i64, + e.arg_r4 as i64, + e.arg_r5 as i64, + e.arg_r6 as i64, + e.return_value as i64, + ]).ok(); + } + } + tx.commit().ok(); + self.import_count += self.import_buffer.len() as u64; + self.import_buffer.clear(); + } + + fn flush_branches(&mut self) { + if self.branch_buffer.is_empty() { return; } + let tx = self.conn.unchecked_transaction().unwrap(); + { + let mut stmt = tx.prepare_cached( + "INSERT INTO branch_trace (cycle, source, target, kind, lr) VALUES (?1, ?2, ?3, ?4, ?5)" + ).unwrap(); + for e in &self.branch_buffer { + stmt.execute(params![ + e.cycle as i64, + e.source as i64, + e.target as i64, + e.kind, + e.lr as i64, + ]).ok(); + } + } + tx.commit().ok(); + self.branch_count += self.branch_buffer.len() as u64; + self.branch_buffer.clear(); + } + + /// Flush remaining trace buffers and create their indices. + pub fn finalize_traces(&mut self) -> anyhow::Result<()> { + self.flush_exec(); + self.flush_imports(); + self.flush_branches(); + + if self.trace_instructions { + eprintln!("[db] creating idx_exec_trace_address..."); + self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_exec_trace_address ON exec_trace(address);")?; + eprintln!("[db] creating idx_exec_trace_cycle..."); + self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_exec_trace_cycle ON exec_trace(cycle);")?; + } + if self.trace_imports { + eprintln!("[db] creating idx_import_calls_name..."); + self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_import_calls_name ON import_calls(name);")?; + eprintln!("[db] creating idx_import_calls_cycle..."); + self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_import_calls_cycle ON import_calls(cycle);")?; + } + if self.trace_branches { + eprintln!("[db] creating idx_branch_trace_source..."); + self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_branch_trace_source ON branch_trace(source);")?; + eprintln!("[db] creating idx_branch_trace_target..."); + self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_branch_trace_target ON branch_trace(target);")?; + eprintln!("[db] creating idx_branch_trace_kind..."); + self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_branch_trace_kind ON branch_trace(kind);")?; + eprintln!("[db] creating idx_branch_trace_cycle..."); + self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_branch_trace_cycle ON branch_trace(cycle);")?; + } + + eprintln!( + "[db] trace totals: {} instructions, {} imports, {} branches", + self.exec_count, self.import_count, self.branch_count + ); + Ok(()) + } +} + +/// Backwards-compatible wrapper that writes the full base + disasm layers. +pub fn write_db( + path: &Path, + pe: &[u8], + info: &DisasmInfo, + func_analysis: &FuncAnalysis, + labels: &HashMap, + _import_map: &HashMap, + xrefs: &XrefMap, +) -> anyhow::Result<()> { + let mut w = DbWriter::open_fresh(path)?; + w.write_base(info)?; + w.write_disasm(pe, info, func_analysis, labels, xrefs)?; + Ok(()) +} + +// ── Helpers ──────────────────────────────────────────────────────────────── + +fn insert_metadata(conn: &Connection, info: &DisasmInfo) -> anyhow::Result<()> { + let mut stmt = conn.prepare("INSERT INTO metadata (key, value) VALUES (?1, ?2)")?; + stmt.execute(params!["image_base", format!("0x{:08X}", info.image_base)])?; + stmt.execute(params!["entry_point", format!("0x{:08X}", info.entry_point)])?; + if let Some(name) = info.original_pe_name { + stmt.execute(params!["original_pe_name", name])?; + } + if let Some(title_id) = info.title_id { + stmt.execute(params!["title_id", format!("0x{:08X}", title_id)])?; + } + if let Some(media_id) = info.media_id { + stmt.execute(params!["media_id", format!("0x{:08X}", media_id)])?; + } + Ok(()) +} + +fn insert_sections(conn: &Connection, sections: &[xenia_xex::pe::PeSection]) -> anyhow::Result<()> { + let mut stmt = conn.prepare( + "INSERT INTO sections (name, virtual_address, virtual_size, raw_offset, raw_size, flags, is_code) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)" + )?; + for s in sections { + stmt.execute(params![ + s.name, + s.virtual_address as i64, + s.virtual_size as i64, + s.raw_offset as i64, + s.raw_size as i64, + s.flags as i64, + s.is_code() as i32, + ])?; + } + Ok(()) +} + +fn insert_imports(conn: &Connection, info: &DisasmInfo) -> anyhow::Result<()> { + let mut stmt = conn.prepare( + "INSERT INTO imports (library, ordinal, name, record_type, address) + VALUES (?1, ?2, ?3, ?4, ?5)" + )?; + for lib in info.import_libraries { + for imp in &lib.imports { + let resolved = crate::resolve_ordinal(&lib.name, imp.ordinal); + stmt.execute(params![ + lib.name, + imp.ordinal as i64, + resolved, + imp.record_type as i64, + imp.address as i64, + ])?; + } + } + Ok(()) +} + +fn insert_functions( + conn: &Connection, + func_analysis: &FuncAnalysis, + labels: &HashMap, +) -> anyhow::Result<()> { + let mut stmt = conn.prepare( + "INSERT INTO functions (address, name, end_address, frame_size, saved_gprs, is_leaf, is_saverestore) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)" + )?; + for (&addr, fi) in &func_analysis.functions { + let name = labels.get(&addr) + .cloned() + .unwrap_or_else(|| format!("sub_{addr:08X}")); + stmt.execute(params![ + addr as i64, + name, + fi.end as i64, + fi.frame_size as i64, + fi.saved_gprs as i64, + fi.is_leaf as i32, + fi.is_saverestore as i32, + ])?; + } + Ok(()) +} + +fn insert_labels( + conn: &Connection, + labels: &HashMap, +) -> anyhow::Result<()> { + let mut stmt = conn.prepare( + "INSERT OR IGNORE INTO labels (address, name, kind) VALUES (?1, ?2, ?3)" + )?; + for (&addr, name) in labels { + let kind = if name.starts_with("sub_") || name == "entry_point" { + "function" + } else if name.starts_with("__imp_") { + "import" + } else if name.starts_with("__savegprlr_") || name.starts_with("__restgprlr_") { + "saverestore" + } else if name.starts_with("loc_") { + "local" + } else if name.starts_with("dat_") { + "data" + } else { + "other" + }; + stmt.execute(params![addr as i64, name, kind])?; + } + Ok(()) +} + +fn insert_instructions_streaming( + conn: &Connection, + pe: &[u8], + info: &DisasmInfo, + func_analysis: &FuncAnalysis, + labels: &HashMap, +) -> anyhow::Result<()> { + let mut tx = conn.unchecked_transaction()?; + let mut count: u64 = 0; + let mut since_commit: u64 = 0; + + for section in info.sections { + if !section.is_code() { continue; } + + let va_start = section.virtual_address; + let va_end = va_start + section.virtual_size; + let file_start = section.virtual_address as usize; + + let mut current_func: Option = None; + let mut addr = va_start; + + while addr < va_end { + let abs_addr = info.image_base + addr; + let off = (addr - va_start) as usize + file_start; + if off + 4 > pe.len() { break; } + + if func_analysis.is_function_start(abs_addr) { + current_func = Some(abs_addr); + } + + let instr = u32::from_be_bytes([pe[off], pe[off+1], pe[off+2], pe[off+3]]); + let decoded = crate::ppc::disasm(instr, abs_addr); + let (mnemonic, operands) = split_disasm(&decoded.base); + + let (ext_mnemonic, ext_operands, ext_disasm): (Option<&str>, Option<&str>, Option<&str>) = + match &decoded.ext { + Some(ext) => { + let (em, eo) = split_disasm(ext); + (Some(em), Some(eo), Some(ext.as_str())) + } + None => (None, None, None), + }; + let label = labels.get(&abs_addr).map(|s| s.as_str()); + + { + let mut stmt = tx.prepare_cached( + "INSERT INTO instructions (address, raw, mnemonic, operands, disasm, ext_mnemonic, ext_operands, ext_disasm, section, function, label) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)" + )?; + stmt.execute(params![ + abs_addr as i64, + instr as i64, + mnemonic, + operands, + decoded.base, + ext_mnemonic, + ext_operands, + ext_disasm, + section.name, + current_func.map(|a| a as i64), + label, + ])?; + } + + count += 1; + since_commit += 1; + addr += 4; + + if since_commit >= batch_size() { + tx.commit()?; + eprintln!("[db] instructions: {count} committed"); + tx = conn.unchecked_transaction()?; + since_commit = 0; + } + } + } + + tx.commit()?; + eprintln!("[db] inserted {count} instructions"); + Ok(()) +} + +fn insert_xrefs_streaming( + conn: &Connection, + xrefs: &XrefMap, + pe: &[u8], + image_base: u32, + func_analysis: &FuncAnalysis, + labels: &HashMap, +) -> anyhow::Result<()> { + let mut tx = conn.unchecked_transaction()?; + let mut count: u64 = 0; + let mut since_commit: u64 = 0; + + for (&target, refs) in xrefs { + let target_label = labels.get(&target).map(|s| s.as_str()); + + for xref in refs { + let kind = xref.kind.db_tag(); + + let instruction: Option = { + let off = xref.source.wrapping_sub(image_base) as usize; + if off + 4 <= pe.len() { + let raw = u32::from_be_bytes([pe[off], pe[off+1], pe[off+2], pe[off+3]]); + let decoded = crate::ppc::disasm(raw, xref.source); + let display = decoded.display().to_string(); + let (mnem, _) = split_disasm(&display); + Some(mnem.to_string()) + } else { + None + } + }; + + let source_func = func_analysis.functions + .range(..=xref.source) + .next_back() + .map(|(&a, _)| a as i64); + + let source_label = resolve_source_label( + xref.source, func_analysis, labels, + ); + + { + let mut stmt = tx.prepare_cached( + "INSERT INTO xrefs (source, target, kind, instruction, source_func, source_label, target_label) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)" + )?; + stmt.execute(params![ + xref.source as i64, + target as i64, + kind, + instruction, + source_func, + source_label, + target_label, + ])?; + } + + count += 1; + since_commit += 1; + + if since_commit >= batch_size() { + tx.commit()?; + eprintln!("[db] xrefs: {count} committed"); + tx = conn.unchecked_transaction()?; + since_commit = 0; + } + } + } + + tx.commit()?; + eprintln!("[db] inserted {count} xrefs"); + Ok(()) +} + +/// Split "mnemonic operands" into (mnemonic, operands). +fn split_disasm(disasm: &str) -> (&str, &str) { + let trimmed = disasm.trim(); + if let Some(pos) = trimmed.find(|c: char| c.is_whitespace()) { + let mnemonic = &trimmed[..pos]; + let operands = trimmed[pos..].trim_start(); + (mnemonic, operands) + } else { + (trimmed, "") + } +} diff --git a/crates/xenia-analysis/src/formatter.rs b/crates/xenia-analysis/src/formatter.rs new file mode 100644 index 0000000..fac4e07 --- /dev/null +++ b/crates/xenia-analysis/src/formatter.rs @@ -0,0 +1,318 @@ +//! Assembly text output formatter for Xbox 360 disassembly. + +use std::collections::HashMap; +use std::io::Write; + +use xenia_xex::header::ImportLibrary; +use xenia_xex::pe::PeSection; + +use crate::func::FuncAnalysis; +use crate::xref::{XrefKind, Xref, XrefMap, section_for_addr, resolve_source_label}; + +/// Metadata passed to the formatter (avoids exposing full Xex2Header internals). +pub struct DisasmInfo<'a> { + pub image_base: u32, + pub entry_point: u32, + pub original_pe_name: Option<&'a str>, + pub title_id: Option, + pub media_id: Option, + pub sections: &'a [PeSection], + pub import_libraries: &'a [ImportLibrary], +} + +/// Write full disassembly to the output stream. +pub fn write_asm( + out: &mut dyn Write, + pe: &[u8], + info: &DisasmInfo, + func_analysis: &FuncAnalysis, + labels: &HashMap, + import_map: &HashMap, + xrefs: &XrefMap, + data_annotations: &HashMap, +) -> anyhow::Result<()> { + // Header + writeln!(out, "; ============================================================================")?; + writeln!(out, "; Xbox 360 Disassembly — generated by xenia-rs")?; + if let Some(name) = info.original_pe_name { + writeln!(out, "; Original PE: {name}")?; + } + if let (Some(title_id), Some(media_id)) = (info.title_id, info.media_id) { + writeln!(out, "; Title ID: 0x{title_id:08X} Media ID: 0x{media_id:08X}")?; + } + writeln!(out, "; Image base: 0x{:08X} Entry point: 0x{:08X}", info.image_base, info.entry_point)?; + writeln!(out, "; Functions detected: {}", func_analysis.functions.len())?; + writeln!(out, "; ============================================================================")?; + writeln!(out)?; + + // Import declarations + if !info.import_libraries.is_empty() { + writeln!(out, "; ── Imports ─────────────────────────────────────────────────────────────────")?; + for lib in info.import_libraries { + writeln!(out, "; Library: {}", lib.name)?; + for imp in &lib.imports { + let resolved = crate::resolve_ordinal(&lib.name, imp.ordinal); + let name = resolved.unwrap_or("???"); + let kind = if imp.record_type == 1 { "thunk" } else { "var" }; + writeln!(out, "; [{kind}] 0x{:08X} ordinal 0x{:04X} = {}", imp.address, imp.ordinal, name)?; + } + } + writeln!(out)?; + } + + // Disassemble each section + for section in info.sections { + writeln!(out, "; ── Section: {:8} VA=0x{:08X} Size=0x{:08X} Flags=0x{:08X} ──", + section.name, section.virtual_address, section.virtual_size, section.flags)?; + + let va_start = section.virtual_address; + let va_end = va_start + section.virtual_size; + let file_start = section.virtual_address as usize; + + // Pre-sort data labels in this section for break-at-label hex dump + let section_labels_sorted: Vec = if !section.is_code() { + let sec_start = info.image_base + va_start; + let sec_end = info.image_base + va_end; + let mut addrs: Vec = labels.keys() + .filter(|&&a| a >= sec_start && a < sec_end) + .copied() + .collect(); + addrs.sort(); + addrs + } else { + Vec::new() + }; + + if section.is_code() { + writeln!(out, ".text")?; + writeln!(out)?; + + let mut in_function = false; + let mut addr = va_start; + while addr < va_end { + let abs_addr = info.image_base + addr; + let off = (addr - va_start) as usize + file_start; + if off + 4 > pe.len() { break; } + + // Function start? Emit separator + header + if let Some(fi) = func_analysis.get(abs_addr) { + if in_function { + writeln!(out, "; end function")?; + } + writeln!(out)?; + writeln!(out, "; ──────────────────────────────────────────────────────────────────────────")?; + + let lbl = labels.get(&abs_addr).cloned() + .unwrap_or_else(|| format!("sub_{abs_addr:08X}")); + + if fi.is_saverestore { + writeln!(out, "; FUNCTION: {lbl} (save/restore GPR helper)")?; + } else if fi.is_leaf { + writeln!(out, "; FUNCTION: {lbl} (leaf)")?; + } else { + let mut details = Vec::new(); + if fi.frame_size > 0 { + details.push(format!("frame={}", fi.frame_size)); + } + if fi.saved_gprs > 0 { + let first_reg = 32 - fi.saved_gprs; + details.push(format!("saves r{first_reg}-r31")); + } + let detail_str = if details.is_empty() { + String::new() + } else { + format!(" ({})", details.join(", ")) + }; + writeln!(out, "; FUNCTION: {lbl}{detail_str}")?; + } + + // Xrefs for function entry + if let Some(xref_lines) = format_xrefs(abs_addr, xrefs, func_analysis, labels) { + for line in &xref_lines { + writeln!(out, "{line}")?; + } + } + + writeln!(out, "; ──────────────────────────────────────────────────────────────────────────")?; + in_function = true; + } + + // Label + if let Some(lbl) = labels.get(&abs_addr) { + if !func_analysis.is_function_start(abs_addr) { + writeln!(out)?; + // Xrefs for local labels + if let Some(xref_lines) = format_xrefs(abs_addr, xrefs, func_analysis, labels) { + for line in &xref_lines { + writeln!(out, "{line}")?; + } + } + writeln!(out, "{lbl}:")?; + } else { + writeln!(out)?; + writeln!(out, "{lbl}:")?; + } + } + + // Import thunk annotation + if let Some(imp_name) = import_map.get(&abs_addr) { + writeln!(out, " ; IMPORT: {imp_name}")?; + } + + let instr = u32::from_be_bytes([ + pe[off], pe[off+1], pe[off+2], pe[off+3] + ]); + + let decoded = crate::ppc::disasm(instr, abs_addr); + let disasm_text = decoded.display().to_string(); + + // Annotate branch targets with label names + let mut annotated = annotate_branch(&disasm_text, labels); + + // Annotate data references + if let Some(&(data_addr, kind)) = data_annotations.get(&abs_addr) { + let tag = match kind { + XrefKind::DataRead => "[R]", + XrefKind::DataWrite => "[W]", + _ => "[&]", + }; + let sec = section_for_addr(data_addr, info.sections, info.image_base) + .unwrap_or("?"); + let data_lbl = labels.get(&data_addr) + .map(|s| format!(" = {s}")) + .unwrap_or_default(); + if !annotated.contains("; ->") { + annotated = format!("{annotated:<40} ; {tag} 0x{data_addr:08X} ({sec}){data_lbl}"); + } else { + annotated = format!("{annotated} {tag} 0x{data_addr:08X} ({sec}){data_lbl}"); + } + } + + writeln!(out, " {:08X}: {:08X} {}", abs_addr, instr, annotated)?; + addr += 4; + } + if in_function { + writeln!(out, "; end function")?; + } + } else { + // Data section: hex dump + writeln!(out, ".data")?; + writeln!(out)?; + + let mut addr = va_start; + while addr < va_end { + let abs_addr = info.image_base + addr; + let off = (addr - va_start) as usize + file_start; + + if let Some(lbl) = labels.get(&abs_addr) { + writeln!(out)?; + // Xrefs for data labels + if let Some(xref_lines) = format_xrefs(abs_addr, xrefs, func_analysis, labels) { + for line in &xref_lines { + writeln!(out, "{line}")?; + } + } + writeln!(out, "{lbl}:")?; + } + + // Emit up to 16 bytes per line, but break at label boundaries + let mut line_end = std::cmp::min(addr + 16, va_end); + for &lbl_addr in §ion_labels_sorted { + let lbl_va = lbl_addr - info.image_base; + if lbl_va > addr && lbl_va < line_end { + line_end = lbl_va; + break; + } + if lbl_va >= line_end { break; } + } + let byte_count = (line_end - addr) as usize; + if off + byte_count > pe.len() { break; } + + write!(out, " {:08X}: ", abs_addr)?; + for i in 0..byte_count { + write!(out, "{:02X}", pe[off + i])?; + if i % 4 == 3 { write!(out, " ")?; } + } + // ASCII representation + let pad = (16 - byte_count) * 2 + (16 - byte_count) / 4; + write!(out, "{:>width$} |", "", width = pad)?; + for i in 0..byte_count { + let b = pe[off + i]; + let ch = if b.is_ascii_graphic() || b == b' ' { b as char } else { '.' }; + write!(out, "{ch}")?; + } + writeln!(out, "|")?; + + addr = line_end; + } + } + writeln!(out)?; + } + + Ok(()) +} + +const XREF_DISPLAY_LIMIT: usize = 8; + +fn format_xrefs( + target: u32, + xrefs: &XrefMap, + func_analysis: &FuncAnalysis, + labels: &HashMap, +) -> Option> { + let refs = xrefs.get(&target)?; + if refs.is_empty() { return None; } + + let mut sorted: Vec = refs.clone(); + sorted.sort(); + sorted.dedup(); + + let total = sorted.len(); + let mut lines = Vec::new(); + + let calls = sorted.iter().filter(|x| x.kind == XrefKind::Call).count(); + let jumps = sorted.iter().filter(|x| x.kind == XrefKind::Jump).count(); + let branches = sorted.iter().filter(|x| x.kind == XrefKind::Branch).count(); + let reads = sorted.iter().filter(|x| x.kind == XrefKind::DataRead).count(); + let writes = sorted.iter().filter(|x| x.kind == XrefKind::DataWrite).count(); + let data_refs = sorted.iter().filter(|x| x.kind == XrefKind::DataRef).count(); + + let mut summary_parts = Vec::new(); + if calls > 0 { summary_parts.push(format!("{calls} call{}", if calls != 1 { "s" } else { "" })); } + if jumps > 0 { summary_parts.push(format!("{jumps} jump{}", if jumps != 1 { "s" } else { "" })); } + if branches > 0 { summary_parts.push(format!("{branches} branch{}", if branches != 1 { "es" } else { "" })); } + if reads > 0 { summary_parts.push(format!("{reads} read{}", if reads != 1 { "s" } else { "" })); } + if writes > 0 { summary_parts.push(format!("{writes} write{}", if writes != 1 { "s" } else { "" })); } + if data_refs > 0 { summary_parts.push(format!("{data_refs} ref{}", if data_refs != 1 { "s" } else { "" })); } + + lines.push(format!("; XREF: {} ({})", summary_parts.join(", "), total)); + + for (i, xref) in sorted.iter().enumerate() { + if i >= XREF_DISPLAY_LIMIT { + lines.push(format!("; ... and {} more", total - XREF_DISPLAY_LIMIT)); + break; + } + let source_label = resolve_source_label(xref.source, func_analysis, labels); + lines.push(format!("; {} from {}", xref.kind.tag(), source_label)); + } + + Some(lines) +} + +fn annotate_branch(disasm: &str, labels: &HashMap) -> String { + if let Some(pos) = disasm.find("0x") { + let hex_start = pos + 2; + let hex_end = disasm[hex_start..].find(|c: char| !c.is_ascii_hexdigit()) + .map(|i| hex_start + i) + .unwrap_or(disasm.len()); + let hex_str = &disasm[hex_start..hex_end]; + if hex_str.len() == 8 { + if let Ok(addr) = u32::from_str_radix(hex_str, 16) { + if let Some(lbl) = labels.get(&addr) { + return format!("{disasm:<40} ; -> {lbl}"); + } + } + } + } + disasm.to_string() +} diff --git a/crates/xenia-analysis/src/func.rs b/crates/xenia-analysis/src/func.rs new file mode 100644 index 0000000..b004d8e --- /dev/null +++ b/crates/xenia-analysis/src/func.rs @@ -0,0 +1,444 @@ +//! Function boundary detection via PPC prologue/epilogue pattern matching. +//! +//! Strategy (multi-pass): +//! 1. Identify all `bl` (branch-and-link) targets — these are call sites, +//! hence very likely function entry points. +//! 2. Scan the save/restore GPR helper region and label it. +//! 3. For each candidate entry, look for prologue patterns: +//! a) `mfspr rN, LR` (typically r0 or r12) +//! b) `bl __savegprlr_NN` (call into save stub) +//! c) `stwu r1, -N(r1)` (allocate stack frame) +//! If a prologue is confirmed, record the function and its stack frame size. +//! 4. Walk forward from each function entry to find the epilogue: +//! a) `blr` (return) +//! b) `b __restgprlr_NN` (tail-branch into restore stub which returns) +//! Mark the function's end address. +//! 5. Detect leaf functions: `bl` targets that lack a prologue but eventually `blr`. + +use std::collections::{HashMap, HashSet, BTreeMap}; + +/// Information about a detected function. +#[derive(Debug, Clone)] +pub struct FuncInfo { + /// Absolute start address. + pub start: u32, + /// Absolute end address (exclusive — one past last instruction). + pub end: u32, + /// Stack frame size (0 if unknown / leaf). + pub frame_size: u32, + /// Number of saved GPRs (via __savegprlr helper), 0 if unknown. + pub saved_gprs: u32, + /// True if this is a leaf function (no bl, no frame setup). + pub is_leaf: bool, + /// True if this is a save/restore GPR helper stub. + pub is_saverestore: bool, +} + +/// Result of the function analysis pass. +pub struct FuncAnalysis { + /// address → FuncInfo for every detected function, sorted by address. + pub functions: BTreeMap, + /// Addresses in the save-GPR region (start of __savegprlr block). + pub save_gpr_base: Option, + /// Addresses in the restore-GPR region (start of __restgprlr block). + pub restore_gpr_base: Option, +} + +// ── Instruction field helpers ────────────────────────────────────────────── + +fn op(instr: u32) -> u32 { (instr >> 26) & 0x3F } +fn bits(instr: u32, hi: u32, lo: u32) -> u32 { + (instr >> (31 - hi)) & ((1 << (hi - lo + 1)) - 1) +} + +fn is_mfspr_lr(instr: u32) -> Option { + // mfspr rD, LR → opcode 31, xo=339, spr=8 + if op(instr) != 31 { return None; } + let xo = bits(instr, 30, 21); + if xo != 339 { return None; } + let spr = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11); + if spr != 8 { return None; } + Some(bits(instr, 10, 6)) // return rD +} + +#[allow(dead_code)] +fn is_mtspr_lr(instr: u32) -> bool { + // mtspr LR, rS → opcode 31, xo=467, spr=8 + if op(instr) != 31 { return false; } + let xo = bits(instr, 30, 21); + if xo != 467 { return false; } + let spr = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11); + spr == 8 +} + +fn is_stwu_r1(instr: u32) -> Option { + // stwu r1, d(r1) → opcode 37, rS=1, rA=1 + if op(instr) != 37 { return None; } + let rs = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + if rs != 1 || ra != 1 { return None; } + let d = ((instr & 0xFFFF) as i16) as i32; + Some(d) // negative = frame allocation +} + +fn is_blr(instr: u32) -> bool { + instr == 0x4E800020 +} + +fn is_bctr(instr: u32) -> bool { + instr == 0x4E800420 +} + +fn is_bl(instr: u32) -> Option { + // bl target → opcode 18, LK=1, AA=0 + if op(instr) != 18 { return None; } + if instr & 1 == 0 { return None; } // must have LK bit + if instr & 2 != 0 { return None; } // not absolute + // Return the signed offset + let li = instr & 0x03FFFFFC; + Some(li) +} + +fn is_b(instr: u32) -> Option { + // b target → opcode 18, LK=0, AA=0 + if op(instr) != 18 { return None; } + if instr & 1 != 0 { return None; } // no LK bit + if instr & 2 != 0 { return None; } // not absolute + Some(instr & 0x03FFFFFC) +} + +fn sign_ext26(val: u32) -> i32 { + ((val << 6) as i32) >> 6 +} + +fn bl_target(instr: u32, addr: u32) -> Option { + is_bl(instr).map(|off| addr.wrapping_add(sign_ext26(off) as u32)) +} + +fn b_target(instr: u32, addr: u32) -> Option { + is_b(instr).map(|off| addr.wrapping_add(sign_ext26(off) as u32)) +} + +// ── Read instruction from PE ─────────────────────────────────────────────── + +fn read_instr(pe: &[u8], abs_addr: u32, image_base: u32) -> Option { + let off = abs_addr.wrapping_sub(image_base) as usize; + if off + 4 > pe.len() { return None; } + Some(u32::from_be_bytes([pe[off], pe[off+1], pe[off+2], pe[off+3]])) +} + +// ── Detect the save/restore GPR helper stubs ─────────────────────────────── +// +// These are a well-known pattern emitted by the Xbox 360 linker. +// Save block: a cascade of `std rN, offset(r1)` for r14..r31 + `stw r12, -8(r1)` + `blr` +// Restore: a cascade of `ld rN, offset(r1)` for r14..r31 + `lwz r12, -8(r1)` + `mtspr LR, r12` + `blr` +// +// We detect the save block by finding 18 consecutive `std rN, ...(r1)` instructions +// for r14 through r31. + +fn find_saverestore_stubs( + pe: &[u8], + image_base: u32, + code_ranges: &[(u32, u32)], // (abs_start, abs_end) +) -> (Option, Option) { + let mut save_base = None; + let mut restore_base = None; + + for &(start, end) in code_ranges { + let mut addr = start; + while addr + 4 * 18 < end { + // Check if this is `std r14, ...(r1)` — opcode 62 (std), rS=14, rA=1 + let instr = match read_instr(pe, addr, image_base) { Some(i) => i, None => { addr += 4; continue; } }; + if op(instr) == 62 && bits(instr, 10, 6) == 14 && bits(instr, 15, 11) == 1 && (instr & 3) == 0 { + // Verify it's a cascade: r14, r15, ..., r31 + let mut ok = true; + for i in 0u32..18 { + let check = match read_instr(pe, addr + i * 4, image_base) { Some(c) => c, None => { ok = false; break; } }; + if op(check) != 62 || bits(check, 10, 6) != 14 + i || bits(check, 15, 11) != 1 { + ok = false; + break; + } + } + if ok { + save_base = Some(addr); + // Restore block typically follows the save block + // After save: stw r12, -8(r1) + blr, then restore starts + let after_save = addr + 18 * 4 + 8; // skip stw r12 + blr + let check = read_instr(pe, after_save, image_base); + if let Some(c) = check { + // Should be `ld r14, ...(r1)` — opcode 58, rT=14, rA=1 + if op(c) == 58 && bits(c, 10, 6) == 14 && bits(c, 15, 11) == 1 { + restore_base = Some(after_save); + } + } + break; + } + } + addr += 4; + } + if save_base.is_some() { break; } + } + + (save_base, restore_base) +} + +// ── Main analysis ────────────────────────────────────────────────────────── + +pub fn analyze( + pe: &[u8], + image_base: u32, + entry_point: u32, + code_sections: &[(u32, u32, u32)], // (va_start, va_size, flags) +) -> FuncAnalysis { + let code_ranges: Vec<(u32, u32)> = code_sections.iter() + .map(|(va, sz, _)| (image_base + va, image_base + va + sz)) + .collect(); + + // 1. Find save/restore stubs + let (save_base, restore_base) = find_saverestore_stubs(pe, image_base, &code_ranges); + if let Some(sb) = save_base { + eprintln!("[func] __savegprlr stub at 0x{sb:08X}"); + } + if let Some(rb) = restore_base { + eprintln!("[func] __restgprlr stub at 0x{rb:08X}"); + } + + // Set of addresses in the save/restore region (to exclude from function detection) + let mut saverestore_addrs: HashSet = HashSet::new(); + if let Some(sb) = save_base { + // Save block: 18 std + stw + blr = 20 instructions + for i in 0..20 { saverestore_addrs.insert(sb + i * 4); } + } + if let Some(rb) = restore_base { + // Restore block: 18 ld + lwz + mtspr + blr = 21 instructions + for i in 0..21 { saverestore_addrs.insert(rb + i * 4); } + } + + // 2. Collect all bl targets as candidate function entries + let mut call_targets: HashSet = HashSet::new(); + call_targets.insert(entry_point); + + for &(start, end) in &code_ranges { + let mut addr = start; + while addr < end { + if let Some(instr) = read_instr(pe, addr, image_base) { + if let Some(target) = bl_target(instr, addr) { + // Don't count calls into save/restore stubs as function entries + if !saverestore_addrs.contains(&target) { + call_targets.insert(target); + } + } + } + addr += 4; + } + } + eprintln!("[func] {} bl targets (candidate functions)", call_targets.len()); + + // 3. For each candidate, detect prologue and walk to epilogue + let mut functions: BTreeMap = BTreeMap::new(); + + for &func_addr in &call_targets { + if let Some(fi) = analyze_function(pe, image_base, func_addr, &code_ranges, save_base, restore_base) { + functions.insert(func_addr, fi); + } + } + + // 4. Label save/restore stubs as special functions — one entry for the whole block + if let Some(sb) = save_base { + // The save block is one cascade: entry at each rN, falls through to blr + // Treat as a single function with the first entry point + functions.insert(sb, FuncInfo { + start: sb, + end: sb + 20 * 4, // 18 std + stw r12 + blr + frame_size: 0, + saved_gprs: 18, + is_leaf: true, + is_saverestore: true, + }); + } + if let Some(rb) = restore_base { + functions.insert(rb, FuncInfo { + start: rb, + end: rb + 21 * 4, // 18 ld + lwz r12 + mtspr LR + blr + frame_size: 0, + saved_gprs: 18, + is_leaf: true, + is_saverestore: true, + }); + } + + eprintln!("[func] {} functions detected", functions.len()); + + FuncAnalysis { + functions, + save_gpr_base: save_base, + restore_gpr_base: restore_base, + } +} + +/// Analyze a single function starting at `func_addr`. +fn analyze_function( + pe: &[u8], + image_base: u32, + func_addr: u32, + code_ranges: &[(u32, u32)], + save_base: Option, + restore_base: Option, +) -> Option { + // Verify the address is within a code section + let in_code = code_ranges.iter().any(|&(s, e)| func_addr >= s && func_addr < e); + if !in_code { return None; } + + let instr0 = read_instr(pe, func_addr, image_base)?; + + let mut frame_size: u32 = 0; + let mut saved_gprs: u32 = 0; + let mut is_leaf = false; + let mut prologue_len: u32 = 0; + + // Pattern A: mfspr rN, LR [+ bl __savegprlr_NN] + stwu r1, -N(r1) + if let Some(_lr_reg) = is_mfspr_lr(instr0) { + prologue_len = 4; + let instr1 = read_instr(pe, func_addr + 4, image_base).unwrap_or(0); + + // Check if next is bl to save stub + if let Some(target) = bl_target(instr1, func_addr + 4) { + if let Some(sb) = save_base { + if target >= sb && target < sb + 18 * 4 { + let idx = (target - sb) / 4; + saved_gprs = 18 - idx; + prologue_len = 8; + } + } + } + + // Next should be stwu r1, -N(r1) + let stwu_instr = read_instr(pe, func_addr + prologue_len, image_base).unwrap_or(0); + if let Some(d) = is_stwu_r1(stwu_instr) { + frame_size = (-d) as u32; + prologue_len += 4; + } + } + // Pattern B: stwu r1, -N(r1) without mfspr (rare but possible for leaf-ish functions) + else if let Some(d) = is_stwu_r1(instr0) { + frame_size = (-d) as u32; + prologue_len = 4; + is_leaf = true; // no LR save = likely leaf (or uses CTR) + } + // Pattern C: no prologue — leaf function, just code until blr + else { + is_leaf = true; + } + + // Walk forward to find the end of the function + let max_range = code_ranges.iter() + .find(|&&(s, e)| func_addr >= s && func_addr < e) + .map(|&(_, e)| e) + .unwrap_or(func_addr + 0x100000); + + let mut end_addr = func_addr + 4; + let mut addr = func_addr + prologue_len; + let scan_limit = std::cmp::min(addr + 0x100000, max_range); // 1MB max function + + while addr < scan_limit { + let instr = match read_instr(pe, addr, image_base) { + Some(i) => i, + None => break, + }; + + // Epilogue: blr + if is_blr(instr) { + end_addr = addr + 4; + // Check if the instruction after blr looks like padding or another function + // Sometimes there's trailing data after blr; we stop at the first blr + // that isn't inside a branch-over pattern + break; + } + + // Epilogue: b __restgprlr_NN (tail branch into restore stub) + if let Some(target) = b_target(instr, addr) { + if let Some(rb) = restore_base { + if target >= rb && target < rb + 18 * 4 { + end_addr = addr + 4; + break; + } + } + } + + // Epilogue: bctr (indirect tail call — end of function) + if is_bctr(instr) { + end_addr = addr + 4; + break; + } + + addr += 4; + } + + // If we didn't find any epilogue within a reasonable range, still emit + // the function but mark end at the scan point + if end_addr <= func_addr + 4 && prologue_len > 0 { + end_addr = addr; + } + + // Don't emit zero-size "functions" for addresses that are just data + if end_addr <= func_addr + 4 && prologue_len == 0 { + return None; + } + + Some(FuncInfo { + start: func_addr, + end: end_addr, + frame_size, + saved_gprs, + is_leaf, + is_saverestore: false, + }) +} + +// ── Label generation ─────────────────────────────────────────────────────── + +impl FuncAnalysis { + /// Generate labels for all detected functions. + /// Call targets with confirmed prologues get `sub_XXXXXXXX`. + /// Save/restore entries get `__savegprlr_NN` / `__restgprlr_NN`. + pub fn generate_labels(&self) -> HashMap { + let mut labels = HashMap::new(); + + for (&addr, fi) in &self.functions { + if fi.is_saverestore { + // Label the block start, plus individual register entry points + if let Some(sb) = self.save_gpr_base { + if addr == sb { + for i in 0u32..18 { + let reg = 14 + i; + labels.insert(sb + i * 4, format!("__savegprlr_{reg}")); + } + continue; + } + } + if let Some(rb) = self.restore_gpr_base { + if addr == rb { + for i in 0u32..18 { + let reg = 14 + i; + labels.insert(rb + i * 4, format!("__restgprlr_{reg}")); + } + continue; + } + } + } + labels.insert(addr, format!("sub_{addr:08X}")); + } + + labels + } + + /// Returns true if `addr` is the start of a detected function. + pub fn is_function_start(&self, addr: u32) -> bool { + self.functions.contains_key(&addr) + } + + /// Get info for the function starting at `addr`. + pub fn get(&self, addr: u32) -> Option<&FuncInfo> { + self.functions.get(&addr) + } +} diff --git a/crates/xenia-analysis/src/lib.rs b/crates/xenia-analysis/src/lib.rs new file mode 100644 index 0000000..6828ff3 --- /dev/null +++ b/crates/xenia-analysis/src/lib.rs @@ -0,0 +1,10 @@ +pub mod ppc; +pub mod func; +pub mod xref; +pub mod db; +pub mod formatter; + +mod ordinals; +pub use ordinals::resolve_ordinal; +pub use xref::{XrefKind, Xref, XrefMap, resolve_source_label}; +pub use db::{DbWriter, ExecTraceEntry, ImportCallEntry, BranchTraceEntry}; diff --git a/crates/xenia-analysis/src/ordinals.rs b/crates/xenia-analysis/src/ordinals.rs new file mode 100644 index 0000000..aa6f6a1 --- /dev/null +++ b/crates/xenia-analysis/src/ordinals.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/ordinals.rs")); diff --git a/crates/xenia-analysis/src/ppc.rs b/crates/xenia-analysis/src/ppc.rs new file mode 100644 index 0000000..d8f9761 --- /dev/null +++ b/crates/xenia-analysis/src/ppc.rs @@ -0,0 +1,1376 @@ +//! PowerPC (big-endian, 32-bit) disassembler for Xbox 360 Xenon. + +/// Decoded instruction carrying both base and (optional) extended mnemonic forms. +pub struct Decoded { + pub base: String, + pub ext: Option, +} + +impl Decoded { + fn base_only(s: String) -> Self { Self { base: s, ext: None } } + fn with_ext(base: String, ext: String) -> Self { Self { base, ext: Some(ext) } } + /// Returns the preferred display form (extended if available, else base). + pub fn display(&self) -> &str { self.ext.as_deref().unwrap_or(&self.base) } +} + +/// Disassemble one 32-bit big-endian PowerPC instruction. +pub fn disasm(instr: u32, addr: u32) -> Decoded { + let op = (instr >> 26) & 0x3F; + match op { + 2 => decode_tdi(instr), + 3 => decode_twi(instr), + 4 => decode_op4(instr), + 5 => decode_op5(instr), + 6 => decode_op6(instr), + 7 => decode_d_mul("mulli", instr), + 8 => decode_d_sub("subfic", instr), + 10 => decode_cmp_imm("cmpli", instr, false), + 11 => decode_cmp_imm("cmpi", instr, true), + 12 => decode_d_add("addic", instr), + 13 => decode_d_add("addic.", instr), + 14 => decode_addi(instr), + 15 => decode_addis(instr), + 16 => decode_bc(instr, addr), + 17 => Decoded::base_only("sc".to_string()), + 18 => decode_b(instr, addr), + 19 => decode_op19(instr), + 20 => decode_rlwimi(instr), + 21 => decode_rlwinm(instr), + 23 => decode_rlwnm(instr), + 24 => decode_ori(instr), + 25 => decode_oris(instr), + 26 => decode_d_logic("xori", instr), + 27 => decode_d_logic("xoris", instr), + 28 => decode_d_logic("andi.", instr), + 29 => decode_d_logic("andis.", instr), + 30 => decode_op30(instr), + 31 => decode_op31(instr), + 32 => decode_ls("lwz", instr), + 33 => decode_ls("lwzu", instr), + 34 => decode_ls("lbz", instr), + 35 => decode_ls("lbzu", instr), + 36 => decode_ls("stw", instr), + 37 => decode_ls("stwu", instr), + 38 => decode_ls("stb", instr), + 39 => decode_ls("stbu", instr), + 40 => decode_ls("lhz", instr), + 41 => decode_ls("lhzu", instr), + 42 => decode_ls("lha", instr), + 43 => decode_ls("lhau", instr), + 44 => decode_ls("sth", instr), + 45 => decode_ls("sthu", instr), + 46 => decode_ls("lmw", instr), + 47 => decode_ls("stmw", instr), + 48 => decode_ls("lfs", instr), + 49 => decode_ls("lfsu", instr), + 50 => decode_ls("lfd", instr), + 51 => decode_ls("lfdu", instr), + 52 => decode_ls("stfs", instr), + 53 => decode_ls("stfsu", instr), + 54 => decode_ls("stfd", instr), + 55 => decode_ls("stfdu", instr), + 58 => decode_ds_form(instr), + 59 => decode_op59(instr), + 62 => decode_ds_store(instr), + 63 => decode_op63(instr), + _ => Decoded::base_only(format!(".long 0x{instr:08X}")), + } +} + +// ── Register names ────────────────────────────────────────────────────────── + +fn gpr(r: u32) -> String { format!("r{r}") } +fn fpr(r: u32) -> String { format!("f{r}") } +fn crb(b: u32) -> String { + let cr = b / 4; + let bit = b % 4; + let bit_name = ["lt", "gt", "eq", "so"][bit as usize]; + if cr == 0 { bit_name.to_string() } else { format!("4*cr{cr}+{bit_name}") } +} + +fn spr_name(spr: u32) -> String { + match spr { + 1 => "XER".into(), + 8 => "LR".into(), + 9 => "CTR".into(), + _ => format!("spr{spr}"), + } +} + +fn vr(r: u32) -> String { format!("v{r}") } + +// ── Field extraction ──────────────────────────────────────────────────────── + +fn bits(instr: u32, hi: u32, lo: u32) -> u32 { + (instr >> (31 - hi)) & ((1 << (hi - lo + 1)) - 1) +} + +fn sign_ext(val: u32, bits: u32) -> i32 { + let shift = 32 - bits; + ((val << shift) as i32) >> shift +} + +// ── VMX128 extended register extraction ───────────────────────────────────── +// Xbox 360 VMX128 uses 7-bit vector registers (v0-v127) split across +// non-contiguous bit positions in the instruction word. + +fn vd128(instr: u32) -> u32 { + bits(instr, 10, 6) | (bits(instr, 29, 28) << 5) +} +fn va128(instr: u32) -> u32 { + bits(instr, 15, 11) | (bits(instr, 26, 26) << 5) | (bits(instr, 21, 21) << 6) +} +fn vb128(instr: u32) -> u32 { + bits(instr, 20, 16) | (bits(instr, 31, 30) << 5) +} + +// ── Shared helpers ────────────────────────────────────────────────────────── + +/// Map trap TO field to condition suffix: 16→"lt", 4→"eq", etc. +fn trap_cond(to: u32) -> Option<&'static str> { + match to { + 16 => Some("lt"), + 4 => Some("eq"), + 8 => Some("gt"), + 12 => Some("ge"), + 20 => Some("le"), + 24 => Some("ne"), + 31 => Some(""), // unconditional + _ => None, + } +} + +/// Decode BO/BI into a condition suffix and CR prefix for simplified branches. +/// Returns Some((cond_suffix, cr_prefix)) e.g. ("eq", "") or ("lt", "cr2, "). +fn cond_branch_ext(bo: u32, bi: u32) -> Option<(&'static str, String)> { + let cond_true = bo & 0x08 != 0; + let no_cond = bo & 0x10 != 0; + let decr = bo & 0x04 == 0; + if no_cond || decr { return None; } + + let cr_field = bi / 4; + let cr_bit = bi % 4; + let cond_name = match (cr_bit, cond_true) { + (0, true) => "lt", (0, false) => "ge", + (1, true) => "gt", (1, false) => "le", + (2, true) => "eq", (2, false) => "ne", + (3, true) => "so", (3, false) => "ns", + _ => return None, + }; + let cr = if cr_field == 0 { String::new() } else { format!("cr{cr_field}, ") }; + Some((cond_name, cr)) +} + +// ── D-form: addi / li / subi ──────────────────────────────────────────────── + +fn decode_addi(instr: u32) -> Decoded { + let rt = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let imm = sign_ext(instr & 0xFFFF, 16); + let base = format!("addi {}, {}, {}", gpr(rt), gpr(ra), imm); + if ra == 0 { + Decoded::with_ext(base, format!("li {}, {}", gpr(rt), imm)) + } else if imm < 0 { + Decoded::with_ext(base, format!("subi {}, {}, {}", gpr(rt), gpr(ra), -imm)) + } else { + Decoded::base_only(base) + } +} + +fn decode_addis(instr: u32) -> Decoded { + let rt = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let imm = sign_ext(instr & 0xFFFF, 16); + let base = format!("addis {}, {}, 0x{:X}", gpr(rt), gpr(ra), imm as u16 as u32); + if ra == 0 { + Decoded::with_ext(base, format!("lis {}, 0x{:X}", gpr(rt), imm as u16 as u32)) + } else if imm < 0 { + Decoded::with_ext(base, format!("subis {}, {}, 0x{:X}", gpr(rt), gpr(ra), (-imm) as u16 as u32)) + } else { + Decoded::base_only(base) + } +} + +fn decode_d_add(mnem: &str, instr: u32) -> Decoded { + let rt = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let imm = sign_ext(instr & 0xFFFF, 16); + let base = format!("{mnem:<8}{}, {}, {}", gpr(rt), gpr(ra), imm); + if imm < 0 { + // addic → subic, addic. → subic. + let ext_mnem = mnem.replace("addic", "subic"); + Decoded::with_ext(base, format!("{ext_mnem:<8}{}, {}, {}", gpr(rt), gpr(ra), -imm)) + } else { + Decoded::base_only(base) + } +} + +fn decode_d_sub(mnem: &str, instr: u32) -> Decoded { + let rt = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let imm = sign_ext(instr & 0xFFFF, 16); + Decoded::base_only(format!("{mnem:<8}{}, {}, {}", gpr(rt), gpr(ra), imm)) +} + +fn decode_d_mul(mnem: &str, instr: u32) -> Decoded { + let rt = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let imm = sign_ext(instr & 0xFFFF, 16); + Decoded::base_only(format!("{mnem:<8}{}, {}, {}", gpr(rt), gpr(ra), imm)) +} + +fn decode_tdi(instr: u32) -> Decoded { + let to = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let imm = sign_ext(instr & 0xFFFF, 16); + let base = format!("tdi {}, {}, {}", to, gpr(ra), imm); + if let Some(cond) = trap_cond(to) { + if cond.is_empty() { + Decoded::base_only(base) + } else { + Decoded::with_ext(base, format!("td{}i{: Decoded { + let to = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let imm = sign_ext(instr & 0xFFFF, 16); + let base = format!("twi {}, {}, {}", to, gpr(ra), imm); + if let Some(cond) = trap_cond(to) { + if cond.is_empty() { + // TO=31 unconditional: no immediate form makes sense; keep base + Decoded::base_only(base) + } else { + Decoded::with_ext(base, format!("tw{}i{: Decoded { + let rs = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let uimm = instr & 0xFFFF; + let base = format!("ori {}, {}, 0x{:X}", gpr(ra), gpr(rs), uimm); + if rs == 0 && ra == 0 && uimm == 0 { + Decoded::with_ext(base, "nop".to_string()) + } else { + Decoded::base_only(base) + } +} + +fn decode_oris(instr: u32) -> Decoded { + let rs = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let uimm = instr & 0xFFFF; + Decoded::base_only(format!("oris {}, {}, 0x{:X}", gpr(ra), gpr(rs), uimm)) +} + +fn decode_d_logic(mnem: &str, instr: u32) -> Decoded { + let rs = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let uimm = instr & 0xFFFF; + Decoded::base_only(format!("{mnem:<8}{}, {}, 0x{:X}", gpr(ra), gpr(rs), uimm)) +} + +// ── Compare immediate ─────────────────────────────────────────────────────── + +fn decode_cmp_imm(mnem: &str, instr: u32, signed: bool) -> Decoded { + let bf = bits(instr, 8, 6); + let l_bit = bits(instr, 10, 10); + let ra = bits(instr, 15, 11); + let imm = if signed { + format!("{}", sign_ext(instr & 0xFFFF, 16)) + } else { + format!("0x{:X}", instr & 0xFFFF) + }; + let cr = if bf == 0 { String::new() } else { format!("cr{bf}, ") }; + let base = format!("{mnem:<8}{cr}{l_bit}, {}, {}", gpr(ra), imm); + + // Extended: cmpi → cmpwi/cmpdi, cmpli → cmplwi/cmpldi + let size = if l_bit == 0 { "w" } else { "d" }; + let ext_mnem = if mnem == "cmpi" { + format!("cmp{size}i") + } else { + format!("cmpl{size}i") + }; + let ext = format!("{ext_mnem:<8}{cr}{}, {}", gpr(ra), imm); + Decoded::with_ext(base, ext) +} + +// ── Load/store D-form ─────────────────────────────────────────────────────── + +fn decode_ls(mnem: &str, instr: u32) -> Decoded { + let rt = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let d = sign_ext(instr & 0xFFFF, 16); + let rn = if mnem.starts_with("lf") || mnem.starts_with("stf") { fpr(rt) } else { gpr(rt) }; + Decoded::base_only(format!("{mnem:<8}{}, {}({})", rn, d, gpr(ra))) +} + +// ── DS-form (ld/ldu/lwa) ─────────────────────────────────────────────────── + +fn decode_ds_form(instr: u32) -> Decoded { + let rt = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let ds = sign_ext(instr & 0xFFFC, 16); + let xo = instr & 3; + let mnem = match xo { 0 => "ld", 1 => "ldu", 2 => "lwa", _ => "ld?" }; + Decoded::base_only(format!("{mnem:<8}{}, {}({})", gpr(rt), ds, gpr(ra))) +} + +fn decode_ds_store(instr: u32) -> Decoded { + let rs = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let ds = sign_ext(instr & 0xFFFC, 16); + let xo = instr & 3; + let mnem = match xo { 0 => "std", 1 => "stdu", _ => "std?" }; + Decoded::base_only(format!("{mnem:<8}{}, {}({})", gpr(rs), ds, gpr(ra))) +} + +// ── I-form: b / bl / ba / bla ─────────────────────────────────────────────── + +fn decode_b(instr: u32, addr: u32) -> Decoded { + let li = sign_ext(instr & 0x03FFFFFC, 26); + let aa = instr & 2 != 0; + let lk = instr & 1 != 0; + let target = if aa { li as u32 } else { addr.wrapping_add(li as u32) }; + let mnem = match (aa, lk) { + (false, false) => "b", + (false, true) => "bl", + (true, false) => "ba", + (true, true) => "bla", + }; + Decoded::base_only(format!("{mnem:<8}0x{target:08X}")) +} + +// ── B-form: bc / bcl ──────────────────────────────────────────────────────── + +fn decode_bc(instr: u32, addr: u32) -> Decoded { + let bo = bits(instr, 10, 6); + let bi = bits(instr, 15, 11); + let bd = sign_ext(instr & 0xFFFC, 16); + let aa = instr & 2 != 0; + let lk = instr & 1 != 0; + let target = if aa { bd as u32 } else { addr.wrapping_add(bd as u32) }; + + let a = if aa { "a" } else { "" }; + let l = if lk { "l" } else { "" }; + let base = format!("bc{a}{l:<5}{bo}, {}, 0x{target:08X}", crb(bi)); + + // Simplified mnemonics + let cr_field = bi / 4; + let cr_bit = bi % 4; + let decr = bo & 0x04 == 0; + let uncond = bo & 0x10 != 0; // branch always if set + + if uncond && !decr { + return Decoded::with_ext(base, format!("b{a}{l:<7}0x{target:08X}")); + } + + let cond_true = bo & 0x08 != 0; + let cond_name = match (cr_bit, cond_true) { + (0, true) => "lt", (0, false) => "ge", + (1, true) => "gt", (1, false) => "le", + (2, true) => "eq", (2, false) => "ne", + (3, true) => "so", (3, false) => "ns", + _ => "??", + }; + + let cr = if cr_field == 0 { String::new() } else { format!("cr{cr_field}, ") }; + + let ext = if decr { + let z = if bo & 0x02 != 0 { "z" } else { "nz" }; + format!("bd{z}{cond_name}{a}{l:<4}{cr}0x{target:08X}") + } else { + format!("b{cond_name}{a}{l:<6}{cr}0x{target:08X}") + }; + Decoded::with_ext(base, ext) +} + +// ── Opcode 19 (XL-form): blr, bctr, crand, etc ───────────────────────────── + +fn decode_op19(instr: u32) -> Decoded { + let xo = bits(instr, 30, 21); + let bo = bits(instr, 10, 6); + let bi = bits(instr, 15, 11); + let lk = instr & 1 != 0; + let l = if lk { "l" } else { "" }; + + match xo { + 16 => { // bclr + let base = format!("bclr{l:<4}{bo}, {}", crb(bi)); + if bo == 20 && bi == 0 { + let ext = if lk { "blrl" } else { "blr" }; + return Decoded::with_ext(base, ext.to_string()); + } + // Simplified conditional: beqlr, bnelr, etc. + if let Some((cond, cr)) = cond_branch_ext(bo, bi) { + let cr_no_comma = cr.trim_end_matches(", "); + if cr_no_comma.is_empty() { + return Decoded::with_ext(base, format!("b{cond}lr{l}")); + } else { + return Decoded::with_ext(base, format!("b{cond}lr{l:<4}{cr_no_comma}")); + } + } + // bdnzlr / bdzlr + let decr = bo & 0x04 == 0; + let uncond = bo & 0x10 != 0; + if decr && uncond { + let z = if bo & 0x02 != 0 { "z" } else { "nz" }; + return Decoded::with_ext(base, format!("bd{z}lr{l}")); + } + Decoded::base_only(base) + } + 528 => { // bcctr + let base = format!("bcctr{l:<3}{bo}, {}", crb(bi)); + if bo == 20 && bi == 0 { + let ext = if lk { "bctrl" } else { "bctr" }; + return Decoded::with_ext(base, ext.to_string()); + } + if let Some((cond, cr)) = cond_branch_ext(bo, bi) { + let cr_no_comma = cr.trim_end_matches(", "); + if cr_no_comma.is_empty() { + return Decoded::with_ext(base, format!("b{cond}ctr{l}")); + } else { + return Decoded::with_ext(base, format!("b{cond}ctr{l:<3}{cr_no_comma}")); + } + } + Decoded::base_only(base) + } + 0 => Decoded::base_only(format!("mcrf cr{}, cr{}", bits(instr, 8, 6), bits(instr, 13, 11))), + 150 => Decoded::base_only("isync".into()), + 50 => Decoded::base_only("rfi".into()), + // CR logical operations with simplified forms + 33 => { // crnor + let bt = bits(instr, 10, 6); let ba = bits(instr, 15, 11); let bb = bits(instr, 20, 16); + let base = format!("crnor {}, {}, {}", crb(bt), crb(ba), crb(bb)); + if ba == bb { + Decoded::with_ext(base, format!("crnot {}, {}", crb(bt), crb(ba))) + } else { Decoded::base_only(base) } + } + 193 => { // crxor + let bt = bits(instr, 10, 6); let ba = bits(instr, 15, 11); let bb = bits(instr, 20, 16); + let base = format!("crxor {}, {}, {}", crb(bt), crb(ba), crb(bb)); + if bt == ba && ba == bb { + Decoded::with_ext(base, format!("crclr {}", crb(bt))) + } else { Decoded::base_only(base) } + } + 289 => { // creqv + let bt = bits(instr, 10, 6); let ba = bits(instr, 15, 11); let bb = bits(instr, 20, 16); + let base = format!("creqv {}, {}, {}", crb(bt), crb(ba), crb(bb)); + if bt == ba && ba == bb { + Decoded::with_ext(base, format!("crset {}", crb(bt))) + } else { Decoded::base_only(base) } + } + 449 => { // cror + let bt = bits(instr, 10, 6); let ba = bits(instr, 15, 11); let bb = bits(instr, 20, 16); + let base = format!("cror {}, {}, {}", crb(bt), crb(ba), crb(bb)); + if ba == bb { + Decoded::with_ext(base, format!("crmove {}, {}", crb(bt), crb(ba))) + } else { Decoded::base_only(base) } + } + 129 => Decoded::base_only(format!("crandc {}, {}, {}", crb(bits(instr, 10, 6)), crb(bits(instr, 15, 11)), crb(bits(instr, 20, 16)))), + 225 => Decoded::base_only(format!("crnand {}, {}, {}", crb(bits(instr, 10, 6)), crb(bits(instr, 15, 11)), crb(bits(instr, 20, 16)))), + 257 => Decoded::base_only(format!("crand {}, {}, {}", crb(bits(instr, 10, 6)), crb(bits(instr, 15, 11)), crb(bits(instr, 20, 16)))), + 417 => Decoded::base_only(format!("crorc {}, {}, {}", crb(bits(instr, 10, 6)), crb(bits(instr, 15, 11)), crb(bits(instr, 20, 16)))), + _ => Decoded::base_only(format!(".long 0x{instr:08X} ; op19 xo={xo}")), + } +} + +// ── Rotate/mask instructions ──────────────────────────────────────────────── + +fn decode_rlwimi(instr: u32) -> Decoded { + let rs = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let sh = bits(instr, 20, 16); + let mb = bits(instr, 25, 21); + let me = bits(instr, 30, 26); + let rc = if instr & 1 != 0 { "." } else { "" }; + let base = format!("rlwimi{rc:<2}{}, {}, {}, {}, {}", gpr(ra), gpr(rs), sh, mb, me); + // inslwi rA,rS,n,b = rlwimi rA,rS,32-b,b,b+n-1 → sh=32-b, n=me-mb+1 + if mb <= me && sh == (32u32.wrapping_sub(mb)) % 32 && sh != 31u32.wrapping_sub(me) { + let n = me - mb + 1; + let b = mb; + return Decoded::with_ext(base, format!("inslwi{rc:<2}{}, {}, {}, {}", gpr(ra), gpr(rs), n, b)); + } + // insrwi rA,rS,n,b = rlwimi rA,rS,32-(b+n),b,b+n-1 → sh=32-(me+1)=31-me, n=me-mb+1 + if mb <= me && sh == 31u32.wrapping_sub(me) % 32 { + let n = me - mb + 1; + let b = mb; + return Decoded::with_ext(base, format!("insrwi{rc:<2}{}, {}, {}, {}", gpr(ra), gpr(rs), n, b)); + } + Decoded::base_only(base) +} + +fn decode_rlwinm(instr: u32) -> Decoded { + let rs = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let sh = bits(instr, 20, 16); + let mb = bits(instr, 25, 21); + let me = bits(instr, 30, 26); + let rc = if instr & 1 != 0 { "." } else { "" }; + let base = format!("rlwinm{rc:<2}{}, {}, {}, {}, {}", gpr(ra), gpr(rs), sh, mb, me); + + // Priority-ordered simplified forms: + // slwi: shift left word immediate + if sh > 0 && mb == 0 && me == 31 - sh { + return Decoded::with_ext(base, format!("slwi{rc:<4}{}, {}, {}", gpr(ra), gpr(rs), sh)); + } + // srwi: shift right word immediate + if sh > 0 && me == 31 && sh + mb == 32 { + return Decoded::with_ext(base, format!("srwi{rc:<4}{}, {}, {}", gpr(ra), gpr(rs), 32 - sh)); + } + // rotlwi: rotate left word immediate (full 32-bit rotate) + if sh > 0 && mb == 0 && me == 31 { + return Decoded::with_ext(base, format!("rotlwi{rc:<2}{}, {}, {}", gpr(ra), gpr(rs), sh)); + } + // clrlwi: clear left n bits → rlwinm rA,rS,0,n,31 + if sh == 0 && me == 31 && mb > 0 { + return Decoded::with_ext(base, format!("clrlwi{rc:<2}{}, {}, {}", gpr(ra), gpr(rs), mb)); + } + // clrrwi: clear right n bits → rlwinm rA,rS,0,0,31-n + if sh == 0 && mb == 0 && me < 31 { + return Decoded::with_ext(base, format!("clrrwi{rc:<2}{}, {}, {}", gpr(ra), gpr(rs), 31 - me)); + } + // extlwi: extract and left-justify → rlwinm rA,rS,b,0,n-1 + if mb == 0 && sh > 0 && me < 31 { + let n = me + 1; + return Decoded::with_ext(base, format!("extlwi{rc:<2}{}, {}, {}, {}", gpr(ra), gpr(rs), n, sh)); + } + // extrwi: extract and right-justify → rlwinm rA,rS,b+n,32-n,31 + if me == 31 && mb > 0 && sh > 0 { + let n = 32 - mb; + let b = sh.wrapping_sub(n) % 32; + return Decoded::with_ext(base, format!("extrwi{rc:<2}{}, {}, {}, {}", gpr(ra), gpr(rs), n, b)); + } + Decoded::base_only(base) +} + +fn decode_rlwnm(instr: u32) -> Decoded { + let rs = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let rb = bits(instr, 20, 16); + let mb = bits(instr, 25, 21); + let me = bits(instr, 30, 26); + let rc = if instr & 1 != 0 { "." } else { "" }; + let base = format!("rlwnm{rc:<3}{}, {}, {}, {}, {}", gpr(ra), gpr(rs), gpr(rb), mb, me); + // rotlw: full rotate by register + if mb == 0 && me == 31 { + return Decoded::with_ext(base, format!("rotlw{rc:<3}{}, {}, {}", gpr(ra), gpr(rs), gpr(rb))); + } + Decoded::base_only(base) +} + +// ── Opcode 30 (MD/MDS-form: 64-bit rotate) ───────────────────────────────── + +fn decode_op30(instr: u32) -> Decoded { + let rs = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let rc = if instr & 1 != 0 { "." } else { "" }; + let sh = bits(instr, 20, 16) | (bits(instr, 30, 30) << 5); + let mb_me = bits(instr, 25, 21) | (bits(instr, 26, 26) << 5); + + let md_xo = bits(instr, 29, 27); // MD-form: bits 27-29 + + match md_xo { + 0 => { // rldicl: Rotate Left Doubleword Immediate then Clear Left + let mb = mb_me; + let base = format!("rldicl{rc:<2}{}, {}, {}, {}", gpr(ra), gpr(rs), sh, mb); + if sh == 0 && mb > 0 { + return Decoded::with_ext(base, format!("clrldi{rc:<2}{}, {}, {}", gpr(ra), gpr(rs), mb)); + } + if mb > 0 && sh == (64u32.wrapping_sub(mb)) & 63 { + return Decoded::with_ext(base, format!("srdi{rc:<4}{}, {}, {}", gpr(ra), gpr(rs), mb)); + } + if sh > 0 && mb == 0 { + return Decoded::with_ext(base, format!("rotldi{rc:<2}{}, {}, {}", gpr(ra), gpr(rs), sh)); + } + return Decoded::base_only(base); + } + 1 => { // rldicr: Rotate Left Doubleword Immediate then Clear Right + let me = mb_me; + let base = format!("rldicr{rc:<2}{}, {}, {}, {}", gpr(ra), gpr(rs), sh, me); + if sh > 0 && me == (63u32.wrapping_sub(sh)) & 63 { + return Decoded::with_ext(base, format!("sldi{rc:<4}{}, {}, {}", gpr(ra), gpr(rs), sh)); + } + if sh == 0 && me < 63 { + return Decoded::with_ext(base, format!("clrrdi{rc:<2}{}, {}, {}", gpr(ra), gpr(rs), 63 - me)); + } + return Decoded::base_only(base); + } + 2 => { // rldic: Rotate Left Doubleword Immediate then Clear + let mb = mb_me; + return Decoded::base_only(format!("rldic{rc:<3}{}, {}, {}, {}", gpr(ra), gpr(rs), sh, mb)); + } + 3 => { // rldimi: Rotate Left Doubleword Immediate then Mask Insert + let mb = mb_me; + let base = format!("rldimi{rc:<2}{}, {}, {}, {}", gpr(ra), gpr(rs), sh, mb); + if mb > 0 { + let n = (64u32.wrapping_sub(sh).wrapping_sub(mb)) & 63; + if n > 0 { + return Decoded::with_ext(base, format!("insrdi{rc:<2}{}, {}, {}, {}", gpr(ra), gpr(rs), n, mb)); + } + } + return Decoded::base_only(base); + } + _ => {} // Fall through to MDS-form + } + + // MDS-form: bits 27-30 + let mds_xo = bits(instr, 30, 27); + let rb = bits(instr, 20, 16); + match mds_xo { + 8 => { // rldcl: Rotate Left Doubleword then Clear Left + let mb = mb_me; + let base = format!("rldcl{rc:<3}{}, {}, {}, {}", gpr(ra), gpr(rs), gpr(rb), mb); + if mb == 0 { + return Decoded::with_ext(base, format!("rotld{rc:<3}{}, {}, {}", gpr(ra), gpr(rs), gpr(rb))); + } + return Decoded::base_only(base); + } + 9 => { // rldcr: Rotate Left Doubleword then Clear Right + let me = mb_me; + return Decoded::base_only(format!("rldcr{rc:<3}{}, {}, {}, {}", gpr(ra), gpr(rs), gpr(rb), me)); + } + _ => {} + } + + Decoded::base_only(format!(".long 0x{instr:08X} ; op30")) +} + +// ── Opcode 31 (X/XO/XFX forms) ───────────────────────────────────────────── + +fn decode_op31(instr: u32) -> Decoded { + let xo_10 = bits(instr, 30, 21); // bits 21-30 + let xo_9 = bits(instr, 30, 22); // bits 22-30 + let rt = bits(instr, 10, 6); + let ra = bits(instr, 15, 11); + let rb = bits(instr, 20, 16); + let rc = if instr & 1 != 0 { "." } else { "" }; + let oe = bits(instr, 21, 21) != 0; + + // XO-form (bits 22-30) + match xo_9 { + 266 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("add{o}{rc:<5}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + 10 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("addc{o}{rc:<4}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + 138 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("adde{o}{rc:<4}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + 234 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("addme{o}{rc:<3}{}, {}", gpr(rt), gpr(ra))); } + 202 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("addze{o}{rc:<3}{}, {}", gpr(rt), gpr(ra))); } + // subf rD, rA, rB = rD = rB - rA → ext: sub rD, rB, rA (natural operand order) + 40 => { + let o = if oe {"o"} else {""}; + let base = format!("subf{o}{rc:<4}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb)); + let ext = format!("sub{o}{rc:<5}{}, {}, {}", gpr(rt), gpr(rb), gpr(ra)); + return Decoded::with_ext(base, ext); + } + 8 => { + let o = if oe {"o"} else {""}; + let base = format!("subfc{o}{rc:<3}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb)); + let ext = format!("subc{o}{rc:<4}{}, {}, {}", gpr(rt), gpr(rb), gpr(ra)); + return Decoded::with_ext(base, ext); + } + 136 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("subfe{o}{rc:<3}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + 232 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("subfme{o}{rc:<2}{}, {}", gpr(rt), gpr(ra))); } + 200 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("subfze{o}{rc:<2}{}, {}", gpr(rt), gpr(ra))); } + 104 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("neg{o}{rc:<5}{}, {}", gpr(rt), gpr(ra))); } + 235 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("mullw{o}{rc:<3}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + 75 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("mulhw{o}{rc:<3}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + 11 => { return Decoded::base_only(format!("mulhwu{rc:<2}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + 491 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("divw{o}{rc:<4}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + 459 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("divwu{o}{rc:<3}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + // 64-bit multiply/divide + 233 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("mulld{o}{rc:<3}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + 73 => { return Decoded::base_only(format!("mulhd{rc:<4}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + 9 => { return Decoded::base_only(format!("mulhdu{rc:<2}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + 489 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("divd{o}{rc:<4}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + 457 => { let o = if oe {"o"} else {""}; return Decoded::base_only(format!("divdu{o}{rc:<3}{}, {}, {}", gpr(rt), gpr(ra), gpr(rb))); } + _ => {} + } + + // X-form (bits 21-30) + match xo_10 { + // Trap word (register form) + 4 => { + let to = rt; // TO field in bits 6-10 + let base = format!("tw {}, {}, {}", to, gpr(ra), gpr(rb)); + if to == 31 && ra == 0 && rb == 0 { + return Decoded::with_ext(base, "trap".to_string()); + } + if let Some(cond) = trap_cond(to) { + if !cond.is_empty() { + return Decoded::with_ext(base, format!("tw{cond:<6}{}, {}", gpr(ra), gpr(rb))); + } + } + return Decoded::base_only(base); + } + // Compare — with cmpw/cmpd extended forms + 0 => { + let bf = bits(instr, 8, 6); + let l_bit = bits(instr, 10, 10); + let cr = if bf == 0 { String::new() } else { format!("cr{bf}, ") }; + let base = format!("cmp {cr}{l_bit}, {}, {}", gpr(ra), gpr(rb)); + let size = if l_bit == 0 { "w" } else { "d" }; + let ext = format!("cmp{size:<5}{cr}{}, {}", gpr(ra), gpr(rb)); + return Decoded::with_ext(base, ext); + } + 32 => { + let bf = bits(instr, 8, 6); + let l_bit = bits(instr, 10, 10); + let cr = if bf == 0 { String::new() } else { format!("cr{bf}, ") }; + let base = format!("cmpl {cr}{l_bit}, {}, {}", gpr(ra), gpr(rb)); + let size = if l_bit == 0 { "w" } else { "d" }; + let ext = format!("cmpl{size:<4}{cr}{}, {}", gpr(ra), gpr(rb)); + return Decoded::with_ext(base, ext); + } + // Logic — with mr/not extended forms + 28 => { + let base = format!("and{rc:<5}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb)); + if rt == rb { return Decoded::with_ext(base, format!("mr{rc:<6}{}, {}", gpr(ra), gpr(rt))); } + return Decoded::base_only(base); + } + 60 => return Decoded::base_only(format!("andc{rc:<4}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb))), + 444 => { + let base = format!("or{rc:<6}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb)); + if rt == rb { return Decoded::with_ext(base, format!("mr{rc:<6}{}, {}", gpr(ra), gpr(rt))); } + return Decoded::base_only(base); + } + 412 => return Decoded::base_only(format!("orc{rc:<5}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb))), + 316 => return Decoded::base_only(format!("xor{rc:<5}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb))), + 476 => return Decoded::base_only(format!("nand{rc:<4}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb))), + 124 => { + let base = format!("nor{rc:<5}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb)); + if rt == rb { return Decoded::with_ext(base, format!("not{rc:<5}{}, {}", gpr(ra), gpr(rt))); } + return Decoded::base_only(base); + } + 284 => return Decoded::base_only(format!("eqv{rc:<5}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb))), + // Extend + 954 => return Decoded::base_only(format!("extsb{rc:<3}{}, {}", gpr(ra), gpr(rt))), + 922 => return Decoded::base_only(format!("extsh{rc:<3}{}, {}", gpr(ra), gpr(rt))), + 986 => return Decoded::base_only(format!("extsw{rc:<3}{}, {}", gpr(ra), gpr(rt))), + 26 => return Decoded::base_only(format!("cntlzw{rc:<2}{}, {}", gpr(ra), gpr(rt))), + 58 => return Decoded::base_only(format!("cntlzd{rc:<2}{}, {}", gpr(ra), gpr(rt))), + // Shift (32-bit and 64-bit) + 24 => return Decoded::base_only(format!("slw{rc:<5}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb))), + 536 => return Decoded::base_only(format!("srw{rc:<5}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb))), + 792 => return Decoded::base_only(format!("sraw{rc:<4}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb))), + 824 => { let sh = bits(instr, 20, 16); return Decoded::base_only(format!("srawi{rc:<3}{}, {}, {}", gpr(ra), gpr(rt), sh)); } + 27 => return Decoded::base_only(format!("sld{rc:<5}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb))), + 539 => return Decoded::base_only(format!("srd{rc:<5}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb))), + 794 => return Decoded::base_only(format!("srad{rc:<4}{}, {}, {}", gpr(ra), gpr(rt), gpr(rb))), + // Load indexed + 23 => return Decoded::base_only(format!("lwzx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 55 => return Decoded::base_only(format!("lwzux {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 87 => return Decoded::base_only(format!("lbzx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 119 => return Decoded::base_only(format!("lbzux {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 279 => return Decoded::base_only(format!("lhzx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 311 => return Decoded::base_only(format!("lhzux {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 343 => return Decoded::base_only(format!("lhax {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 375 => return Decoded::base_only(format!("lhaux {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 534 => return Decoded::base_only(format!("lwbrx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 790 => return Decoded::base_only(format!("lhbrx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 20 => return Decoded::base_only(format!("lwarx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 84 => return Decoded::base_only(format!("ldarx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + // 64-bit load indexed + 21 => return Decoded::base_only(format!("ldx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 53 => return Decoded::base_only(format!("ldux {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 341 => return Decoded::base_only(format!("lwax {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 373 => return Decoded::base_only(format!("lwaux {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 532 => return Decoded::base_only(format!("ldbrx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 533 => return Decoded::base_only(format!("lswx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 597 => return Decoded::base_only(format!("lswi {}, {}, {}", gpr(rt), gpr(ra), rb)), + // Store indexed + 151 => return Decoded::base_only(format!("stwx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 183 => return Decoded::base_only(format!("stwux {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 215 => return Decoded::base_only(format!("stbx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 247 => return Decoded::base_only(format!("stbux {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 407 => return Decoded::base_only(format!("sthx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 439 => return Decoded::base_only(format!("sthux {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 662 => return Decoded::base_only(format!("stwbrx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 918 => return Decoded::base_only(format!("sthbrx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 150 => return Decoded::base_only(format!("stwcx. {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + // 64-bit store indexed + 149 => return Decoded::base_only(format!("stdx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 181 => return Decoded::base_only(format!("stdux {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 214 => return Decoded::base_only(format!("stdcx. {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 660 => return Decoded::base_only(format!("stdbrx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 661 => return Decoded::base_only(format!("stswx {}, {}, {}", gpr(rt), gpr(ra), gpr(rb))), + 725 => return Decoded::base_only(format!("stswi {}, {}, {}", gpr(rt), gpr(ra), rb)), + // FP load/store indexed + 535 => return Decoded::base_only(format!("lfsx {}, {}, {}", fpr(rt), gpr(ra), gpr(rb))), + 567 => return Decoded::base_only(format!("lfsux {}, {}, {}", fpr(rt), gpr(ra), gpr(rb))), + 599 => return Decoded::base_only(format!("lfdx {}, {}, {}", fpr(rt), gpr(ra), gpr(rb))), + 631 => return Decoded::base_only(format!("lfdux {}, {}, {}", fpr(rt), gpr(ra), gpr(rb))), + 663 => return Decoded::base_only(format!("stfsx {}, {}, {}", fpr(rt), gpr(ra), gpr(rb))), + 695 => return Decoded::base_only(format!("stfsux {}, {}, {}", fpr(rt), gpr(ra), gpr(rb))), + 727 => return Decoded::base_only(format!("stfdx {}, {}, {}", fpr(rt), gpr(ra), gpr(rb))), + 759 => return Decoded::base_only(format!("stfdux {}, {}, {}", fpr(rt), gpr(ra), gpr(rb))), + 983 => return Decoded::base_only(format!("stfiwx {}, {}, {}", fpr(rt), gpr(ra), gpr(rb))), + // Trap doubleword (register form) + 68 => { + let to = rt; + let base = format!("td {}, {}, {}", to, gpr(ra), gpr(rb)); + if to == 31 && ra == 0 && rb == 0 { + return Decoded::with_ext(base, "trap".to_string()); + } + if let Some(cond) = trap_cond(to) { + if !cond.is_empty() { + return Decoded::with_ext(base, format!("td{cond:<6}{}, {}", gpr(ra), gpr(rb))); + } + } + return Decoded::base_only(base); + } + // AltiVec load indexed (standard 5-bit vr0-vr31) + 6 => return Decoded::base_only(format!("lvsl {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 38 => return Decoded::base_only(format!("lvsr {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 7 => return Decoded::base_only(format!("lvebx {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 39 => return Decoded::base_only(format!("lvehx {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 71 => return Decoded::base_only(format!("lvewx {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 103 => return Decoded::base_only(format!("lvx {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 359 => return Decoded::base_only(format!("lvxl {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 519 => return Decoded::base_only(format!("lvlx {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 551 => return Decoded::base_only(format!("lvrx {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 775 => return Decoded::base_only(format!("lvlxl {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 807 => return Decoded::base_only(format!("lvrxl {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + // AltiVec store indexed + 135 => return Decoded::base_only(format!("stvebx {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 167 => return Decoded::base_only(format!("stvehx {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 199 => return Decoded::base_only(format!("stvewx {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 231 => return Decoded::base_only(format!("stvx {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 487 => return Decoded::base_only(format!("stvxl {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 647 => return Decoded::base_only(format!("stvlx {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 679 => return Decoded::base_only(format!("stvrx {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 903 => return Decoded::base_only(format!("stvlxl {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + 935 => return Decoded::base_only(format!("stvrxl {}, {}, {}", vr(rt), gpr(ra), gpr(rb))), + // MSR + 83 => return Decoded::base_only(format!("mfmsr {}", gpr(rt))), + 146 => return Decoded::base_only(format!("mtmsr {}", gpr(rt))), + 178 => return Decoded::base_only(format!("mtmsrd {}", gpr(rt))), + // Time base + 371 => { + let tbr = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11); + let base = format!("mftb {}, {}", gpr(rt), tbr); + return match tbr { + 268 => Decoded::with_ext(base, format!("mftb {}", gpr(rt))), + 269 => Decoded::with_ext(base, format!("mftbu {}", gpr(rt))), + _ => Decoded::base_only(base), + }; + } + // Condition register XER + 512 => { + let crd = bits(instr, 8, 6); + return Decoded::base_only(format!("mcrxr cr{crd}")); + } + // SPR — with mflr/mfctr/mfxer / mtlr/mtctr/mtxer extended forms + 339 => { + let spr_raw = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11); + let base = format!("mfspr {}, {}", gpr(rt), spr_name(spr_raw)); + let ext = match spr_raw { + 8 => Some(format!("mflr {}", gpr(rt))), + 9 => Some(format!("mfctr {}", gpr(rt))), + 1 => Some(format!("mfxer {}", gpr(rt))), + _ => None, + }; + return match ext { + Some(e) => Decoded::with_ext(base, e), + None => Decoded::base_only(base), + }; + } + 467 => { + let spr_raw = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11); + let base = format!("mtspr {}, {}", spr_name(spr_raw), gpr(rt)); + let ext = match spr_raw { + 8 => Some(format!("mtlr {}", gpr(rt))), + 9 => Some(format!("mtctr {}", gpr(rt))), + 1 => Some(format!("mtxer {}", gpr(rt))), + _ => None, + }; + return match ext { + Some(e) => Decoded::with_ext(base, e), + None => Decoded::base_only(base), + }; + } + 19 => return Decoded::base_only(format!("mfcr {}", gpr(rt))), + 144 => { + let fxm = bits(instr, 19, 12); + let base = format!("mtcrf 0x{:02X}, {}", fxm, gpr(rt)); + if fxm == 0xFF { + return Decoded::with_ext(base, format!("mtcr {}", gpr(rt))); + } + return Decoded::base_only(base); + } + // Cache/sync — with lwsync extended form + 598 => { + let l_field = bits(instr, 10, 9); + let base = format!("sync {}", l_field); + if l_field == 0 { + return Decoded::with_ext(base, "sync".to_string()); + } else if l_field == 1 { + return Decoded::with_ext(base, "lwsync".to_string()); + } + return Decoded::base_only(base); + } + 854 => return Decoded::base_only("eieio".into()), + 86 => return Decoded::base_only(format!("dcbf {}, {}", gpr(ra), gpr(rb))), + 54 => return Decoded::base_only(format!("dcbst {}, {}", gpr(ra), gpr(rb))), + 278 => return Decoded::base_only(format!("dcbt {}, {}", gpr(ra), gpr(rb))), + 246 => return Decoded::base_only(format!("dcbtst {}, {}", gpr(ra), gpr(rb))), + 1014 => { + if rt == 1 { + return Decoded::base_only(format!("dcbz128 {}, {}", gpr(ra), gpr(rb))); + } + return Decoded::base_only(format!("dcbz {}, {}", gpr(ra), gpr(rb))); + } + 470 => return Decoded::base_only(format!("dcbi {}, {}", gpr(ra), gpr(rb))), + 982 => return Decoded::base_only(format!("icbi {}, {}", gpr(ra), gpr(rb))), + 566 => return Decoded::base_only("tlbsync".into()), + _ => {} + } + + // XS-form: sradi (bits 21-29, bit 30 is sh[5]) + let xo_9b = bits(instr, 29, 21); + if xo_9b == 413 { + let sh = bits(instr, 20, 16) | (bits(instr, 30, 30) << 5); + return Decoded::base_only(format!("sradi{rc:<3}{}, {}, {}", gpr(ra), gpr(rt), sh)); + } + + Decoded::base_only(format!(".long 0x{instr:08X} ; op31 xo10={xo_10} xo9={xo_9}")) +} + +// ── Opcode 59 (FP single-precision) ──────────────────────────────────────── + +fn decode_op59(instr: u32) -> Decoded { + let xo = bits(instr, 30, 26); + let frt = bits(instr, 10, 6); + let fra = bits(instr, 15, 11); + let frb = bits(instr, 20, 16); + let frc = bits(instr, 25, 21); + let rc = if instr & 1 != 0 { "." } else { "" }; + + Decoded::base_only(match xo { + 18 => format!("fdivs{rc:<3}{}, {}, {}", fpr(frt), fpr(fra), fpr(frb)), + 20 => format!("fsubs{rc:<3}{}, {}, {}", fpr(frt), fpr(fra), fpr(frb)), + 21 => format!("fadds{rc:<3}{}, {}, {}", fpr(frt), fpr(fra), fpr(frb)), + 22 => format!("fsqrts{rc:<2}{}, {}", fpr(frt), fpr(frb)), + 24 => format!("fres{rc:<4}{}, {}", fpr(frt), fpr(frb)), + 25 => format!("fmuls{rc:<3}{}, {}, {}", fpr(frt), fpr(fra), fpr(frc)), + 28 => format!("fmsubs{rc:<2}{}, {}, {}, {}", fpr(frt), fpr(fra), fpr(frc), fpr(frb)), + 29 => format!("fmadds{rc:<2}{}, {}, {}, {}", fpr(frt), fpr(fra), fpr(frc), fpr(frb)), + 30 => format!("fnmsubs{rc} {}, {}, {}, {}", fpr(frt), fpr(fra), fpr(frc), fpr(frb)), + 31 => format!("fnmadds{rc} {}, {}, {}, {}", fpr(frt), fpr(fra), fpr(frc), fpr(frb)), + _ => format!(".long 0x{instr:08X} ; op59 xo={xo}"), + }) +} + +// ── Opcode 63 (FP double-precision / X-form FP) ──────────────────────────── + +fn decode_op63(instr: u32) -> Decoded { + let xo_a = bits(instr, 30, 26); // A-form xo (bits 26-30) + let xo_x = bits(instr, 30, 21); // X-form xo (bits 21-30) + let frt = bits(instr, 10, 6); + let fra = bits(instr, 15, 11); + let frb = bits(instr, 20, 16); + let frc = bits(instr, 25, 21); + let rc = if instr & 1 != 0 { "." } else { "" }; + + // A-form first + match xo_a { + 18 => return Decoded::base_only(format!("fdiv{rc:<4}{}, {}, {}", fpr(frt), fpr(fra), fpr(frb))), + 20 => return Decoded::base_only(format!("fsub{rc:<4}{}, {}, {}", fpr(frt), fpr(fra), fpr(frb))), + 21 => return Decoded::base_only(format!("fadd{rc:<4}{}, {}, {}", fpr(frt), fpr(fra), fpr(frb))), + 22 => return Decoded::base_only(format!("fsqrt{rc:<3}{}, {}", fpr(frt), fpr(frb))), + 23 => return Decoded::base_only(format!("fsel{rc:<4}{}, {}, {}, {}", fpr(frt), fpr(fra), fpr(frc), fpr(frb))), + 25 => return Decoded::base_only(format!("fmul{rc:<4}{}, {}, {}", fpr(frt), fpr(fra), fpr(frc))), + 26 => return Decoded::base_only(format!("frsqrte{rc} {}, {}", fpr(frt), fpr(frb))), + 28 => return Decoded::base_only(format!("fmsub{rc:<3}{}, {}, {}, {}", fpr(frt), fpr(fra), fpr(frc), fpr(frb))), + 29 => return Decoded::base_only(format!("fmadd{rc:<3}{}, {}, {}, {}", fpr(frt), fpr(fra), fpr(frc), fpr(frb))), + 30 => return Decoded::base_only(format!("fnmsub{rc:<2}{}, {}, {}, {}", fpr(frt), fpr(fra), fpr(frc), fpr(frb))), + 31 => return Decoded::base_only(format!("fnmadd{rc:<2}{}, {}, {}, {}", fpr(frt), fpr(fra), fpr(frc), fpr(frb))), + _ => {} + } + + // X-form + match xo_x { + 0 => { let bf = bits(instr, 8, 6); return Decoded::base_only(format!("fcmpu cr{bf}, {}, {}", fpr(fra), fpr(frb))); } + 32 => { let bf = bits(instr, 8, 6); return Decoded::base_only(format!("fcmpo cr{bf}, {}, {}", fpr(fra), fpr(frb))); } + 12 => return Decoded::base_only(format!("frsp{rc:<4}{}, {}", fpr(frt), fpr(frb))), + 14 => return Decoded::base_only(format!("fctiw{rc:<3}{}, {}", fpr(frt), fpr(frb))), + 15 => return Decoded::base_only(format!("fctiwz{rc:<2}{}, {}", fpr(frt), fpr(frb))), + 40 => return Decoded::base_only(format!("fneg{rc:<4}{}, {}", fpr(frt), fpr(frb))), + 72 => return Decoded::base_only(format!("fmr{rc:<5}{}, {}", fpr(frt), fpr(frb))), + 136 => return Decoded::base_only(format!("fnabs{rc:<3}{}, {}", fpr(frt), fpr(frb))), + 264 => return Decoded::base_only(format!("fabs{rc:<4}{}, {}", fpr(frt), fpr(frb))), + 583 => return Decoded::base_only(format!("mffs{rc:<4}{}", fpr(frt))), + 711 => return Decoded::base_only(format!("mtfsf 0x{:02X}, {}", bits(instr, 17, 10), fpr(frb))), + // 64-bit FP conversions + 814 => return Decoded::base_only(format!("fctid{rc:<3}{}, {}", fpr(frt), fpr(frb))), + 815 => return Decoded::base_only(format!("fctidz{rc:<2}{}, {}", fpr(frt), fpr(frb))), + 846 => return Decoded::base_only(format!("fcfid{rc:<3}{}, {}", fpr(frt), fpr(frb))), + // FPSCR bit manipulation + 38 => return Decoded::base_only(format!("mtfsb1{rc:<2}{}", bits(instr, 10, 6))), + 70 => return Decoded::base_only(format!("mtfsb0{rc:<2}{}", bits(instr, 10, 6))), + 134 => { + let bf = bits(instr, 8, 6); + let imm = bits(instr, 19, 16); + return Decoded::base_only(format!("mtfsfi{rc:<2}cr{bf}, {}", imm)); + } + 64 => { + let bf = bits(instr, 8, 6); + let bfa = bits(instr, 13, 11); + return Decoded::base_only(format!("mcrfs cr{bf}, cr{bfa}")); + } + _ => {} + } + + Decoded::base_only(format!(".long 0x{instr:08X} ; op63 xo_a={xo_a} xo_x={xo_x}")) +} + +// ── Opcode 4 (AltiVec / VMX128 load-store) ───────────────────────────────── + +fn decode_op4(instr: u32) -> Decoded { + let vd = bits(instr, 10, 6); + let va = bits(instr, 15, 11); + let vb = bits(instr, 20, 16); + + // 1. VMX128 load/store (VX128_1 form): key = (bits 21-27 << 4) | bits 30-31 + let vmx_ls = (bits(instr, 27, 21) << 4) | bits(instr, 31, 30); + let vd128_val = vd128(instr); + let vmx_ls_name = match vmx_ls { + 0b00000000011 => Some("lvsl128"), + 0b00001000011 => Some("lvsr128"), + 0b00010000011 => Some("lvewx128"), + 0b00011000011 => Some("lvx128"), + 0b01011000011 => Some("lvxl128"), + 0b10000000011 => Some("lvlx128"), + 0b10001000011 => Some("lvrx128"), + 0b11000000011 => Some("lvlxl128"), + 0b11001000011 => Some("lvrxl128"), + 0b00110000011 => Some("stvewx128"), + 0b00111000011 => Some("stvx128"), + 0b01111000011 => Some("stvxl128"), + 0b10100000011 => Some("stvlx128"), + 0b10101000011 => Some("stvrx128"), + 0b11100000011 => Some("stvlxl128"), + 0b11101000011 => Some("stvrxl128"), + _ => None, + }; + if let Some(mnem) = vmx_ls_name { + return Decoded::base_only(format!("{mnem:<12}{}, {}, {}", vr(vd128_val), gpr(va), gpr(vb))); + } + + // 2. VX-form: bits 21-31 (11-bit key) + let vx = bits(instr, 31, 21); + + // 3-operand: VD, VA, VB + let vx_3op = match vx { + 0 => Some("vaddubm"), 2 => Some("vmaxub"), 4 => Some("vrlb"), + 8 => Some("vmuloub"), 10 => Some("vaddfp"), 12 => Some("vmrghb"), + 14 => Some("vpkuhum"), 64 => Some("vadduhm"), 66 => Some("vmaxuh"), + 68 => Some("vrlh"), 72 => Some("vmulouh"), 74 => Some("vsubfp"), + 76 => Some("vmrghh"), 78 => Some("vpkuwum"), 128 => Some("vadduwm"), + 130 => Some("vmaxuw"), 132 => Some("vrlw"), 140 => Some("vmrghw"), + 142 => Some("vpkuhus"), 206 => Some("vpkuwus"), + 258 => Some("vmaxsb"), 260 => Some("vslb"), 264 => Some("vmulosb"), + 268 => Some("vmrglb"), 270 => Some("vpkshus"), + 322 => Some("vmaxsh"), 324 => Some("vslh"), 328 => Some("vmulosh"), + 332 => Some("vmrglh"), 334 => Some("vpkswus"), + 384 => Some("vaddcuw"), 386 => Some("vmaxsw"), 388 => Some("vslw"), + 396 => Some("vmrglw"), 398 => Some("vpkshss"), + 452 => Some("vsl"), 462 => Some("vpkswss"), + 512 => Some("vaddubs"), 514 => Some("vminub"), 516 => Some("vsrb"), + 520 => Some("vmuleub"), + 576 => Some("vadduhs"), 578 => Some("vminuh"), 580 => Some("vsrh"), + 584 => Some("vmuleuh"), + 640 => Some("vadduws"), 642 => Some("vminuw"), 644 => Some("vsrw"), + 708 => Some("vsr"), + 768 => Some("vaddsbs"), 770 => Some("vminsb"), 772 => Some("vsrab"), + 776 => Some("vmulesb"), 782 => Some("vpkpx"), + 832 => Some("vaddshs"), 834 => Some("vminsh"), 836 => Some("vsrah"), + 840 => Some("vmulesh"), + 896 => Some("vaddsws"), 898 => Some("vminsw"), 900 => Some("vsraw"), + 1024 => Some("vsububm"), 1026 => Some("vavgub"), 1028 => Some("vand"), + 1034 => Some("vmaxfp"), 1036 => Some("vslo"), + 1088 => Some("vsubuhm"), 1090 => Some("vavguh"), 1092 => Some("vandc"), + 1098 => Some("vminfp"), 1100 => Some("vsro"), + 1152 => Some("vsubuwm"), 1154 => Some("vavguw"), 1156 => Some("vor"), + 1220 => Some("vxor"), + 1282 => Some("vavgsb"), 1284 => Some("vnor"), + 1346 => Some("vavgsh"), + 1408 => Some("vsubcuw"), 1410 => Some("vavgsw"), + 1536 => Some("vsububs"), 1544 => Some("vsum4ubs"), + 1600 => Some("vsubuhs"), 1608 => Some("vsum4shs"), + 1664 => Some("vsubuws"), 1672 => Some("vsum2sws"), + 1792 => Some("vsubsbs"), 1800 => Some("vsum4sbs"), + 1856 => Some("vsubshs"), + 1920 => Some("vsubsws"), 1928 => Some("vsumsws"), + _ => None, + }; + if let Some(mnem) = vx_3op { + return Decoded::base_only(format!("{mnem:<8}{}, {}, {}", vr(vd), vr(va), vr(vb))); + } + + // Unary: VD, VB (VA field unused) + let vx_unary = match vx { + 266 => Some("vrefp"), 330 => Some("vrsqrtefp"), 394 => Some("vexptefp"), + 458 => Some("vlogefp"), 522 => Some("vrfin"), 586 => Some("vrfiz"), + 650 => Some("vrfip"), 714 => Some("vrfim"), + 526 => Some("vupkhsb"), 590 => Some("vupkhsh"), + 654 => Some("vupklsb"), 718 => Some("vupklsh"), + 846 => Some("vupkhpx"), 974 => Some("vupklpx"), + _ => None, + }; + if let Some(mnem) = vx_unary { + return Decoded::base_only(format!("{mnem:<8}{}, {}", vr(vd), vr(vb))); + } + + // VD, VB, UIMM (VA field = UIMM) + let vx_uimm = match vx { + 524 => Some("vspltb"), 588 => Some("vsplth"), 652 => Some("vspltw"), + 778 => Some("vcfux"), 842 => Some("vcfsx"), + 906 => Some("vctuxs"), 970 => Some("vctsxs"), + _ => None, + }; + if let Some(mnem) = vx_uimm { + return Decoded::base_only(format!("{mnem:<8}{}, {}, {}", vr(vd), vr(vb), va)); + } + + // VD, SIMM (VA field = sign-extended 5-bit immediate) + match vx { + 780 => { + let simm = sign_ext(va, 5); + return Decoded::base_only(format!("vspltisb {}, {}", vr(vd), simm)); + } + 844 => { + let simm = sign_ext(va, 5); + return Decoded::base_only(format!("vspltish {}, {}", vr(vd), simm)); + } + 908 => { + let simm = sign_ext(va, 5); + return Decoded::base_only(format!("vspltisw {}, {}", vr(vd), simm)); + } + // Special + 1540 => return Decoded::base_only(format!("mfvscr {}", vr(vd))), + 1604 => return Decoded::base_only(format!("mtvscr {}", vr(vb))), + _ => {} + } + + // 3. VC-form (compare): bits 22-31 (10-bit key), Rc in bit 21 + let vc = bits(instr, 31, 22); + let vc_rc = if bits(instr, 21, 21) != 0 { "." } else { "" }; + let vc_cmp = match vc { + 6 => Some("vcmpequb"), 70 => Some("vcmpequh"), 134 => Some("vcmpequw"), + 198 => Some("vcmpeqfp"), 454 => Some("vcmpgefp"), + 518 => Some("vcmpgtub"), 582 => Some("vcmpgtuh"), 646 => Some("vcmpgtuw"), + 710 => Some("vcmpgtfp"), + 774 => Some("vcmpgtsb"), 838 => Some("vcmpgtsh"), 902 => Some("vcmpgtsw"), + 966 => Some("vcmpbfp"), + _ => None, + }; + if let Some(mnem) = vc_cmp { + let full = format!("{mnem}{vc_rc}"); + return Decoded::base_only(format!("{full:<12}{}, {}, {}", vr(vd), vr(va), vr(vb))); + } + + // 4. VA-form: bits 26-31 (6-bit key), 4-operand VD, VA, VB, VC + let va_key = bits(instr, 31, 26); + let vc_reg = bits(instr, 25, 21); + + let va_4op = match va_key { + 32 => Some("vmhaddshs"), 33 => Some("vmhraddshs"), + 34 => Some("vmladduhm"), + 36 => Some("vmsumubm"), 37 => Some("vmsummbm"), + 38 => Some("vmsumuhm"), 39 => Some("vmsumuhs"), + 40 => Some("vmsumshm"), 41 => Some("vmsumshs"), + 42 => Some("vsel"), 43 => Some("vperm"), + _ => None, + }; + if let Some(mnem) = va_4op { + return Decoded::base_only(format!("{mnem:<12}{}, {}, {}, {}", vr(vd), vr(va), vr(vb), vr(vc_reg))); + } + + match va_key { + 44 => { // vsldoi VD, VA, VB, SH + let sh = bits(instr, 25, 22); + return Decoded::base_only(format!("vsldoi {}, {}, {}, {}", vr(vd), vr(va), vr(vb), sh)); + } + 46 => return Decoded::base_only(format!("vmaddfp {}, {}, {}, {}", vr(vd), vr(va), vr(vc_reg), vr(vb))), + 47 => return Decoded::base_only(format!("vnmsubfp {}, {}, {}, {}", vr(vd), vr(va), vr(vc_reg), vr(vb))), + _ => {} + } + + // 5. vsldoi128: bit 27 == 1 (VX128_5 form) + if bits(instr, 27, 27) == 1 { + let vd_128 = vd128(instr); + let va_128 = va128(instr); + let vb_128 = vb128(instr); + let sh = bits(instr, 25, 22); + return Decoded::base_only(format!("vsldoi128 {}, {}, {}, {}", vr(vd_128), vr(va_128), vr(vb_128), sh)); + } + + Decoded::base_only(format!(".long 0x{instr:08X} ; op4")) +} + +// ── Opcode 5 (VMX128 operations) ─────────────────────────────────────────── + +fn decode_op5(instr: u32) -> Decoded { + let vd = vd128(instr); + let va = va128(instr); + let vb = vb128(instr); + let vc = bits(instr, 25, 23); // 3-bit VC field + + // Table 1: vperm128 — key = (bit 22 << 5) | bit 27 + let key1 = (bits(instr, 22, 22) << 5) | bits(instr, 27, 27); + if key1 == 0 { + return Decoded::base_only(format!("vperm128 {}, {}, {}, {}", vr(vd), vr(va), vr(vb), vc)); + } + + // Table 2: key = (bits 22-25 << 2) | bit 27 + let key2 = (bits(instr, 25, 22) << 2) | bits(instr, 27, 27); + + // 3-operand VD, VA, VB + let op5_3 = match key2 { + 0b000001 => Some("vaddfp128"), + 0b000101 => Some("vsubfp128"), + 0b001001 => Some("vmulfp128"), + 0b011001 => Some("vmsum3fp128"), + 0b011101 => Some("vmsum4fp128"), + 0b100000 => Some("vpkshss128"), + 0b100100 => Some("vpkshus128"), + 0b101000 => Some("vpkswss128"), + 0b101100 => Some("vpkswus128"), + 0b110000 => Some("vpkuhum128"), + 0b110100 => Some("vpkuhus128"), + 0b111000 => Some("vpkuwum128"), + 0b111100 => Some("vpkuwus128"), + 0b100001 => Some("vand128"), + 0b100101 => Some("vandc128"), + 0b101001 => Some("vnor128"), + 0b101101 => Some("vor128"), + 0b110001 => Some("vxor128"), + 0b110101 => Some("vsel128"), + 0b111001 => Some("vslo128"), + 0b111101 => Some("vsro128"), + _ => None, + }; + if let Some(mnem) = op5_3 { + return Decoded::base_only(format!("{mnem:<12}{}, {}, {}", vr(vd), vr(va), vr(vb))); + } + + // 4-operand VD, VA, VB, VC (3-bit) + let op5_4 = match key2 { + 0b001101 => Some("vmaddfp128"), + 0b010001 => Some("vmaddcfp128"), + 0b010101 => Some("vnmsubfp128"), + _ => None, + }; + if let Some(mnem) = op5_4 { + return Decoded::base_only(format!("{mnem:<12}{}, {}, {}, {}", vr(vd), vr(va), vr(vb), vc)); + } + + Decoded::base_only(format!(".long 0x{instr:08X} ; op5")) +} + +// ── Opcode 6 (VMX128 special operations) ─────────────────────────────────── + +fn decode_op6(instr: u32) -> Decoded { + let vd = vd128(instr); + let vb = vb128(instr); + + // Table 1: vpermwi128 (kVX128_P form) + let key1 = (bits(instr, 22, 21) << 5) | bits(instr, 27, 26); + if key1 == 0b0100001 { + let uimm = bits(instr, 15, 11) | (bits(instr, 25, 23) << 5); + return Decoded::base_only(format!("vpermwi128 {}, {}, 0x{:X}", vr(vd), vr(vb), uimm)); + } + + // Table 2: vpkd3d128, vrlimi128 (kVX128_4 form) + let key2 = (bits(instr, 23, 21) << 4) | bits(instr, 27, 26); + match key2 { + 0b1100001 => { + let imm = bits(instr, 15, 11); + let z = bits(instr, 25, 24); + return Decoded::base_only(format!("vpkd3d128 {}, {}, {}, {}", vr(vd), vr(vb), imm, z)); + } + 0b1110001 => { + let imm = bits(instr, 15, 11); + let z = bits(instr, 25, 24); + return Decoded::base_only(format!("vrlimi128 {}, {}, {}, {}", vr(vd), vr(vb), imm, z)); + } + _ => {} + } + + // Table 3: kVX128_3 form (key = bits 21-27) + let key3 = bits(instr, 27, 21); + let uimm3 = bits(instr, 15, 11); + match key3 { + // Unary: VD, VB + 0b0110011 => return Decoded::base_only(format!("vrfim128 {}, {}", vr(vd), vr(vb))), + 0b0110111 => return Decoded::base_only(format!("vrfin128 {}, {}", vr(vd), vr(vb))), + 0b0111011 => return Decoded::base_only(format!("vrfip128 {}, {}", vr(vd), vr(vb))), + 0b0111111 => return Decoded::base_only(format!("vrfiz128 {}, {}", vr(vd), vr(vb))), + 0b1100011 => return Decoded::base_only(format!("vrefp128 {}, {}", vr(vd), vr(vb))), + 0b1100111 => return Decoded::base_only(format!("vrsqrtefp128 {}, {}", vr(vd), vr(vb))), + 0b1101011 => return Decoded::base_only(format!("vexptefp128 {}, {}", vr(vd), vr(vb))), + 0b1101111 => return Decoded::base_only(format!("vlogefp128 {}, {}", vr(vd), vr(vb))), + // VD, VB, UIMM + 0b0100011 => return Decoded::base_only(format!("vcfpsxws128 {}, {}, {}", vr(vd), vr(vb), uimm3)), + 0b0100111 => return Decoded::base_only(format!("vcfpuxws128 {}, {}, {}", vr(vd), vr(vb), uimm3)), + 0b0101011 => return Decoded::base_only(format!("vcsxwfp128 {}, {}, {}", vr(vd), vr(vb), uimm3)), + 0b0101111 => return Decoded::base_only(format!("vcuxwfp128 {}, {}, {}", vr(vd), vr(vb), uimm3)), + 0b1110011 => return Decoded::base_only(format!("vspltw128 {}, {}, {}", vr(vd), vr(vb), uimm3)), + 0b1111111 => return Decoded::base_only(format!("vupkd3d128 {}, {}, {}", vr(vd), vr(vb), uimm3)), + // VD, SIMM + 0b1110111 => { + let simm = sign_ext(uimm3, 5); + return Decoded::base_only(format!("vspltisw128 {}, {}", vr(vd), simm)); + } + _ => {} + } + + // Table 4: Compare (kVX128_R form) + // key = (bits 22-24 << 3) | bit 27 + let key4 = (bits(instr, 24, 22) << 3) | bits(instr, 27, 27); + let va = va128(instr); + let rc6 = if bits(instr, 25, 25) != 0 { "." } else { "" }; + let cmp_name = match key4 { + 0b000000 => Some("vcmpeqfp128"), + 0b001000 => Some("vcmpgefp128"), + 0b010000 => Some("vcmpgtfp128"), + 0b011000 => Some("vcmpbfp128"), + 0b100000 => Some("vcmpequw128"), + _ => None, + }; + if let Some(mnem) = cmp_name { + let full = format!("{mnem}{rc6}"); + return Decoded::base_only(format!("{full:<14}{}, {}, {}", vr(vd), vr(va), vr(vb))); + } + + // Table 5: Shift/rotate/misc (kVX128 form) + // key = (bits 22-25 << 2) | bit 27 + let key5 = (bits(instr, 25, 22) << 2) | bits(instr, 27, 27); + let shift_name = match key5 { + 0b000101 => Some("vrlw128"), + 0b001101 => Some("vslw128"), + 0b010101 => Some("vsraw128"), + 0b011101 => Some("vsrw128"), + 0b101000 => Some("vmaxfp128"), + 0b101100 => Some("vminfp128"), + 0b110000 => Some("vmrghw128"), + 0b110100 => Some("vmrglw128"), + 0b111000 => Some("vupkhsb128"), + 0b111100 => Some("vupklsb128"), + _ => None, + }; + if let Some(mnem) = shift_name { + return Decoded::base_only(format!("{mnem:<12}{}, {}, {}", vr(vd), vr(va), vr(vb))); + } + + Decoded::base_only(format!(".long 0x{instr:08X} ; op6")) +} diff --git a/crates/xenia-analysis/src/xref.rs b/crates/xenia-analysis/src/xref.rs new file mode 100644 index 0000000..d5028ce --- /dev/null +++ b/crates/xenia-analysis/src/xref.rs @@ -0,0 +1,296 @@ +//! Cross-reference analysis for Xbox 360 PE images. + +use std::collections::HashMap; +use xenia_xex::pe::PeSection; +use crate::func::FuncAnalysis; + +// ── Cross-reference types ──────────────────────────────────────────────── + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum XrefKind { + Call, // bl + Jump, // b (unconditional) + Branch, // bc / bXX (conditional) + DataRead, // lwz, lbz, lhz, lha, lfs, lfd, etc. from resolved address + DataWrite, // stw, stb, sth, stfs, stfd, etc. to resolved address + DataRef, // address computed via lis+addi/ori but not directly loaded/stored +} + +impl XrefKind { + pub fn tag(self) -> &'static str { + match self { + XrefKind::Call => "call", + XrefKind::Jump => "j", + XrefKind::Branch => "br", + XrefKind::DataRead => "read", + XrefKind::DataWrite => "write", + XrefKind::DataRef => "ref", + } + } + + pub fn is_data(self) -> bool { + matches!(self, XrefKind::DataRead | XrefKind::DataWrite | XrefKind::DataRef) + } + + pub fn db_tag(self) -> &'static str { + self.tag() + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Xref { + pub source: u32, + pub kind: XrefKind, +} + +pub type XrefMap = HashMap>; + +/// Result of cross-reference analysis. +pub struct XrefResult { + pub labels: HashMap, + pub xrefs: XrefMap, + pub data_annotations: HashMap, +} + +/// Perform full cross-reference analysis on a PE image. +pub fn analyze_xrefs( + pe: &[u8], + image_base: u32, + entry_point: u32, + sections: &[PeSection], + func_analysis: &FuncAnalysis, + import_map: &HashMap, +) -> XrefResult { + let func_labels = func_analysis.generate_labels(); + let mut labels: HashMap = func_labels; + labels.insert(entry_point, "entry_point".to_string()); + + // Add import thunks as labels + for (addr, name) in import_map { + labels.insert(*addr, format!("__imp_{}", name.replace("::", "_"))); + } + + // First pass: collect branch targets + cross-references from code sections + let mut xrefs: XrefMap = HashMap::new(); + + for section in sections { + if !section.is_code() { continue; } + let va_start = section.virtual_address; + let va_end = va_start + section.virtual_size; + let file_start = section.virtual_address as usize; + + let mut addr = va_start; + while addr < va_end { + let abs_addr = image_base + addr; + let off = (addr - va_start) as usize + file_start; + if off + 4 > pe.len() { break; } + let instr = u32::from_be_bytes([ + pe[off], pe[off+1], pe[off+2], pe[off+3] + ]); + + collect_branch_target(instr, abs_addr, &mut labels, &mut xrefs); + addr += 4; + } + } + + // Second pass: resolve data references via lis+load/store pattern matching + let mut data_annotations: HashMap = HashMap::new(); + + // Build set of valid data address ranges for filtering false positives + let data_ranges: Vec<(u32, u32)> = sections.iter() + .map(|s| (image_base + s.virtual_address, + image_base + s.virtual_address + s.virtual_size)) + .collect(); + + for section in sections { + if !section.is_code() { continue; } + let va_start = section.virtual_address; + let va_end = va_start + section.virtual_size; + let file_start = section.virtual_address as usize; + + // Register state: track lis results. reg_hi[r] = Some(high_16_bits << 16) + let mut reg_hi: [Option; 32] = [None; 32]; + + let mut addr = va_start; + while addr < va_end { + let abs_addr = image_base + addr; + let off = (addr - va_start) as usize + file_start; + if off + 4 > pe.len() { break; } + let instr = u32::from_be_bytes([ + pe[off], pe[off+1], pe[off+2], pe[off+3] + ]); + + let opcode = (instr >> 26) & 0x3F; + let rd = ((instr >> 21) & 0x1F) as usize; + let ra = ((instr >> 16) & 0x1F) as usize; + let simm = ((instr & 0xFFFF) as i16) as i32; + let uimm = (instr & 0xFFFF) as u32; + + // Reset tracking on function boundaries (prologue = mfspr rN, LR) + if opcode == 31 { + let xo = (instr >> 1) & 0x3FF; + if xo == 339 { // mfspr + let spr = (((instr >> 16) & 0x1F) << 5) | ((instr >> 11) & 0x1F); + if spr == 8 { // LR + reg_hi = [None; 32]; + } + } + } + + match opcode { + // lis rD, IMM (encoded as addis rD, r0, IMM) + 15 if ra == 0 => { + reg_hi[rd] = Some(uimm << 16); + } + // addis rD, rA, IMM (rA != 0) — if rA has known lis, update + 15 if ra != 0 => { + if let Some(base) = reg_hi[ra] { + reg_hi[rd] = Some(base.wrapping_add(uimm << 16)); + } else { + reg_hi[rd] = None; + } + } + // addi rD, rA, IMM — compute full address if rA has known lis + 14 if ra != 0 => { + if let Some(base) = reg_hi[ra] { + let data_addr = base.wrapping_add(simm as u32); + if is_in_ranges(data_addr, &data_ranges) { + data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRef)); + xrefs.entry(data_addr).or_default().push(Xref { source: abs_addr, kind: XrefKind::DataRef }); + labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}")); + } + reg_hi[rd] = Some(data_addr); // propagate for chained access + } else { + reg_hi[rd] = None; + } + } + // ori rA, rS, UIMM — compute full address + 24 => { + let rs = rd; // source is bits 21-25 for ori + if let Some(base) = reg_hi[rs] { + let data_addr = base | uimm; + if is_in_ranges(data_addr, &data_ranges) { + data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRef)); + xrefs.entry(data_addr).or_default().push(Xref { source: abs_addr, kind: XrefKind::DataRef }); + labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}")); + } + reg_hi[ra] = Some(data_addr); + } else { + reg_hi[ra] = None; + } + } + // Load instructions: lwz, lbz, lhz, lha, lfs, lfd, lwzu, etc. + 32 | 33 | 34 | 35 | 40 | 41 | 42 | 43 | 46 | 48 | 49 | 50 | 51 => { + if ra != 0 { + if let Some(base) = reg_hi[ra] { + let data_addr = base.wrapping_add(simm as u32); + if is_in_ranges(data_addr, &data_ranges) { + data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRead)); + xrefs.entry(data_addr).or_default().push(Xref { source: abs_addr, kind: XrefKind::DataRead }); + labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}")); + } + } + } + // Load into rD may clobber the tracked value + reg_hi[rd] = None; + } + // Store instructions: stw, stb, sth, stfs, stfd, stwu, etc. + 36 | 37 | 38 | 39 | 44 | 45 | 47 | 52 | 53 | 54 | 55 => { + if ra != 0 { + if let Some(base) = reg_hi[ra] { + let data_addr = base.wrapping_add(simm as u32); + if is_in_ranges(data_addr, &data_ranges) { + data_annotations.insert(abs_addr, (data_addr, XrefKind::DataWrite)); + xrefs.entry(data_addr).or_default().push(Xref { source: abs_addr, kind: XrefKind::DataWrite }); + labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}")); + } + } + } + } + // Any other instruction writing to rD: invalidate + _ => { + // Conservatively invalidate for instructions that modify rD + // (most ALU ops, loads, etc.) + if opcode != 18 && opcode != 16 && opcode != 17 { // skip branch/sc + reg_hi[rd] = None; + } + } + } + + addr += 4; + } + } + + XrefResult { labels, xrefs, data_annotations } +} + +fn collect_branch_target(instr: u32, addr: u32, labels: &mut HashMap, xrefs: &mut XrefMap) { + let op = (instr >> 26) & 0x3F; + match op { + 18 => { + // I-form: b/bl/ba/bla + let li = sign_ext26(instr & 0x03FFFFFC); + let aa = instr & 2 != 0; + let lk = instr & 1 != 0; + let target = if aa { li as u32 } else { addr.wrapping_add(li as u32) }; + labels.entry(target).or_insert_with(|| format!("loc_{target:08X}")); + let kind = if lk { XrefKind::Call } else { XrefKind::Jump }; + xrefs.entry(target).or_default().push(Xref { source: addr, kind }); + } + 16 => { + // B-form: bc/bcl + let bd = sign_ext16(instr & 0xFFFC); + let aa = instr & 2 != 0; + let target = if aa { bd as u32 } else { addr.wrapping_add(bd as u32) }; + labels.entry(target).or_insert_with(|| format!("loc_{target:08X}")); + xrefs.entry(target).or_default().push(Xref { source: addr, kind: XrefKind::Branch }); + } + _ => {} + } +} + +fn sign_ext16(val: u32) -> i32 { + ((val << 16) as i32) >> 16 +} + +fn sign_ext26(val: u32) -> i32 { + ((val << 6) as i32) >> 6 +} + +fn is_in_ranges(addr: u32, ranges: &[(u32, u32)]) -> bool { + ranges.iter().any(|&(start, end)| addr >= start && addr < end) +} + +/// Find which section a data address falls in. +pub fn section_for_addr<'a>(addr: u32, sections: &'a [PeSection], image_base: u32) -> Option<&'a str> { + for s in sections { + let start = image_base + s.virtual_address; + let end = start + s.virtual_size; + if addr >= start && addr < end { + return Some(&s.name); + } + } + None +} + +/// Resolve a source address to "function_name+0xNN" or just "0xADDR". +pub fn resolve_source_label( + addr: u32, + func_analysis: &FuncAnalysis, + labels: &HashMap, +) -> String { + // Direct label hit? + if let Some(lbl) = labels.get(&addr) { + return lbl.clone(); + } + + // Find the containing function (largest start <= addr) + if let Some((&func_start, _fi)) = func_analysis.functions.range(..=addr).next_back() { + if let Some(func_label) = labels.get(&func_start) { + let offset = addr - func_start; + return format!("{func_label}+0x{offset:X}"); + } + } + + format!("0x{addr:08X}") +} diff --git a/crates/xenia-app/Cargo.toml b/crates/xenia-app/Cargo.toml new file mode 100644 index 0000000..001a2d9 --- /dev/null +++ b/crates/xenia-app/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "xenia-app" +version.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "xenia-rs" +path = "src/main.rs" + +[dependencies] +xenia-types = { workspace = true } +xenia-memory = { workspace = true } +xenia-cpu = { workspace = true } +xenia-xex = { workspace = true } +xenia-vfs = { workspace = true } +xenia-kernel = { workspace = true } +xenia-gpu = { workspace = true } +xenia-apu = { workspace = true } +xenia-hid = { workspace = true } +xenia-debugger = { workspace = true } +xenia-analysis = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +anyhow = { workspace = true } +clap = { version = "4", features = ["derive"] } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs new file mode 100644 index 0000000..6aa3694 --- /dev/null +++ b/crates/xenia-app/src/main.rs @@ -0,0 +1,812 @@ +use std::collections::HashMap; + +use anyhow::Result; +use clap::{Parser, Subcommand}; +use tracing_subscriber::EnvFilter; + +use xenia_kernel::ModuleId; +use xenia_memory::MemoryAccess; + +#[derive(Parser)] +#[command(name = "xenia-rs")] +#[command(about = "Xbox 360 emulator for reverse engineering and preservation")] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Disassemble a XEX file from its entry point + Disasm { + /// Path to XEX file + path: String, + /// Number of instructions to disassemble + #[arg(short = 'n', default_value = "64")] + count: usize, + }, + /// Load and execute a XEX file with tracing + Exec { + /// Path to XEX file + path: String, + /// Maximum instructions to execute before stopping (unlimited if omitted) + #[arg(short = 'n')] + max_instructions: Option, + /// SQLite database to write to. Includes the full static analysis + /// that `dis --db` would produce, plus any opt-in trace tables. + #[arg(long)] + db: Option, + /// Log each executed instruction to the `exec_trace` table + #[arg(long)] + trace_instructions: bool, + /// Log kernel/import calls to the `import_calls` table + #[arg(long)] + trace_imports: bool, + /// Log taken branches (calls, returns, jumps) to the `branch_trace` table + #[arg(long)] + trace_branches: bool, + /// Suppress banners, kernel-call logs, and final register dump + /// (only errors, faults, halts, and the summary line are printed) + #[arg(long)] + quiet: bool, + }, + /// Browse XISO disc image contents + Browse { + /// Path to XISO file + path: String, + }, + /// Display XEX header information + Info { + /// Path to XEX file + path: String, + }, + /// Extract PE image and metadata from a XEX file + Extract { + /// Path to XEX or ISO file + path: String, + /// Output directory (default: same directory as input) + #[arg(short, long)] + output: Option, + /// Write base tables (metadata, sections, imports) to a SQLite database + #[arg(long)] + db: Option, + }, + /// Full disassembly with function detection, cross-references, and optional database + Dis { + /// Path to XEX or ISO file + path: String, + /// Output .asm file (default: stdout) + #[arg(short, long)] + output: Option, + /// Output SQLite database (also includes the base extract tables) + #[arg(long)] + db: Option, + /// Suppress assembly text output (DB-only mode) + #[arg(long)] + quiet: bool, + }, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + // Bump default log level to `warn` for quiet exec runs so kernel-call + // tracing::info! spam is filtered out. RUST_LOG still wins if set. + let exec_quiet = matches!(&cli.command, Commands::Exec { quiet: true, .. }); + let default_level = if exec_quiet { "warn" } else { "info" }; + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env().add_directive(default_level.parse()?)) + .init(); + + match cli.command { + Commands::Disasm { path, count } => cmd_disasm(&path, count), + Commands::Exec { + path, + max_instructions, + db, + trace_instructions, + trace_imports, + trace_branches, + quiet, + } => cmd_exec( + &path, + max_instructions, + db.as_deref(), + trace_instructions, + trace_imports, + trace_branches, + quiet, + ), + Commands::Browse { path } => cmd_browse(&path), + Commands::Info { path } => cmd_info(&path), + Commands::Extract { path, output, db } => cmd_extract(&path, output.as_deref(), db.as_deref()), + Commands::Dis { path, output, db, quiet } => cmd_dis(&path, output.as_deref(), db.as_deref(), quiet), + } +} + +/// Load XEX data from a path. If the path is an ISO, extract default.xex from it. +fn load_xex_data(path: &str) -> Result> { + let lower = path.to_lowercase(); + if lower.ends_with(".iso") || lower.ends_with(".xiso") { + use xenia_vfs::VfsDevice; + println!("Detected disc image, extracting default.xex..."); + let disc = xenia_vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path)) + .map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?; + disc.read_file("default.xex") + .map_err(|e| anyhow::anyhow!("Failed to extract default.xex from disc image: {}", e)) + } else { + Ok(std::fs::read(path)?) + } +} + +fn cmd_info(path: &str) -> Result<()> { + let data = load_xex_data(path)?; + let header = xenia_xex::loader::parse_xex2_header(&data)?; + + println!("=== XEX2 Header ==="); + println!("Magic: {:#010x}", header.magic); + println!("Module Flags: {:#010x}", header.module_flags); + println!("Header Size: {:#x}", header.header_size); + println!("Headers: {}", header.header_count); + + if let Some(entry) = xenia_xex::loader::get_entry_point(&header) { + println!("Entry Point: {:#010x}", entry); + } + if let Some(base) = xenia_xex::loader::get_image_base(&header) { + println!("Image Base: {:#010x}", base); + } + + println!("\n=== Optional Headers ==="); + for h in &header.optional_headers { + println!(" Key: {:#010x} Value: {:#010x}", h.key, h.value); + } + + if let Some(ref sec) = header.security_info { + println!("\n=== Security Info ==="); + println!("Image Size: {:#x}", sec.image_size); + println!("Load Address: {:#010x}", sec.load_address); + println!("Image Flags: {:#010x}", sec.image_flags); + println!("Page Descs: {}", sec.page_descriptors.len()); + } + + if let Some(ref ffi) = header.file_format_info { + println!("\n=== File Format ==="); + println!("Encryption: {}", match ffi.encryption_type { + 0 => "None", 1 => "Normal (AES)", _ => "Unknown" + }); + println!("Compression: {}", match ffi.compression_type { + 0 => "None", 1 => "Basic", 2 => "Normal (LZX)", _ => "Unknown" + }); + if !ffi.basic_blocks.is_empty() { + println!("Basic blocks: {}", ffi.basic_blocks.len()); + } + if ffi.normal_window_size != 0 { + println!("LZX Window: {:#x}", ffi.normal_window_size); + } + } + + if let Some(ref name) = header.original_pe_name { + println!("\nOriginal PE: {}", name); + } + + if let Some(ref ei) = header.execution_info { + println!("\n=== Execution Info ==="); + println!("Title ID: {:#010x}", ei.title_id); + println!("Media ID: {:#010x}", ei.media_id); + println!("Disc: {} of {}", ei.disc_number, ei.disc_count); + } + + if !header.import_libraries.is_empty() { + println!("\n=== Import Libraries ==="); + for lib in &header.import_libraries { + println!(" {} (v{:#010x}, {} imports)", lib.name, lib.version_cur, lib.imports.len()); + } + } + + Ok(()) +} + +fn cmd_disasm(path: &str, count: usize) -> Result<()> { + let data = load_xex_data(path)?; + let header = xenia_xex::loader::parse_xex2_header(&data)?; + + let entry = xenia_xex::loader::get_entry_point(&header) + .ok_or_else(|| anyhow::anyhow!("No entry point found in XEX2 header"))?; + let base = xenia_xex::loader::get_image_base(&header) + .ok_or_else(|| anyhow::anyhow!("No image base found in XEX2 header"))?; + + println!("Entry point: {:#010x}, Image base: {:#010x}", entry, base); + + // Load and decompress the image + let image_data = xenia_xex::loader::load_image(&data, &header)?; + println!("Image loaded: {} bytes decompressed", image_data.len()); + println!("Disassembly from entry point ({} instructions):\n", count); + + let entry_offset = (entry - base) as usize; + if entry_offset + count * 4 <= image_data.len() { + let block = xenia_cpu::disasm::disassemble_block(&image_data[entry_offset..], entry, count); + for (addr, text) in block { + println!(" {:#010x}: {}", addr, text); + } + } else { + println!(" (entry point offset {:#x} is outside image bounds, image is {:#x} bytes)", entry_offset, image_data.len()); + } + + Ok(()) +} + +fn cmd_exec( + path: &str, + max_instructions: Option, + db_path: Option<&str>, + trace_instructions: bool, + trace_imports: bool, + trace_branches: bool, + quiet: bool, +) -> Result<()> { + let data = load_xex_data(path)?; + let mut header = xenia_xex::loader::parse_xex2_header(&data)?; + + let entry = xenia_xex::loader::get_entry_point(&header) + .ok_or_else(|| anyhow::anyhow!("No entry point found"))?; + let base = xenia_xex::loader::get_image_base(&header) + .ok_or_else(|| anyhow::anyhow!("No image base found"))?; + + if !quiet { + if let Some(ref ffi) = header.file_format_info { + println!("Compression: {} (encryption: {})", + match ffi.compression_type { + 0 => "none", 1 => "basic", 2 => "normal (LZX)", _ => "unknown" + }, + match ffi.encryption_type { + 0 => "none", 1 => "normal (AES)", _ => "unknown" + }); + } + if !header.import_libraries.is_empty() { + println!("Import libraries:"); + for lib in &header.import_libraries { + println!(" {} ({} imports)", lib.name, lib.imports.len()); + } + } + println!("Loading XEX: entry={:#010x} base={:#010x}", entry, base); + } + + // Allocate guest memory + let mut mem = xenia_memory::GuestMemory::new() + .map_err(|e| anyhow::anyhow!("Failed to allocate guest memory: {}", e))?; + + // Load and decompress the XEX image + let image_data = xenia_xex::loader::load_image(&data, &header)?; + + // Resolve import ordinals from PE image + xenia_xex::loader::resolve_imports(&mut header, &image_data); + + let alloc_size = ((image_data.len() + 4095) & !4095) as u32; + let rw = xenia_memory::page_table::MemoryProtect::READ + | xenia_memory::page_table::MemoryProtect::WRITE; + mem.alloc(base, alloc_size, rw) + .map_err(|e| anyhow::anyhow!("Failed to allocate guest memory region: {}", e))?; + mem.write_bulk(base, &image_data); + + // ── Phase 1: Build import thunk map ────────────────────────────────── + let mut thunk_map: HashMap = HashMap::new(); + for lib in &header.import_libraries { + let module = match lib.name.as_str() { + "xboxkrnl.exe" => ModuleId::Xboxkrnl, + "xam.xex" => ModuleId::Xam, + _ => continue, + }; + for imp in &lib.imports { + if imp.record_type == 1 { + let name = xenia_analysis::resolve_ordinal(&lib.name, imp.ordinal) + .map(|s| s.to_string()) + .unwrap_or_else(|| format!("ordinal_{:#06X}", imp.ordinal)); + thunk_map.insert(imp.address, (module, imp.ordinal, name)); + } + } + } + if !quiet { + println!("Import thunks mapped: {}", thunk_map.len()); + } + + // ── Phase 2: CPU initialization per xenia-canary ───────────────────── + // Allocate stack (1MB at 0x70000000) + let stack_base = 0x7000_0000u32; + let stack_size = 0x10_0000u32; + mem.alloc(stack_base, stack_size, rw) + .map_err(|e| anyhow::anyhow!("Failed to allocate stack: {}", e))?; + + // Allocate PCR (Processor Control Region) and TLS + let pcr_addr = 0x7FFF_0000u32; + let tls_addr = 0x7FFE_0000u32; + mem.alloc(pcr_addr, 0x1000, rw)?; + mem.alloc(tls_addr, 0x1000, rw)?; + + // Initialize PCR structure + mem.write_u32(pcr_addr, tls_addr); // PCR->tls_ptr + mem.write_u32(pcr_addr + 0x100, 0x1000); // PCR->current_thread (fake) + mem.write_u32(pcr_addr + 0x150, 0); // PCR->dpc_active + + // Set up CPU context per xenia-canary/cpu/thread_state.cc + let mut ctx = xenia_cpu::PpcContext::new(); + ctx.pc = entry; + ctx.gpr[1] = (stack_base + stack_size - 0x80) as u64; // Stack pointer + ctx.gpr[2] = 0x2000_0000; // RTOC + ctx.gpr[13] = pcr_addr as u64; // PCR/TLS pointer + ctx.msr = 0x9030; // Hardware-dumped MSR + + // ── Phase 3: Data export patching (variable imports) ───────────────── + for lib in &header.import_libraries { + for imp in &lib.imports { + if imp.record_type != 0 { continue; } // Only variable entries + let addr = imp.address; + match (lib.name.as_str(), imp.ordinal) { + ("xboxkrnl.exe", 0x0158) => { + // XboxKrnlVersion: {major=2, minor=0, build=20000, qfe=0} + mem.write_u16(addr, 2); + mem.write_u16(addr + 2, 0); + mem.write_u16(addr + 4, 20000); + mem.write_u16(addr + 6, 0); + } + ("xboxkrnl.exe", 0x0193) => { + // XexExecutableModuleHandle -> image base + mem.write_u32(addr, base); + } + ("xboxkrnl.exe", 0x01C0) => { + // VdGpuClockInMHz + mem.write_u32(addr, 500); + } + _ => { + // All other variable exports: write 0 + mem.write_u32(addr, 0); + } + } + } + } + + // ── Phase 4: Set up kernel ─────────────────────────────────────────── + let mut kernel = xenia_kernel::KernelState::new(); + kernel.image_base = base; + + // ── Phase 5: Set up SQLite DB with full static analysis + opt-in traces ── + let mut db_writer: Option = None; + if let Some(db) = db_path { + use std::collections::HashMap; + + let sections = xenia_xex::pe::parse_sections(&image_data)?; + + // Build import address -> name map (for xref analysis) + let mut import_map: HashMap = HashMap::new(); + for lib in &header.import_libraries { + for imp in &lib.imports { + let resolved = xenia_analysis::resolve_ordinal(&lib.name, imp.ordinal); + let name = match resolved { + Some(n) => format!("{}::{}", lib.name, n), + None => format!("{}::ordinal_{:#06X}", lib.name, imp.ordinal), + }; + import_map.insert(imp.address, name); + } + } + + // Function + xref analysis + let code_sections: Vec<(u32, u32, u32)> = sections.iter() + .filter(|s| s.is_code()) + .map(|s| (s.virtual_address, s.virtual_size, s.flags)) + .collect(); + let func_analysis = xenia_analysis::func::analyze(&image_data, base, entry, &code_sections); + eprintln!("Functions detected: {}", func_analysis.functions.len()); + + let xref_result = xenia_analysis::xref::analyze_xrefs( + &image_data, base, entry, §ions, &func_analysis, &import_map, + ); + let total_xrefs: usize = xref_result.xrefs.values().map(|v| v.len()).sum(); + eprintln!("Labels: {}, Cross-references: {}", xref_result.labels.len(), total_xrefs); + + let disasm_info = xenia_analysis::formatter::DisasmInfo { + image_base: base, + entry_point: entry, + original_pe_name: header.original_pe_name.as_deref(), + title_id: header.execution_info.as_ref().map(|e| e.title_id), + media_id: header.execution_info.as_ref().map(|e| e.media_id), + sections: §ions, + import_libraries: &header.import_libraries, + }; + + eprintln!("Writing database to {db}..."); + let mut w = xenia_analysis::DbWriter::open_fresh(std::path::Path::new(db))?; + w.write_base(&disasm_info)?; + w.write_disasm(&image_data, &disasm_info, &func_analysis, &xref_result.labels, &xref_result.xrefs)?; + w.prepare_trace_tables(trace_instructions, trace_imports, trace_branches)?; + db_writer = Some(w); + } + + // Set up debugger (in-memory trace disabled when any file-based trace is on) + let mut debugger = xenia_debugger::Debugger::new(); + debugger.paused = false; + debugger.step_mode = xenia_debugger::StepMode::Run; + debugger.trace_enabled = !trace_instructions; + + if !quiet { + match max_instructions { + Some(n) => println!("Starting execution (max {n} instructions)...\n"), + None => println!("Starting execution (unlimited)...\n"), + } + } + + // ── Phase 6: Execution loop with thunk interception ────────────────── + use xenia_cpu::interpreter::{step, StepResult}; + use xenia_cpu::PpcOpcode; + + let mut instruction_count: u64 = 0; + let mut unimpl_count: u64 = 0; + let mut import_count: u64 = 0; + + loop { + if let Some(limit) = max_instructions { + if instruction_count >= limit { + println!("\nReached max instruction count ({limit})"); + break; + } + } + + // ── Import thunk interception ── + if let Some((module, ordinal, name)) = thunk_map.get(&ctx.pc) { + let module = *module; + let ordinal_u32 = *ordinal as u32; + + let thunk_pc = ctx.pc; + let args = [ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.gpr[6]]; + + kernel.call_export(module, ordinal_u32, &mut ctx, &mut mem); + + if let Some(ref mut db) = db_writer { + db.log_import_call(xenia_analysis::ImportCallEntry { + address: thunk_pc, + cycle: ctx.cycle_count, + module: match module { + ModuleId::Xboxkrnl => "xboxkrnl.exe".to_string(), + ModuleId::Xam => "xam.xex".to_string(), + ModuleId::Xbdm => "xbdm.xex".to_string(), + }, + ordinal: *ordinal, + name: name.clone(), + arg_r3: args[0], + arg_r4: args[1], + arg_r5: args[2], + arg_r6: args[3], + return_value: ctx.gpr[3], + }); + } + + // Simulate blr (return to caller) + ctx.pc = ctx.lr as u32; + ctx.cycle_count += 1; + instruction_count += 1; + import_count += 1; + continue; + } + + // Check if PC is in mapped memory + if !mem.is_mapped(ctx.pc) { + println!("[{:>8}] FAULT: PC {:#010x} is in unmapped memory", instruction_count, ctx.pc); + break; + } + + // Pre-step debugger + debugger.pre_step(&ctx, &mem); + + let pc_before = ctx.pc; + // Decode the instruction word before step() so we can classify branches + let raw_before = mem.read_u32(pc_before); + let opcode_before = xenia_cpu::decode(raw_before, pc_before).opcode; + + let result = step(&mut ctx, &mut mem); + instruction_count += 1; + + if let Some(ref mut db) = db_writer { + db.log_instruction(xenia_analysis::ExecTraceEntry { + address: pc_before, + cycle: ctx.cycle_count, + r3: ctx.gpr[3], + r4: ctx.gpr[4], + lr: ctx.lr, + sp: ctx.gpr[1], + }); + + // Branch detection — fallthrough (pc_before + 4) means untaken conditional + if opcode_before.is_branch() && ctx.pc != pc_before.wrapping_add(4) { + let lk = (raw_before & 1) == 1; + let kind: &'static str = if lk { + "call" + } else if opcode_before == PpcOpcode::bclrx { + "return" + } else if opcode_before == PpcOpcode::bcctrx { + "jump" + } else { + "branch" + }; + db.log_branch(xenia_analysis::BranchTraceEntry { + cycle: ctx.cycle_count, + source: pc_before, + target: ctx.pc, + kind, + lr: ctx.lr, + }); + } + } + + // Post-step debugger + debugger.post_step(&ctx, &mem); + + match result { + StepResult::Continue => {} + StepResult::SystemCall => { + tracing::warn!("SYSCALL at {:#010x}", pc_before); + } + StepResult::Unimplemented(op) => { + unimpl_count += 1; + if unimpl_count <= 50 { + println!("[{:>8}] UNIMPL: {:?} at {:#010x}", instruction_count, op, pc_before); + } else if unimpl_count == 51 { + println!(" (suppressing further UNIMPL messages)"); + } + } + StepResult::Trap => { + tracing::warn!("TRAP at {:#010x}", pc_before); + } + StepResult::Halted => { + println!("[{:>8}] HALTED", instruction_count); + break; + } + } + + if debugger.should_break() { + println!("[{:>8}] BREAK at {:#010x}", instruction_count, ctx.pc); + break; + } + } + + // Finalize trace DB (flush buffers + create indices) + if let Some(ref mut db) = db_writer { + db.finalize_traces()?; + } + + if !quiet { + println!("\n=== Final State ==="); + println!("PC: {:#010x}", ctx.pc); + println!("LR: {:#010x}", ctx.lr as u32); + println!("CTR: {:#010x}", ctx.ctr as u32); + println!("CR: {:#010x}", ctx.cr()); + println!("XER: CA={} OV={} SO={}", ctx.xer_ca, ctx.xer_ov, ctx.xer_so); + for i in 0..32 { + if ctx.gpr[i] != 0 { + println!("r{:<2}: {:#018x}", i, ctx.gpr[i]); + } + } + } + println!("Executed {} instructions ({} import calls, {} unimplemented)", + instruction_count, import_count, unimpl_count); + if !quiet && db_writer.is_none() { + println!("Trace log: {} entries", debugger.trace_log.len()); + } + + Ok(()) +} + +fn cmd_browse(path: &str) -> Result<()> { + use xenia_vfs::VfsDevice; + + let disc = xenia_vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path)) + .map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?; + + println!("=== XISO Contents: {} ===", path); + match disc.list_root() { + Ok(entries) => { + for entry in entries { + let kind = if entry.is_directory { "DIR " } else { "FILE" }; + println!(" {} {:>10} {}", kind, entry.size, entry.name); + } + } + Err(e) => println!(" Error listing contents: {}", e), + } + + Ok(()) +} + +/// Helper: load XEX, parse header, decompress PE, resolve imports, parse sections. +fn load_and_prepare(path: &str) -> Result<(xenia_xex::Xex2Header, Vec, Vec)> { + let data = load_xex_data(path)?; + let mut header = xenia_xex::loader::parse_xex2_header(&data)?; + + let entry = xenia_xex::loader::get_entry_point(&header) + .ok_or_else(|| anyhow::anyhow!("No entry point found in XEX2 header"))?; + let base = xenia_xex::loader::get_image_base(&header) + .ok_or_else(|| anyhow::anyhow!("No image base found in XEX2 header"))?; + + eprintln!("Entry point: {:#010x}, Image base: {:#010x}", entry, base); + + let pe_image = xenia_xex::loader::load_image(&data, &header)?; + eprintln!("Image loaded: {} bytes decompressed", pe_image.len()); + + // Resolve import ordinals and record types from the PE image + xenia_xex::loader::resolve_imports(&mut header, &pe_image); + + // Parse PE sections + let sections = xenia_xex::pe::parse_sections(&pe_image)?; + eprintln!("PE sections: {}", sections.len()); + + Ok((header, pe_image, sections)) +} + +fn cmd_extract(path: &str, output_dir: Option<&str>, db_path: Option<&str>) -> Result<()> { + use serde::Serialize; + + let (header, pe_image, sections) = load_and_prepare(path)?; + + let entry = xenia_xex::loader::get_entry_point(&header).unwrap(); + let base = xenia_xex::loader::get_image_base(&header).unwrap(); + let image_size = header.security_info.as_ref().map(|s| s.image_size).unwrap_or(0); + + // Build JSON-serializable info struct + #[derive(Serialize)] + struct Xex2Info<'a> { + module_flags: u32, + image_base: u32, + entry_point: u32, + image_size: u32, + original_pe_name: Option<&'a str>, + execution_info: &'a Option, + import_libraries: &'a [xenia_xex::header::ImportLibrary], + sections: &'a [xenia_xex::pe::PeSection], + } + + let info = Xex2Info { + module_flags: header.module_flags, + image_base: base, + entry_point: entry, + image_size, + original_pe_name: header.original_pe_name.as_deref(), + execution_info: &header.execution_info, + import_libraries: &header.import_libraries, + sections: §ions, + }; + + // Determine output directory + let input_path = std::path::Path::new(path); + let out_dir = match output_dir { + Some(d) => std::path::PathBuf::from(d), + None => input_path.parent().unwrap_or(std::path::Path::new(".")).to_path_buf(), + }; + std::fs::create_dir_all(&out_dir)?; + + let stem = input_path.file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("output"); + + // Write PE image + let pe_path = out_dir.join(format!("{stem}.pe")); + std::fs::write(&pe_path, &pe_image)?; + eprintln!("Wrote PE image: {} ({} bytes)", pe_path.display(), pe_image.len()); + + // Write JSON metadata + let json_path = out_dir.join(format!("{stem}.xex.json")); + let json = serde_json::to_string_pretty(&info)?; + std::fs::write(&json_path, &json)?; + eprintln!("Wrote metadata: {}", json_path.display()); + + // Print summary + let total_imports: usize = header.import_libraries.iter().map(|l| l.imports.len()).sum(); + println!("Extracted: {} sections, {} import libraries ({} imports)", + sections.len(), header.import_libraries.len(), total_imports); + if let Some(ref ei) = header.execution_info { + println!("Title ID: 0x{:08X} Media ID: 0x{:08X}", ei.title_id, ei.media_id); + } + + // Write base tables to SQLite if requested + if let Some(db) = db_path { + let disasm_info = xenia_analysis::formatter::DisasmInfo { + image_base: base, + entry_point: entry, + original_pe_name: header.original_pe_name.as_deref(), + title_id: header.execution_info.as_ref().map(|e| e.title_id), + media_id: header.execution_info.as_ref().map(|e| e.media_id), + sections: §ions, + import_libraries: &header.import_libraries, + }; + eprintln!("Writing base tables to {db}..."); + let mut w = xenia_analysis::DbWriter::open_fresh(std::path::Path::new(db))?; + w.write_base(&disasm_info)?; + eprintln!("Database written: {db}"); + } + + Ok(()) +} + +fn cmd_dis(path: &str, output: Option<&str>, db_path: Option<&str>, quiet: bool) -> Result<()> { + use std::collections::HashMap; + + let (header, pe_image, sections) = load_and_prepare(path)?; + + let entry = xenia_xex::loader::get_entry_point(&header).unwrap(); + let base = xenia_xex::loader::get_image_base(&header).unwrap(); + + // Build import address -> name map + let mut import_map: HashMap = HashMap::new(); + for lib in &header.import_libraries { + for imp in &lib.imports { + let resolved = xenia_analysis::resolve_ordinal(&lib.name, imp.ordinal); + let name = match resolved { + Some(n) => format!("{}::{}", lib.name, n), + None => format!("{}::ordinal_{:#06X}", lib.name, imp.ordinal), + }; + import_map.insert(imp.address, name); + } + } + eprintln!("Resolved {} import thunks", import_map.len()); + + // Function analysis + let code_sections: Vec<(u32, u32, u32)> = sections.iter() + .filter(|s| s.is_code()) + .map(|s| (s.virtual_address, s.virtual_size, s.flags)) + .collect(); + let func_analysis = xenia_analysis::func::analyze(&pe_image, base, entry, &code_sections); + eprintln!("Functions detected: {}", func_analysis.functions.len()); + + // Cross-reference analysis + let xref_result = xenia_analysis::xref::analyze_xrefs( + &pe_image, base, entry, §ions, &func_analysis, &import_map, + ); + let total_xrefs: usize = xref_result.xrefs.values().map(|v| v.len()).sum(); + eprintln!("Labels: {}, Cross-references: {}", xref_result.labels.len(), total_xrefs); + + // Build DisasmInfo + let disasm_info = xenia_analysis::formatter::DisasmInfo { + image_base: base, + entry_point: entry, + original_pe_name: header.original_pe_name.as_deref(), + title_id: header.execution_info.as_ref().map(|e| e.title_id), + media_id: header.execution_info.as_ref().map(|e| e.media_id), + sections: §ions, + import_libraries: &header.import_libraries, + }; + + // SQLite database output (base + disasm layers) + if let Some(db) = db_path { + eprintln!("Writing database to {db}..."); + let mut w = xenia_analysis::DbWriter::open_fresh(std::path::Path::new(db))?; + w.write_base(&disasm_info)?; + w.write_disasm( + &pe_image, + &disasm_info, + &func_analysis, + &xref_result.labels, + &xref_result.xrefs, + )?; + eprintln!("Database written: {db}"); + } + + // Assembly output (skipped when --quiet and no --output specified) + if !quiet || output.is_some() { + let mut out: Box = match output { + Some(path) => Box::new(std::io::BufWriter::new(std::fs::File::create(path)?)), + None => Box::new(std::io::BufWriter::new(std::io::stdout().lock())), + }; + + xenia_analysis::formatter::write_asm( + &mut *out, + &pe_image, + &disasm_info, + &func_analysis, + &xref_result.labels, + &import_map, + &xref_result.xrefs, + &xref_result.data_annotations, + )?; + + if let Some(path) = output { + eprintln!("Wrote disassembly: {path}"); + } + } + + Ok(()) +} diff --git a/crates/xenia-apu/Cargo.toml b/crates/xenia-apu/Cargo.toml new file mode 100644 index 0000000..7da119c --- /dev/null +++ b/crates/xenia-apu/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "xenia-apu" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/xenia-apu/src/lib.rs b/crates/xenia-apu/src/lib.rs new file mode 100644 index 0000000..885083c --- /dev/null +++ b/crates/xenia-apu/src/lib.rs @@ -0,0 +1,16 @@ +/// Audio processing unit stub. Logging only for now. +pub struct AudioSystem { + pub enabled: bool, +} + +impl AudioSystem { + pub fn new() -> Self { + Self { enabled: false } + } +} + +impl Default for AudioSystem { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/xenia-cpu/Cargo.toml b/crates/xenia-cpu/Cargo.toml new file mode 100644 index 0000000..3ca488b --- /dev/null +++ b/crates/xenia-cpu/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "xenia-cpu" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +xenia-memory = { workspace = true } +tracing = { workspace = true } +bitflags = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/xenia-cpu/src/context.rs b/crates/xenia-cpu/src/context.rs new file mode 100644 index 0000000..b500c50 --- /dev/null +++ b/crates/xenia-cpu/src/context.rs @@ -0,0 +1,191 @@ +use xenia_types::Vec128; + +/// Condition register field (one of CR0-CR7). +#[derive(Debug, Clone, Copy, Default)] +pub struct CrField { + pub lt: bool, + pub gt: bool, + pub eq: bool, + pub so: bool, +} + +impl CrField { + pub fn as_u8(&self) -> u8 { + ((self.lt as u8) << 3) | ((self.gt as u8) << 2) | ((self.eq as u8) << 1) | (self.so as u8) + } + + pub fn from_u8(val: u8) -> Self { + Self { + lt: val & 8 != 0, + gt: val & 4 != 0, + eq: val & 2 != 0, + so: val & 1 != 0, + } + } +} + +/// SPR (Special Purpose Register) numbers used by mfspr/mtspr. +pub mod spr { + pub const XER: u32 = 1; + pub const LR: u32 = 8; + pub const CTR: u32 = 9; + pub const TBL: u32 = 268; + pub const TBU: u32 = 269; + pub const SPRG0: u32 = 272; + pub const SPRG1: u32 = 273; + pub const SPRG2: u32 = 274; + pub const SPRG3: u32 = 275; + pub const PVR: u32 = 287; + pub const PIR: u32 = 1023; +} + +/// PowerPC processor context. Holds all register state for one guest thread. +/// Mirrors PPCContext from ppc_context.h, minus JIT-specific fields. +#[repr(C, align(64))] +pub struct PpcContext { + // General purpose registers (R0-R31) + pub gpr: [u64; 32], + // Count register + pub ctr: u64, + // Link register + pub lr: u64, + // Machine state register + pub msr: u64, + // Floating-point registers (F0-F31) + pub fpr: [f64; 32], + // VMX128 vector registers (V0-V127, Xbox 360 extended set) + pub vr: [Vec128; 128], + + // Condition register fields (CR0-CR7) + pub cr: [CrField; 8], + // Floating-point status and control register + pub fpscr: u32, + // XER register (split for easy individual updates) + pub xer_ca: u8, + pub xer_ov: u8, + pub xer_so: u8, + // Altivec VSCR saturation bit + pub vscr_sat: u8, + + // Program counter + pub pc: u32, + // Reservation address/value for lwarx/stwcx + pub reserved_addr: u32, + pub reserved_val: u64, + pub has_reservation: bool, + + // Thread ID (for kernel use) + pub thread_id: u32, + + // Cycle counter for timing + pub cycle_count: u64, + + // Time base (incremented each instruction for debugging) + pub timebase: u64, +} + +impl PpcContext { + pub fn new() -> Self { + Self { + gpr: [0; 32], + ctr: 0, + lr: 0, + msr: 0, + fpr: [0.0; 32], + vr: [Vec128::ZERO; 128], + cr: [CrField::default(); 8], + fpscr: 0, + xer_ca: 0, + xer_ov: 0, + xer_so: 0, + vscr_sat: 0, + pc: 0, + reserved_addr: 0, + reserved_val: 0, + has_reservation: false, + thread_id: 0, + cycle_count: 0, + timebase: 0, + } + } + + /// Get the full 32-bit condition register. + pub fn cr(&self) -> u32 { + let mut val = 0u32; + for (i, field) in self.cr.iter().enumerate() { + val |= (field.as_u8() as u32) << (28 - i * 4); + } + val + } + + /// Set the full 32-bit condition register. + pub fn set_cr(&mut self, val: u32) { + for i in 0..8 { + self.cr[i] = CrField::from_u8(((val >> (28 - i * 4)) & 0xF) as u8); + } + } + + /// Get a single CR bit by absolute bit number (0-31). + pub fn get_cr_bit(&self, bit: u32) -> bool { + let field = (bit / 4) as usize; + let sub = bit % 4; + match sub { + 0 => self.cr[field].lt, + 1 => self.cr[field].gt, + 2 => self.cr[field].eq, + 3 => self.cr[field].so, + _ => unreachable!(), + } + } + + /// Set a single CR bit by absolute bit number (0-31). + pub fn set_cr_bit(&mut self, bit: u32, val: bool) { + let field = (bit / 4) as usize; + let sub = bit % 4; + match sub { + 0 => self.cr[field].lt = val, + 1 => self.cr[field].gt = val, + 2 => self.cr[field].eq = val, + 3 => self.cr[field].so = val, + _ => unreachable!(), + } + } + + /// Update a condition register field based on a comparison result (signed). + pub fn update_cr_signed(&mut self, field: usize, val: i64) { + self.cr[field] = CrField { + lt: val < 0, + gt: val > 0, + eq: val == 0, + so: self.xer_so != 0, + }; + } + + /// Update a condition register field based on a comparison result (unsigned). + pub fn update_cr_unsigned(&mut self, field: usize, a: u64, b: u64) { + self.cr[field] = CrField { + lt: a < b, + gt: a > b, + eq: a == b, + so: self.xer_so != 0, + }; + } + + /// Get the full XER register value. + pub fn xer(&self) -> u32 { + ((self.xer_so as u32) << 31) | ((self.xer_ov as u32) << 30) | ((self.xer_ca as u32) << 29) + } + + /// Set XER from a full 32-bit value. + pub fn set_xer(&mut self, val: u32) { + self.xer_so = ((val >> 31) & 1) as u8; + self.xer_ov = ((val >> 30) & 1) as u8; + self.xer_ca = ((val >> 29) & 1) as u8; + } +} + +impl Default for PpcContext { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/xenia-cpu/src/decoder.rs b/crates/xenia-cpu/src/decoder.rs new file mode 100644 index 0000000..c84ddca --- /dev/null +++ b/crates/xenia-cpu/src/decoder.rs @@ -0,0 +1,819 @@ +use crate::opcode::PpcOpcode; + +/// Extract bits [a..=b] from a 32-bit value (PPC bit numbering: 0 = MSB). +#[inline(always)] +const fn extract_bits(v: u32, a: u32, b: u32) -> u32 { + (v >> (32 - 1 - b)) & ((1 << (b - a + 1)) - 1) +} + +/// Decoded PPC instruction with extracted operand fields. +#[derive(Debug, Clone, Copy)] +pub struct DecodedInstr { + pub opcode: PpcOpcode, + pub raw: u32, + pub addr: u32, +} + +impl DecodedInstr { + // Common field extractors (PPC bit numbering) + + /// Primary opcode (bits 0-5) + #[inline] pub fn op(&self) -> u32 { extract_bits(self.raw, 0, 5) } + + /// rD/rS/rT (bits 6-10) - destination/source register + #[inline] pub fn rd(&self) -> usize { extract_bits(self.raw, 6, 10) as usize } + #[inline] pub fn rs(&self) -> usize { self.rd() } + #[inline] pub fn rt(&self) -> usize { self.rd() } + + /// rA (bits 11-15) + #[inline] pub fn ra(&self) -> usize { extract_bits(self.raw, 11, 15) as usize } + + /// rB (bits 16-20) + #[inline] pub fn rb(&self) -> usize { extract_bits(self.raw, 16, 20) as usize } + + /// rC (bits 21-25) - for 4-operand instructions + #[inline] pub fn rc(&self) -> usize { extract_bits(self.raw, 21, 25) as usize } + + /// SIMM/UIMM (bits 16-31) - signed/unsigned immediate + #[inline] pub fn simm16(&self) -> i16 { (self.raw & 0xFFFF) as i16 } + #[inline] pub fn uimm16(&self) -> u16 { (self.raw & 0xFFFF) as u16 } + + /// D-form displacement (signed, bits 16-31) + #[inline] pub fn d(&self) -> i32 { self.simm16() as i32 } + + /// DS-form displacement (signed, bits 16-29, shifted left 2) + #[inline] pub fn ds(&self) -> i32 { (self.raw & 0xFFFC) as i16 as i32 } + + /// LI field for branch (bits 6-29, sign-extended, shifted left 2) + #[inline] pub fn li(&self) -> i32 { + let li = extract_bits(self.raw, 6, 29); + // Sign-extend from 24 bits, then shift left 2 + let sign_extended = ((li as i32) << 8) >> 8; + sign_extended << 2 + } + + /// BD field for conditional branch (bits 16-29, sign-extended, shifted left 2) + #[inline] pub fn bd(&self) -> i32 { + let bd = extract_bits(self.raw, 16, 29); + let sign_extended = ((bd as i32) << 18) >> 18; + sign_extended << 2 + } + + /// BO field (bits 6-10) - branch options + #[inline] pub fn bo(&self) -> u32 { extract_bits(self.raw, 6, 10) } + + /// BI field (bits 11-15) - branch condition + #[inline] pub fn bi(&self) -> u32 { extract_bits(self.raw, 11, 15) } + + /// AA bit (bit 30) - absolute address + #[inline] pub fn aa(&self) -> bool { (self.raw >> 1) & 1 != 0 } + + /// LK bit (bit 31) - link (update LR) + #[inline] pub fn lk(&self) -> bool { self.raw & 1 != 0 } + + /// Rc bit (bit 31) - record CR0 + #[inline] pub fn rc_bit(&self) -> bool { self.raw & 1 != 0 } + + /// OE bit (bit 21) - overflow enable + #[inline] pub fn oe(&self) -> bool { extract_bits(self.raw, 21, 21) != 0 } + + /// MB, ME fields for rotate instructions + #[inline] pub fn mb(&self) -> u32 { extract_bits(self.raw, 21, 25) } + #[inline] pub fn me(&self) -> u32 { extract_bits(self.raw, 26, 30) } + + /// SH field (bits 16-20) for shift instructions + #[inline] pub fn sh(&self) -> u32 { extract_bits(self.raw, 16, 20) } + + /// SH field for 64-bit shifts (bits 16-20 + bit 30) + #[inline] pub fn sh64(&self) -> u32 { + (extract_bits(self.raw, 16, 20) << 1) | extract_bits(self.raw, 30, 30) + } + + /// SPR field (bits 11-20, swapped halves) + #[inline] pub fn spr(&self) -> u32 { + let spr_raw = extract_bits(self.raw, 11, 20); + ((spr_raw & 0x1F) << 5) | ((spr_raw >> 5) & 0x1F) + } + + /// CRM field (bits 12-19) for mtcrf + #[inline] pub fn crm(&self) -> u32 { extract_bits(self.raw, 12, 19) } + + /// crfD (bits 6-8) - condition register field destination + #[inline] pub fn crfd(&self) -> usize { extract_bits(self.raw, 6, 8) as usize } + + /// crfS (bits 11-13) + #[inline] pub fn crfs(&self) -> usize { extract_bits(self.raw, 11, 13) as usize } + + /// L bit (bit 10) - 64-bit compare + #[inline] pub fn l(&self) -> bool { extract_bits(self.raw, 10, 10) != 0 } + + /// crbD (bits 6-10) + #[inline] pub fn crbd(&self) -> u32 { extract_bits(self.raw, 6, 10) } + /// crbA (bits 11-15) + #[inline] pub fn crba(&self) -> u32 { extract_bits(self.raw, 11, 15) } + /// crbB (bits 16-20) + #[inline] pub fn crbb(&self) -> u32 { extract_bits(self.raw, 16, 20) } + + // VMX128 field extractors + + /// VA128 (bits 6-10, plus bit from 29) + #[inline] pub fn va128(&self) -> usize { + (extract_bits(self.raw, 6, 10) | (extract_bits(self.raw, 29, 29) << 5)) as usize + } + + /// VB128 (bits 16-20, plus bits from 28, 30) + #[inline] pub fn vb128(&self) -> usize { + (extract_bits(self.raw, 16, 20) + | (extract_bits(self.raw, 28, 28) << 5) + | (extract_bits(self.raw, 30, 30) << 6)) as usize + } + + /// VD128 (bits 6-10, plus bits from 21, 22) + #[inline] pub fn vd128(&self) -> usize { + (extract_bits(self.raw, 6, 10) + | (extract_bits(self.raw, 21, 21) << 5) + | (extract_bits(self.raw, 22, 22) << 6)) as usize + } + + /// VS128 - same encoding as VD128 + #[inline] pub fn vs128(&self) -> usize { self.vd128() } + + /// NB field (bits 16-20) for lswi/stswi + #[inline] pub fn nb(&self) -> u32 { extract_bits(self.raw, 16, 20) } +} + +/// Decode a 32-bit PPC instruction into its opcode. +/// Direct translation of the C++ LookupOpcode from ppc_opcode_lookup_gen.cc. +pub fn decode(raw: u32, addr: u32) -> DecodedInstr { + let opcode = lookup_opcode(raw); + DecodedInstr { opcode, raw, addr } +} + +fn lookup_opcode(code: u32) -> PpcOpcode { + match extract_bits(code, 0, 5) { + 2 => PpcOpcode::tdi, + 3 => PpcOpcode::twi, + 4 => decode_op4(code), + 5 => decode_op5(code), + 6 => decode_op6(code), + 7 => PpcOpcode::mulli, + 8 => PpcOpcode::subficx, + 10 => PpcOpcode::cmpli, + 11 => PpcOpcode::cmpi, + 12 => PpcOpcode::addic, + 13 => PpcOpcode::addicx, + 14 => PpcOpcode::addi, + 15 => PpcOpcode::addis, + 16 => PpcOpcode::bcx, + 17 => PpcOpcode::sc, + 18 => PpcOpcode::bx, + 19 => decode_op19(code), + 20 => PpcOpcode::rlwimix, + 21 => PpcOpcode::rlwinmx, + 23 => PpcOpcode::rlwnmx, + 24 => PpcOpcode::ori, + 25 => PpcOpcode::oris, + 26 => PpcOpcode::xori, + 27 => PpcOpcode::xoris, + 28 => PpcOpcode::andix, + 29 => PpcOpcode::andisx, + 30 => decode_op30(code), + 31 => decode_op31(code), + 32 => PpcOpcode::lwz, + 33 => PpcOpcode::lwzu, + 34 => PpcOpcode::lbz, + 35 => PpcOpcode::lbzu, + 36 => PpcOpcode::stw, + 37 => PpcOpcode::stwu, + 38 => PpcOpcode::stb, + 39 => PpcOpcode::stbu, + 40 => PpcOpcode::lhz, + 41 => PpcOpcode::lhzu, + 42 => PpcOpcode::lha, + 43 => PpcOpcode::lhau, + 44 => PpcOpcode::sth, + 45 => PpcOpcode::sthu, + 46 => PpcOpcode::lmw, + 47 => PpcOpcode::stmw, + 48 => PpcOpcode::lfs, + 49 => PpcOpcode::lfsu, + 50 => PpcOpcode::lfd, + 51 => PpcOpcode::lfdu, + 52 => PpcOpcode::stfs, + 53 => PpcOpcode::stfsu, + 54 => PpcOpcode::stfd, + 55 => PpcOpcode::stfdu, + 58 => match extract_bits(code, 30, 31) { + 0b00 => PpcOpcode::ld, + 0b01 => PpcOpcode::ldu, + 0b10 => PpcOpcode::lwa, + _ => PpcOpcode::Invalid, + }, + 59 => match extract_bits(code, 26, 30) { + 0b10010 => PpcOpcode::fdivsx, + 0b10100 => PpcOpcode::fsubsx, + 0b10101 => PpcOpcode::faddsx, + 0b10110 => PpcOpcode::fsqrtsx, + 0b11000 => PpcOpcode::fresx, + 0b11001 => PpcOpcode::fmulsx, + 0b11100 => PpcOpcode::fmsubsx, + 0b11101 => PpcOpcode::fmaddsx, + 0b11110 => PpcOpcode::fnmsubsx, + 0b11111 => PpcOpcode::fnmaddsx, + _ => PpcOpcode::Invalid, + }, + 62 => match extract_bits(code, 30, 31) { + 0b00 => PpcOpcode::std, + 0b01 => PpcOpcode::stdu, + _ => PpcOpcode::Invalid, + }, + 63 => decode_op63(code), + _ => PpcOpcode::Invalid, + } +} + +fn decode_op4(code: u32) -> PpcOpcode { + // VMX128 load/store (op=4, bits 21-27 << 4 | bits 30-31) + let key1 = (extract_bits(code, 21, 27) << 4) | extract_bits(code, 30, 31); + match key1 { + 0b00000000011 => return PpcOpcode::lvsl128, + 0b00001000011 => return PpcOpcode::lvsr128, + 0b00010000011 => return PpcOpcode::lvewx128, + 0b00011000011 => return PpcOpcode::lvx128, + 0b00110000011 => return PpcOpcode::stvewx128, + 0b00111000011 => return PpcOpcode::stvx128, + 0b01011000011 => return PpcOpcode::lvxl128, + 0b01111000011 => return PpcOpcode::stvxl128, + 0b10000000011 => return PpcOpcode::lvlx128, + 0b10001000011 => return PpcOpcode::lvrx128, + 0b10100000011 => return PpcOpcode::stvlx128, + 0b10101000011 => return PpcOpcode::stvrx128, + 0b11000000011 => return PpcOpcode::lvlxl128, + 0b11001000011 => return PpcOpcode::lvrxl128, + 0b11100000011 => return PpcOpcode::stvlxl128, + 0b11101000011 => return PpcOpcode::stvrxl128, + _ => {} + } + + // Standard VMX (op=4, bits 21-31) + let key2 = extract_bits(code, 21, 31); + match key2 { + 0b00000000000 => return PpcOpcode::vaddubm, + 0b00000000010 => return PpcOpcode::vmaxub, + 0b00000000100 => return PpcOpcode::vrlb, + 0b00000001000 => return PpcOpcode::vmuloub, + 0b00000001010 => return PpcOpcode::vaddfp, + 0b00000001100 => return PpcOpcode::vmrghb, + 0b00000001110 => return PpcOpcode::vpkuhum, + 0b00001000000 => return PpcOpcode::vadduhm, + 0b00001000010 => return PpcOpcode::vmaxuh, + 0b00001000100 => return PpcOpcode::vrlh, + 0b00001001000 => return PpcOpcode::vmulouh, + 0b00001001010 => return PpcOpcode::vsubfp, + 0b00001001100 => return PpcOpcode::vmrghh, + 0b00001001110 => return PpcOpcode::vpkuwum, + 0b00010000000 => return PpcOpcode::vadduwm, + 0b00010000010 => return PpcOpcode::vmaxuw, + 0b00010000100 => return PpcOpcode::vrlw, + 0b00010001100 => return PpcOpcode::vmrghw, + 0b00010001110 => return PpcOpcode::vpkuhus, + 0b00011001110 => return PpcOpcode::vpkuwus, + 0b00100000010 => return PpcOpcode::vmaxsb, + 0b00100000100 => return PpcOpcode::vslb, + 0b00100001000 => return PpcOpcode::vmulosb, + 0b00100001010 => return PpcOpcode::vrefp, + 0b00100001100 => return PpcOpcode::vmrglb, + 0b00100001110 => return PpcOpcode::vpkshus, + 0b00101000010 => return PpcOpcode::vmaxsh, + 0b00101000100 => return PpcOpcode::vslh, + 0b00101001000 => return PpcOpcode::vmulosh, + 0b00101001010 => return PpcOpcode::vrsqrtefp, + 0b00101001100 => return PpcOpcode::vmrglh, + 0b00101001110 => return PpcOpcode::vpkswus, + 0b00110000000 => return PpcOpcode::vaddcuw, + 0b00110000010 => return PpcOpcode::vmaxsw, + 0b00110000100 => return PpcOpcode::vslw, + 0b00110001010 => return PpcOpcode::vexptefp, + 0b00110001100 => return PpcOpcode::vmrglw, + 0b00110001110 => return PpcOpcode::vpkshss, + 0b00111000100 => return PpcOpcode::vsl, + 0b00111001010 => return PpcOpcode::vlogefp, + 0b00111001110 => return PpcOpcode::vpkswss, + 0b01000000000 => return PpcOpcode::vaddubs, + 0b01000000010 => return PpcOpcode::vminub, + 0b01000000100 => return PpcOpcode::vsrb, + 0b01000001000 => return PpcOpcode::vmuleub, + 0b01000001010 => return PpcOpcode::vrfin, + 0b01000001100 => return PpcOpcode::vspltb, + 0b01000001110 => return PpcOpcode::vupkhsb, + 0b01001000000 => return PpcOpcode::vadduhs, + 0b01001000010 => return PpcOpcode::vminuh, + 0b01001000100 => return PpcOpcode::vsrh, + 0b01001001000 => return PpcOpcode::vmuleuh, + 0b01001001010 => return PpcOpcode::vrfiz, + 0b01001001100 => return PpcOpcode::vsplth, + 0b01001001110 => return PpcOpcode::vupkhsh, + 0b01010000000 => return PpcOpcode::vadduws, + 0b01010000010 => return PpcOpcode::vminuw, + 0b01010000100 => return PpcOpcode::vsrw, + 0b01010001010 => return PpcOpcode::vrfip, + 0b01010001100 => return PpcOpcode::vspltw, + 0b01010001110 => return PpcOpcode::vupklsb, + 0b01011000100 => return PpcOpcode::vsr, + 0b01011001010 => return PpcOpcode::vrfim, + 0b01011001110 => return PpcOpcode::vupklsh, + 0b01100000000 => return PpcOpcode::vaddsbs, + 0b01100000010 => return PpcOpcode::vminsb, + 0b01100000100 => return PpcOpcode::vsrab, + 0b01100001000 => return PpcOpcode::vmulesb, + 0b01100001010 => return PpcOpcode::vcfux, + 0b01100001100 => return PpcOpcode::vspltisb, + 0b01100001110 => return PpcOpcode::vpkpx, + 0b01101000000 => return PpcOpcode::vaddshs, + 0b01101000010 => return PpcOpcode::vminsh, + 0b01101000100 => return PpcOpcode::vsrah, + 0b01101001000 => return PpcOpcode::vmulesh, + 0b01101001010 => return PpcOpcode::vcfsx, + 0b01101001100 => return PpcOpcode::vspltish, + 0b01101001110 => return PpcOpcode::vupkhpx, + 0b01110000000 => return PpcOpcode::vaddsws, + 0b01110000010 => return PpcOpcode::vminsw, + 0b01110000100 => return PpcOpcode::vsraw, + 0b01110001010 => return PpcOpcode::vctuxs, + 0b01110001100 => return PpcOpcode::vspltisw, + 0b01111001010 => return PpcOpcode::vctsxs, + 0b01111001110 => return PpcOpcode::vupklpx, + 0b10000000000 => return PpcOpcode::vsububm, + 0b10000000010 => return PpcOpcode::vavgub, + 0b10000000100 => return PpcOpcode::vand, + 0b10000001010 => return PpcOpcode::vmaxfp, + 0b10000001100 => return PpcOpcode::vslo, + 0b10001000000 => return PpcOpcode::vsubuhm, + 0b10001000010 => return PpcOpcode::vavguh, + 0b10001000100 => return PpcOpcode::vandc, + 0b10001001010 => return PpcOpcode::vminfp, + 0b10001001100 => return PpcOpcode::vsro, + 0b10010000000 => return PpcOpcode::vsubuwm, + 0b10010000010 => return PpcOpcode::vavguw, + 0b10010000100 => return PpcOpcode::vor, + 0b10011000100 => return PpcOpcode::vxor, + 0b10100000010 => return PpcOpcode::vavgsb, + 0b10100000100 => return PpcOpcode::vnor, + 0b10101000010 => return PpcOpcode::vavgsh, + 0b10110000000 => return PpcOpcode::vsubcuw, + 0b10110000010 => return PpcOpcode::vavgsw, + 0b11000000000 => return PpcOpcode::vsububs, + 0b11000000100 => return PpcOpcode::mfvscr, + 0b11000001000 => return PpcOpcode::vsum4ubs, + 0b11001000000 => return PpcOpcode::vsubuhs, + 0b11001000100 => return PpcOpcode::mtvscr, + 0b11001001000 => return PpcOpcode::vsum4shs, + 0b11010000000 => return PpcOpcode::vsubuws, + 0b11010001000 => return PpcOpcode::vsum2sws, + 0b11100000000 => return PpcOpcode::vsubsbs, + 0b11100001000 => return PpcOpcode::vsum4sbs, + 0b11101000000 => return PpcOpcode::vsubshs, + 0b11110000000 => return PpcOpcode::vsubsws, + 0b11110001000 => return PpcOpcode::vsumsws, + _ => {} + } + + // VMX compare (op=4, bits 22-31) + let key3 = extract_bits(code, 22, 31); + match key3 { + 0b0000000110 => return PpcOpcode::vcmpequb, + 0b0001000110 => return PpcOpcode::vcmpequh, + 0b0010000110 => return PpcOpcode::vcmpequw, + 0b0011000110 => return PpcOpcode::vcmpeqfp, + 0b0111000110 => return PpcOpcode::vcmpgefp, + 0b1000000110 => return PpcOpcode::vcmpgtub, + 0b1001000110 => return PpcOpcode::vcmpgtuh, + 0b1010000110 => return PpcOpcode::vcmpgtuw, + 0b1011000110 => return PpcOpcode::vcmpgtfp, + 0b1100000110 => return PpcOpcode::vcmpgtsb, + 0b1101000110 => return PpcOpcode::vcmpgtsh, + 0b1110000110 => return PpcOpcode::vcmpgtsw, + 0b1111000110 => return PpcOpcode::vcmpbfp, + _ => {} + } + + // VMX 4-operand (op=4, bits 26-31) + let key4 = extract_bits(code, 26, 31); + match key4 { + 0b100000 => return PpcOpcode::vmhaddshs, + 0b100001 => return PpcOpcode::vmhraddshs, + 0b100010 => return PpcOpcode::vmladduhm, + 0b100100 => return PpcOpcode::vmsumubm, + 0b100101 => return PpcOpcode::vmsummbm, + 0b100110 => return PpcOpcode::vmsumuhm, + 0b100111 => return PpcOpcode::vmsumuhs, + 0b101000 => return PpcOpcode::vmsumshm, + 0b101001 => return PpcOpcode::vmsumshs, + 0b101010 => return PpcOpcode::vsel, + 0b101011 => return PpcOpcode::vperm, + 0b101100 => return PpcOpcode::vsldoi, + 0b101110 => return PpcOpcode::vmaddfp, + 0b101111 => return PpcOpcode::vnmsubfp, + _ => {} + } + + // vsldoi128 (op=4, bit 27) + if extract_bits(code, 27, 27) == 1 { + return PpcOpcode::vsldoi128; + } + + PpcOpcode::Invalid +} + +fn decode_op5(code: u32) -> PpcOpcode { + // vperm128 (op=5, bits 22,27) + let key1 = (extract_bits(code, 22, 22) << 5) | extract_bits(code, 27, 27); + if key1 == 0b000000 { + return PpcOpcode::vperm128; + } + + let key2 = (extract_bits(code, 22, 25) << 2) | extract_bits(code, 27, 27); + match key2 { + 0b000001 => PpcOpcode::vaddfp128, + 0b000101 => PpcOpcode::vsubfp128, + 0b001001 => PpcOpcode::vmulfp128, + 0b001101 => PpcOpcode::vmaddfp128, + 0b010001 => PpcOpcode::vmaddcfp128, + 0b010101 => PpcOpcode::vnmsubfp128, + 0b011001 => PpcOpcode::vmsum3fp128, + 0b011101 => PpcOpcode::vmsum4fp128, + 0b100000 => PpcOpcode::vpkshss128, + 0b100001 => PpcOpcode::vand128, + 0b100100 => PpcOpcode::vpkshus128, + 0b100101 => PpcOpcode::vandc128, + 0b101000 => PpcOpcode::vpkswss128, + 0b101001 => PpcOpcode::vnor128, + 0b101100 => PpcOpcode::vpkswus128, + 0b101101 => PpcOpcode::vor128, + 0b110000 => PpcOpcode::vpkuhum128, + 0b110001 => PpcOpcode::vxor128, + 0b110100 => PpcOpcode::vpkuhus128, + 0b110101 => PpcOpcode::vsel128, + 0b111000 => PpcOpcode::vpkuwum128, + 0b111001 => PpcOpcode::vslo128, + 0b111100 => PpcOpcode::vpkuwus128, + 0b111101 => PpcOpcode::vsro128, + _ => PpcOpcode::Invalid, + } +} + +fn decode_op6(code: u32) -> PpcOpcode { + // vpermwi128 + let key1 = (extract_bits(code, 21, 22) << 5) | extract_bits(code, 26, 27); + if key1 == 0b0100001 { + return PpcOpcode::vpermwi128; + } + + // vpkd3d128, vrlimi128 + let key2 = (extract_bits(code, 21, 23) << 4) | extract_bits(code, 26, 27); + match key2 { + 0b1100001 => return PpcOpcode::vpkd3d128, + 0b1110001 => return PpcOpcode::vrlimi128, + _ => {} + } + + // Unary VMX128 ops + let key3 = extract_bits(code, 21, 27); + match key3 { + 0b0100011 => return PpcOpcode::vcfpsxws128, + 0b0100111 => return PpcOpcode::vcfpuxws128, + 0b0101011 => return PpcOpcode::vcsxwfp128, + 0b0101111 => return PpcOpcode::vcuxwfp128, + 0b0110011 => return PpcOpcode::vrfim128, + 0b0110111 => return PpcOpcode::vrfin128, + 0b0111011 => return PpcOpcode::vrfip128, + 0b0111111 => return PpcOpcode::vrfiz128, + 0b1100011 => return PpcOpcode::vrefp128, + 0b1100111 => return PpcOpcode::vrsqrtefp128, + 0b1101011 => return PpcOpcode::vexptefp128, + 0b1101111 => return PpcOpcode::vlogefp128, + 0b1110011 => return PpcOpcode::vspltw128, + 0b1110111 => return PpcOpcode::vspltisw128, + 0b1111111 => return PpcOpcode::vupkd3d128, + _ => {} + } + + // VMX128 compare + let key4 = (extract_bits(code, 22, 24) << 3) | extract_bits(code, 27, 27); + match key4 { + 0b000000 => return PpcOpcode::vcmpeqfp128, + 0b001000 => return PpcOpcode::vcmpgefp128, + 0b010000 => return PpcOpcode::vcmpgtfp128, + 0b011000 => return PpcOpcode::vcmpbfp128, + 0b100000 => return PpcOpcode::vcmpequw128, + _ => {} + } + + // VMX128 shift/merge + let key5 = (extract_bits(code, 22, 25) << 2) | extract_bits(code, 27, 27); + match key5 { + 0b000101 => return PpcOpcode::vrlw128, + 0b001101 => return PpcOpcode::vslw128, + 0b010101 => return PpcOpcode::vsraw128, + 0b011101 => return PpcOpcode::vsrw128, + 0b101000 => return PpcOpcode::vmaxfp128, + 0b101100 => return PpcOpcode::vminfp128, + 0b110000 => return PpcOpcode::vmrghw128, + 0b110100 => return PpcOpcode::vmrglw128, + 0b111000 => return PpcOpcode::vupkhsb128, + 0b111100 => return PpcOpcode::vupklsb128, + _ => {} + } + + PpcOpcode::Invalid +} + +fn decode_op19(code: u32) -> PpcOpcode { + match extract_bits(code, 21, 30) { + 0b0000000000 => PpcOpcode::mcrf, + 0b0000010000 => PpcOpcode::bclrx, + 0b0000100001 => PpcOpcode::crnor, + 0b0010000001 => PpcOpcode::crandc, + 0b0010010110 => PpcOpcode::isync, + 0b0011000001 => PpcOpcode::crxor, + 0b0011100001 => PpcOpcode::crnand, + 0b0100000001 => PpcOpcode::crand, + 0b0100100001 => PpcOpcode::creqv, + 0b0110100001 => PpcOpcode::crorc, + 0b0111000001 => PpcOpcode::cror, + 0b1000010000 => PpcOpcode::bcctrx, + _ => PpcOpcode::Invalid, + } +} + +fn decode_op30(code: u32) -> PpcOpcode { + match extract_bits(code, 27, 29) { + 0b000 => PpcOpcode::rldiclx, + 0b001 => PpcOpcode::rldicrx, + 0b010 => PpcOpcode::rldicx, + 0b011 => PpcOpcode::rldimix, + _ => match extract_bits(code, 27, 30) { + 0b1000 => PpcOpcode::rldclx, + 0b1001 => PpcOpcode::rldcrx, + _ => PpcOpcode::Invalid, + }, + } +} + +fn decode_op31(code: u32) -> PpcOpcode { + // sradix has a unique 10-bit key (bits 21-29) + if extract_bits(code, 21, 29) == 0b110011101 { + return PpcOpcode::sradix; + } + + // Main op31 table (bits 21-30) + let key = extract_bits(code, 21, 30); + match key { + 0b0000000000 => return PpcOpcode::cmp, + 0b0000000100 => return PpcOpcode::tw, + 0b0000000110 => return PpcOpcode::lvsl, + 0b0000000111 => return PpcOpcode::lvebx, + 0b0000010011 => return PpcOpcode::mfcr, + 0b0000010100 => return PpcOpcode::lwarx, + 0b0000010101 => return PpcOpcode::ldx, + 0b0000010111 => return PpcOpcode::lwzx, + 0b0000011000 => return PpcOpcode::slwx, + 0b0000011010 => return PpcOpcode::cntlzwx, + 0b0000011011 => return PpcOpcode::sldx, + 0b0000011100 => return PpcOpcode::andx, + 0b0000100000 => return PpcOpcode::cmpl, + 0b0000100110 => return PpcOpcode::lvsr, + 0b0000100111 => return PpcOpcode::lvehx, + 0b0000110101 => return PpcOpcode::ldux, + 0b0000110110 => return PpcOpcode::dcbst, + 0b0000110111 => return PpcOpcode::lwzux, + 0b0000111010 => return PpcOpcode::cntlzdx, + 0b0000111100 => return PpcOpcode::andcx, + 0b0001000100 => return PpcOpcode::td, + 0b0001000111 => return PpcOpcode::lvewx, + 0b0001010011 => return PpcOpcode::mfmsr, + 0b0001010100 => return PpcOpcode::ldarx, + 0b0001010110 => return PpcOpcode::dcbf, + 0b0001010111 => return PpcOpcode::lbzx, + 0b0001100111 => return PpcOpcode::lvx, + 0b0001110111 => return PpcOpcode::lbzux, + 0b0001111100 => return PpcOpcode::norx, + 0b0010000111 => return PpcOpcode::stvebx, + 0b0010010000 => return PpcOpcode::mtcrf, + 0b0010010010 => return PpcOpcode::mtmsr, + 0b0010010101 => return PpcOpcode::stdx, + 0b0010010110 => return PpcOpcode::stwcx, + 0b0010010111 => return PpcOpcode::stwx, + 0b0010100111 => return PpcOpcode::stvehx, + 0b0010110010 => return PpcOpcode::mtmsrd, + 0b0010110101 => return PpcOpcode::stdux, + 0b0010110111 => return PpcOpcode::stwux, + 0b0011000111 => return PpcOpcode::stvewx, + 0b0011010110 => return PpcOpcode::stdcx, + 0b0011010111 => return PpcOpcode::stbx, + 0b0011100111 => return PpcOpcode::stvx, + 0b0011110110 => return PpcOpcode::dcbtst, + 0b0011110111 => return PpcOpcode::stbux, + 0b0100010110 => return PpcOpcode::dcbt, + 0b0100010111 => return PpcOpcode::lhzx, + 0b0100011100 => return PpcOpcode::eqvx, + 0b0100110111 => return PpcOpcode::lhzux, + 0b0100111100 => return PpcOpcode::xorx, + 0b0101010011 => return PpcOpcode::mfspr, + 0b0101010101 => return PpcOpcode::lwax, + 0b0101010111 => return PpcOpcode::lhax, + 0b0101100111 => return PpcOpcode::lvxl, + 0b0101110011 => return PpcOpcode::mftb, + 0b0101110101 => return PpcOpcode::lwaux, + 0b0101110111 => return PpcOpcode::lhaux, + 0b0110010111 => return PpcOpcode::sthx, + 0b0110011100 => return PpcOpcode::orcx, + 0b0110110111 => return PpcOpcode::sthux, + 0b0110111100 => return PpcOpcode::orx, + 0b0111010011 => return PpcOpcode::mtspr, + 0b0111010110 => return PpcOpcode::dcbi, + 0b0111011100 => return PpcOpcode::nandx, + 0b0111100111 => return PpcOpcode::stvxl, + 0b1000000000 => return PpcOpcode::mcrxr, + 0b1000000111 => return PpcOpcode::lvlx, + 0b1000010100 => return PpcOpcode::ldbrx, + 0b1000010101 => return PpcOpcode::lswx, + 0b1000010110 => return PpcOpcode::lwbrx, + 0b1000010111 => return PpcOpcode::lfsx, + 0b1000011000 => return PpcOpcode::srwx, + 0b1000011011 => return PpcOpcode::srdx, + 0b1000100111 => return PpcOpcode::lvrx, + 0b1000110111 => return PpcOpcode::lfsux, + 0b1001010101 => return PpcOpcode::lswi, + 0b1001010110 => return PpcOpcode::sync, + 0b1001010111 => return PpcOpcode::lfdx, + 0b1001110111 => return PpcOpcode::lfdux, + 0b1010000111 => return PpcOpcode::stvlx, + 0b1010010100 => return PpcOpcode::stdbrx, + 0b1010010101 => return PpcOpcode::stswx, + 0b1010010110 => return PpcOpcode::stwbrx, + 0b1010010111 => return PpcOpcode::stfsx, + 0b1010100111 => return PpcOpcode::stvrx, + 0b1010110111 => return PpcOpcode::stfsux, + 0b1011010101 => return PpcOpcode::stswi, + 0b1011010111 => return PpcOpcode::stfdx, + 0b1011110111 => return PpcOpcode::stfdux, + 0b1100000111 => return PpcOpcode::lvlxl, + 0b1100010110 => return PpcOpcode::lhbrx, + 0b1100011000 => return PpcOpcode::srawx, + 0b1100011010 => return PpcOpcode::sradx, + 0b1100100111 => return PpcOpcode::lvrxl, + 0b1100111000 => return PpcOpcode::srawix, + 0b1101010110 => return PpcOpcode::eieio, + 0b1110000111 => return PpcOpcode::stvlxl, + 0b1110010110 => return PpcOpcode::sthbrx, + 0b1110011010 => return PpcOpcode::extshx, + 0b1110100111 => return PpcOpcode::stvrxl, + 0b1110111010 => return PpcOpcode::extsbx, + 0b1111010110 => return PpcOpcode::icbi, + 0b1111010111 => return PpcOpcode::stfiwx, + 0b1111011010 => return PpcOpcode::extswx, + _ => {} + } + + // Arithmetic op31 (bits 22-30) + let key2 = extract_bits(code, 22, 30); + match key2 { + 0b000001000 => return PpcOpcode::subfcx, + 0b000001001 => return PpcOpcode::mulhdux, + 0b000001010 => return PpcOpcode::addcx, + 0b000001011 => return PpcOpcode::mulhwux, + 0b000101000 => return PpcOpcode::subfx, + 0b001001001 => return PpcOpcode::mulhdx, + 0b001001011 => return PpcOpcode::mulhwx, + 0b001101000 => return PpcOpcode::negx, + 0b010001000 => return PpcOpcode::subfex, + 0b010001010 => return PpcOpcode::addex, + 0b011001000 => return PpcOpcode::subfzex, + 0b011001010 => return PpcOpcode::addzex, + 0b011101000 => return PpcOpcode::subfmex, + 0b011101001 => return PpcOpcode::mulldx, + 0b011101010 => return PpcOpcode::addmex, + 0b011101011 => return PpcOpcode::mullwx, + 0b100001010 => return PpcOpcode::addx, + 0b111001001 => return PpcOpcode::divdux, + 0b111001011 => return PpcOpcode::divwux, + 0b111101001 => return PpcOpcode::divdx, + 0b111101011 => return PpcOpcode::divwx, + _ => {} + } + + // dcbz/dcbz128 special case + let key3 = (extract_bits(code, 6, 10) << 20) | (extract_bits(code, 21, 30)); + match key3 { + 0b0000000000000001111110110 => return PpcOpcode::dcbz, + 0b0000100000000001111110110 => return PpcOpcode::dcbz128, + _ => {} + } + + PpcOpcode::Invalid +} + +fn decode_op63(code: u32) -> PpcOpcode { + // Primary op63 table (bits 21-30) + match extract_bits(code, 21, 30) { + 0b0000000000 => return PpcOpcode::fcmpu, + 0b0000001100 => return PpcOpcode::frspx, + 0b0000001110 => return PpcOpcode::fctiwx, + 0b0000001111 => return PpcOpcode::fctiwzx, + 0b0000100000 => return PpcOpcode::fcmpo, + 0b0000100110 => return PpcOpcode::mtfsb1x, + 0b0000101000 => return PpcOpcode::fnegx, + 0b0001000000 => return PpcOpcode::mcrfs, + 0b0001000110 => return PpcOpcode::mtfsb0x, + 0b0001001000 => return PpcOpcode::fmrx, + 0b0010000110 => return PpcOpcode::mtfsfix, + 0b0010001000 => return PpcOpcode::fnabsx, + 0b0100001000 => return PpcOpcode::fabsx, + 0b1001000111 => return PpcOpcode::mffsx, + 0b1011000111 => return PpcOpcode::mtfsfx, + 0b1100101110 => return PpcOpcode::fctidx, + 0b1100101111 => return PpcOpcode::fctidzx, + 0b1101001110 => return PpcOpcode::fcfidx, + _ => {} + } + + // FPU arithmetic (bits 26-30) + match extract_bits(code, 26, 30) { + 0b10010 => PpcOpcode::fdivx, + 0b10100 => PpcOpcode::fsubx, + 0b10101 => PpcOpcode::faddx, + 0b10110 => PpcOpcode::fsqrtx, + 0b10111 => PpcOpcode::fselx, + 0b11001 => PpcOpcode::fmulx, + 0b11010 => PpcOpcode::frsqrtex, + 0b11100 => PpcOpcode::fmsubx, + 0b11101 => PpcOpcode::fmaddx, + 0b11110 => PpcOpcode::fnmsubx, + 0b11111 => PpcOpcode::fnmaddx, + _ => PpcOpcode::Invalid, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decode_addi() { + // addi r3, r1, 0x10 => opcode 14, rD=3, rA=1, SIMM=0x10 + let raw: u32 = (14 << 26) | (3 << 21) | (1 << 16) | 0x10; + let instr = decode(raw, 0); + assert_eq!(instr.opcode, PpcOpcode::addi); + assert_eq!(instr.rd(), 3); + assert_eq!(instr.ra(), 1); + assert_eq!(instr.simm16(), 0x10); + } + + #[test] + fn test_decode_lwz() { + // lwz r5, 0x20(r1) => opcode 32 + let raw: u32 = (32 << 26) | (5 << 21) | (1 << 16) | 0x20; + let instr = decode(raw, 0); + assert_eq!(instr.opcode, PpcOpcode::lwz); + assert_eq!(instr.rd(), 5); + assert_eq!(instr.ra(), 1); + assert_eq!(instr.d(), 0x20); + } + + #[test] + fn test_decode_branch() { + // b +0x100 => opcode 18, LI=0x40 (shifted left 2 = 0x100), AA=0, LK=0 + let raw: u32 = (18 << 26) | (0x40 << 2); + let instr = decode(raw, 0); + assert_eq!(instr.opcode, PpcOpcode::bx); + assert_eq!(instr.li(), 0x100); + assert!(!instr.aa()); + assert!(!instr.lk()); + } + + #[test] + fn test_decode_stw() { + // stw r7, 0x8(r2) + let raw: u32 = (36 << 26) | (7 << 21) | (2 << 16) | 0x8; + let instr = decode(raw, 0); + assert_eq!(instr.opcode, PpcOpcode::stw); + assert_eq!(instr.rs(), 7); + assert_eq!(instr.ra(), 2); + } + + #[test] + fn test_decode_ori_nop() { + // ori r0, r0, 0 = NOP + let raw: u32 = 24 << 26; + let instr = decode(raw, 0); + assert_eq!(instr.opcode, PpcOpcode::ori); + } + + #[test] + fn test_extract_bits() { + assert_eq!(extract_bits(0xFFFF_FFFF, 0, 5), 0x3F); + assert_eq!(extract_bits(0x8000_0000, 0, 0), 1); + assert_eq!(extract_bits(0x0000_0001, 31, 31), 1); + } +} diff --git a/crates/xenia-cpu/src/disasm.rs b/crates/xenia-cpu/src/disasm.rs new file mode 100644 index 0000000..e4ee2ca --- /dev/null +++ b/crates/xenia-cpu/src/disasm.rs @@ -0,0 +1,276 @@ +use crate::decoder::DecodedInstr; +use crate::opcode::PpcOpcode; +use std::fmt::Write; + +/// Disassemble a decoded instruction into PPC assembly text. +pub fn disassemble(instr: &DecodedInstr) -> String { + let mut out = String::new(); + match instr.opcode { + // Branch instructions + PpcOpcode::bx => { + let target = if instr.aa() { + instr.li() as u32 + } else { + instr.addr.wrapping_add(instr.li() as u32) + }; + let mnemonic = if instr.lk() { "bl" } else { "b" }; + write!(out, "{} 0x{:08X}", mnemonic, target).unwrap(); + } + PpcOpcode::bcx => { + let bo = instr.bo(); + let bi = instr.bi(); + let target = if instr.aa() { + instr.bd() as u32 + } else { + instr.addr.wrapping_add(instr.bd() as u32) + }; + let mnemonic = if instr.lk() { "bcl" } else { "bc" }; + write!(out, "{} {},{},0x{:08X}", mnemonic, bo, bi, target).unwrap(); + } + PpcOpcode::bclrx => { + let mnemonic = if instr.lk() { "bclrl" } else { "bclr" }; + write!(out, "{} {},{}", mnemonic, instr.bo(), instr.bi()).unwrap(); + } + PpcOpcode::bcctrx => { + let mnemonic = if instr.lk() { "bcctrl" } else { "bcctr" }; + write!(out, "{} {},{}", mnemonic, instr.bo(), instr.bi()).unwrap(); + } + + // System call + PpcOpcode::sc => { + write!(out, "sc").unwrap(); + } + + // D-form load/store + PpcOpcode::lwz | PpcOpcode::lwzu | PpcOpcode::lbz | PpcOpcode::lbzu | + PpcOpcode::lhz | PpcOpcode::lhzu | PpcOpcode::lha | PpcOpcode::lhau | + PpcOpcode::lfs | PpcOpcode::lfsu | PpcOpcode::lfd | PpcOpcode::lfdu => { + write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rd(), instr.d(), instr.ra()).unwrap(); + } + PpcOpcode::stw | PpcOpcode::stwu | PpcOpcode::stb | PpcOpcode::stbu | + PpcOpcode::sth | PpcOpcode::sthu | + PpcOpcode::stfs | PpcOpcode::stfsu | PpcOpcode::stfd | PpcOpcode::stfdu => { + write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rs(), instr.d(), instr.ra()).unwrap(); + } + + // D-form immediate ALU + PpcOpcode::addi | PpcOpcode::addis | PpcOpcode::addic | PpcOpcode::addicx | + PpcOpcode::subficx | PpcOpcode::mulli => { + write!(out, "{:?} r{},r{},{}", instr.opcode, instr.rd(), instr.ra(), instr.simm16()).unwrap(); + } + + // D-form immediate logical + PpcOpcode::ori | PpcOpcode::oris | PpcOpcode::xori | PpcOpcode::xoris | + PpcOpcode::andix | PpcOpcode::andisx => { + write!(out, "{:?} r{},r{},0x{:04X}", instr.opcode, instr.ra(), instr.rs(), instr.uimm16()).unwrap(); + } + + // Compare + PpcOpcode::cmpi => { + write!(out, "cmp{}i cr{},r{},{}", if instr.l() { "d" } else { "w" }, + instr.crfd(), instr.ra(), instr.simm16()).unwrap(); + } + PpcOpcode::cmpli => { + write!(out, "cmpl{}i cr{},r{},0x{:04X}", if instr.l() { "d" } else { "w" }, + instr.crfd(), instr.ra(), instr.uimm16()).unwrap(); + } + PpcOpcode::cmp => { + write!(out, "cmp{} cr{},r{},r{}", if instr.l() { "d" } else { "w" }, + instr.crfd(), instr.ra(), instr.rb()).unwrap(); + } + PpcOpcode::cmpl => { + write!(out, "cmpl{} cr{},r{},r{}", if instr.l() { "d" } else { "w" }, + instr.crfd(), instr.ra(), instr.rb()).unwrap(); + } + + // X-form ALU (3-register) + PpcOpcode::addx | PpcOpcode::addcx | PpcOpcode::addex | PpcOpcode::addzex | + PpcOpcode::addmex | PpcOpcode::subfx | PpcOpcode::subfcx | PpcOpcode::subfex | + PpcOpcode::subfzex | PpcOpcode::subfmex | PpcOpcode::negx | + PpcOpcode::mullwx | PpcOpcode::mulhwx | PpcOpcode::mulhwux | + PpcOpcode::divwx | PpcOpcode::divwux | + PpcOpcode::mulldx | PpcOpcode::mulhdx | PpcOpcode::mulhdux | + PpcOpcode::divdx | PpcOpcode::divdux => { + write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rd(), instr.ra(), instr.rb()).unwrap(); + } + + // X-form logical + PpcOpcode::andx | PpcOpcode::andcx | PpcOpcode::orx | PpcOpcode::orcx | + PpcOpcode::xorx | PpcOpcode::norx | PpcOpcode::nandx | PpcOpcode::eqvx => { + write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.ra(), instr.rs(), instr.rb()).unwrap(); + } + + // Shift/rotate + PpcOpcode::slwx | PpcOpcode::srwx | PpcOpcode::srawx | PpcOpcode::sldx | + PpcOpcode::srdx | PpcOpcode::sradx => { + write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.ra(), instr.rs(), instr.rb()).unwrap(); + } + PpcOpcode::srawix => { + write!(out, "srawi r{},r{},{}", instr.ra(), instr.rs(), instr.sh()).unwrap(); + } + PpcOpcode::sradix => { + write!(out, "sradi r{},r{},{}", instr.ra(), instr.rs(), instr.sh64()).unwrap(); + } + + // Rotate + PpcOpcode::rlwinmx => { + write!(out, "rlwinm r{},r{},{},{},{}", instr.ra(), instr.rs(), instr.sh(), instr.mb(), instr.me()).unwrap(); + } + PpcOpcode::rlwimix => { + write!(out, "rlwimi r{},r{},{},{},{}", instr.ra(), instr.rs(), instr.sh(), instr.mb(), instr.me()).unwrap(); + } + PpcOpcode::rlwnmx => { + write!(out, "rlwnm r{},r{},r{},{},{}", instr.ra(), instr.rs(), instr.rb(), instr.mb(), instr.me()).unwrap(); + } + + // Special register moves + PpcOpcode::mfspr => { + let spr_name = match instr.spr() { + 1 => "xer", + 8 => "lr", + 9 => "ctr", + 268 => "tbl", + 269 => "tbu", + _ => "", + }; + if spr_name.is_empty() { + write!(out, "mfspr r{},{}", instr.rd(), instr.spr()).unwrap(); + } else { + write!(out, "mf{} r{}", spr_name, instr.rd()).unwrap(); + } + } + PpcOpcode::mtspr => { + let spr_name = match instr.spr() { + 1 => "xer", + 8 => "lr", + 9 => "ctr", + _ => "", + }; + if spr_name.is_empty() { + write!(out, "mtspr {},r{}", instr.spr(), instr.rs()).unwrap(); + } else { + write!(out, "mt{} r{}", spr_name, instr.rs()).unwrap(); + } + } + PpcOpcode::mfcr => { + write!(out, "mfcr r{}", instr.rd()).unwrap(); + } + PpcOpcode::mtcrf => { + write!(out, "mtcrf 0x{:02X},r{}", instr.crm(), instr.rs()).unwrap(); + } + + // Extend + PpcOpcode::extsbx => write!(out, "extsb r{},r{}", instr.ra(), instr.rs()).unwrap(), + PpcOpcode::extshx => write!(out, "extsh r{},r{}", instr.ra(), instr.rs()).unwrap(), + PpcOpcode::extswx => write!(out, "extsw r{},r{}", instr.ra(), instr.rs()).unwrap(), + PpcOpcode::cntlzwx => write!(out, "cntlzw r{},r{}", instr.ra(), instr.rs()).unwrap(), + PpcOpcode::cntlzdx => write!(out, "cntlzd r{},r{}", instr.ra(), instr.rs()).unwrap(), + + // X-form load/store + PpcOpcode::lwzx | PpcOpcode::lwzux | PpcOpcode::lbzx | PpcOpcode::lbzux | + PpcOpcode::lhzx | PpcOpcode::lhzux | PpcOpcode::lhax | PpcOpcode::lhaux | + PpcOpcode::lwax | PpcOpcode::lwaux | PpcOpcode::ldx | PpcOpcode::ldux | + PpcOpcode::lfsx | PpcOpcode::lfsux | PpcOpcode::lfdx | PpcOpcode::lfdux | + PpcOpcode::lwbrx | PpcOpcode::lhbrx | PpcOpcode::ldbrx | + PpcOpcode::lwarx | PpcOpcode::ldarx => { + write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rd(), instr.ra(), instr.rb()).unwrap(); + } + PpcOpcode::stwx | PpcOpcode::stwux | PpcOpcode::stbx | PpcOpcode::stbux | + PpcOpcode::sthx | PpcOpcode::sthux | PpcOpcode::stdx | PpcOpcode::stdux | + PpcOpcode::stfsx | PpcOpcode::stfsux | PpcOpcode::stfdx | PpcOpcode::stfdux | + PpcOpcode::stwbrx | PpcOpcode::sthbrx | PpcOpcode::stdbrx | + PpcOpcode::stwcx | PpcOpcode::stdcx | PpcOpcode::stfiwx => { + write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rs(), instr.ra(), instr.rb()).unwrap(); + } + + // Cache/sync ops (no-ops for interpreter) + PpcOpcode::dcbf | PpcOpcode::dcbi | PpcOpcode::dcbst | + PpcOpcode::dcbt | PpcOpcode::dcbtst | PpcOpcode::icbi => { + write!(out, "{:?} r{},r{}", instr.opcode, instr.ra(), instr.rb()).unwrap(); + } + PpcOpcode::dcbz | PpcOpcode::dcbz128 => { + write!(out, "{:?} r{},r{}", instr.opcode, instr.ra(), instr.rb()).unwrap(); + } + PpcOpcode::sync | PpcOpcode::eieio | PpcOpcode::isync => { + write!(out, "{:?}", instr.opcode).unwrap(); + } + + // Load/store multiple + PpcOpcode::lmw => write!(out, "lmw r{},{}(r{})", instr.rd(), instr.d(), instr.ra()).unwrap(), + PpcOpcode::stmw => write!(out, "stmw r{},{}(r{})", instr.rs(), instr.d(), instr.ra()).unwrap(), + + // DS-form loads/stores + PpcOpcode::ld | PpcOpcode::ldu | PpcOpcode::lwa => { + write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rd(), instr.ds(), instr.ra()).unwrap(); + } + PpcOpcode::std | PpcOpcode::stdu => { + write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rs(), instr.ds(), instr.ra()).unwrap(); + } + + // CR logical ops + PpcOpcode::crand | PpcOpcode::crandc | PpcOpcode::creqv | PpcOpcode::crnand | + PpcOpcode::crnor | PpcOpcode::cror | PpcOpcode::crorc | PpcOpcode::crxor => { + write!(out, "{:?} {},{},{}", instr.opcode, instr.crbd(), instr.crba(), instr.crbb()).unwrap(); + } + PpcOpcode::mcrf => { + write!(out, "mcrf cr{},cr{}", instr.crfd(), instr.crfs()).unwrap(); + } + + // Trap + PpcOpcode::tdi => write!(out, "tdi {},r{},{}", instr.rd(), instr.ra(), instr.simm16()).unwrap(), + PpcOpcode::twi => write!(out, "twi {},r{},{}", instr.rd(), instr.ra(), instr.simm16()).unwrap(), + PpcOpcode::td => write!(out, "td {},r{},r{}", instr.rd(), instr.ra(), instr.rb()).unwrap(), + PpcOpcode::tw => write!(out, "tw {},r{},r{}", instr.rd(), instr.ra(), instr.rb()).unwrap(), + + // Default: just print opcode and raw hex + _ => { + write!(out, "{:?} [{:08X}]", instr.opcode, instr.raw).unwrap(); + } + } + out +} + +/// Disassemble a range of instructions from a byte slice. +pub fn disassemble_block(data: &[u8], base_addr: u32, count: usize) -> Vec<(u32, String)> { + let mut result = Vec::new(); + for i in 0..count { + let offset = i * 4; + if offset + 4 > data.len() { + break; + } + let raw = u32::from_be_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]); + let addr = base_addr + offset as u32; + let instr = crate::decode(raw, addr); + let text = disassemble(&instr); + result.push((addr, text)); + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::decoder::decode; + + #[test] + fn test_disasm_nop() { + // ori r0, r0, 0 = NOP + let instr = decode(0x60000000, 0); + let text = disassemble(&instr); + assert!(text.contains("ori"), "Expected 'ori', got: {}", text); + } + + #[test] + fn test_disasm_addi() { + let raw = (14u32 << 26) | (3 << 21) | (1 << 16) | 16; + let instr = decode(raw, 0); + let text = disassemble(&instr); + assert!(text.contains("addi"), "Got: {}", text); + assert!(text.contains("r3"), "Got: {}", text); + } +} diff --git a/crates/xenia-cpu/src/interpreter.rs b/crates/xenia-cpu/src/interpreter.rs new file mode 100644 index 0000000..02c4bd7 --- /dev/null +++ b/crates/xenia-cpu/src/interpreter.rs @@ -0,0 +1,2529 @@ +//! PPC interpreter - executes instructions one at a time. +//! This is the core execution engine. Every instruction is observable +//! by the debugger (pre_step/post_step hooks on every cycle). + +use crate::context::PpcContext; +use crate::decoder::{decode, DecodedInstr}; +use crate::opcode::PpcOpcode; +use xenia_memory::MemoryAccess; + +/// Result of executing a single instruction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StepResult { + /// Normal execution, advance to next instruction. + Continue, + /// Hit a system call (sc instruction). Kernel should handle. + SystemCall, + /// Hit an unimplemented opcode. + Unimplemented(PpcOpcode), + /// Hit a trap instruction. + Trap, + /// Execution halted (by debugger or error). + Halted, +} + +/// Execute a single PPC instruction. +pub fn step(ctx: &mut PpcContext, mem: &mut dyn MemoryAccess) -> StepResult { + let raw = mem.read_u32(ctx.pc); + let instr = decode(raw, ctx.pc); + + let result = execute(ctx, mem, &instr); + + ctx.cycle_count += 1; + ctx.timebase += 1; + + result +} + +/// Execute a decoded instruction, updating context and memory. +fn execute(ctx: &mut PpcContext, mem: &mut dyn MemoryAccess, instr: &DecodedInstr) -> StepResult { + match instr.opcode { + // ===== ALU: Immediate ===== + PpcOpcode::addi => { + let ra_val = if instr.ra() == 0 { 0 } else { ctx.gpr[instr.ra()] }; + ctx.gpr[instr.rd()] = ra_val.wrapping_add(instr.simm16() as i64 as u64); + ctx.pc += 4; + } + PpcOpcode::addis => { + let ra_val = if instr.ra() == 0 { 0 } else { ctx.gpr[instr.ra()] }; + ctx.gpr[instr.rd()] = ra_val.wrapping_add((instr.simm16() as i64 as u64) << 16); + ctx.pc += 4; + } + PpcOpcode::addic => { + let ra = ctx.gpr[instr.ra()]; + let imm = instr.simm16() as i64 as u64; + let result = ra.wrapping_add(imm); + ctx.xer_ca = if result < ra { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + ctx.pc += 4; + } + PpcOpcode::addicx => { + let ra = ctx.gpr[instr.ra()]; + let imm = instr.simm16() as i64 as u64; + let result = ra.wrapping_add(imm); + ctx.xer_ca = if result < ra { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + // Update CR0 + ctx.update_cr_signed(0, result as i32 as i64); + ctx.pc += 4; + } + PpcOpcode::subficx => { + let ra = ctx.gpr[instr.ra()]; + let imm = instr.simm16() as i64 as u64; + let result = imm.wrapping_sub(ra); + ctx.xer_ca = if imm >= ra { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + ctx.pc += 4; + } + PpcOpcode::mulli => { + let ra = ctx.gpr[instr.ra()] as i64; + let imm = instr.simm16() as i64; + ctx.gpr[instr.rd()] = ra.wrapping_mul(imm) as u64; + ctx.pc += 4; + } + + // ===== ALU: Register ===== + PpcOpcode::addx => { + let ra = ctx.gpr[instr.ra()]; + let rb = ctx.gpr[instr.rb()]; + let result = ra.wrapping_add(rb); + ctx.gpr[instr.rd()] = result; + if instr.oe() { + // TODO: overflow detection + } + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::addcx => { + let ra = ctx.gpr[instr.ra()]; + let rb = ctx.gpr[instr.rb()]; + let result = ra.wrapping_add(rb); + ctx.xer_ca = if result < ra { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::addex => { + let ra = ctx.gpr[instr.ra()]; + let rb = ctx.gpr[instr.rb()]; + let ca = ctx.xer_ca as u64; + let result = ra.wrapping_add(rb).wrapping_add(ca); + ctx.xer_ca = if result < ra || (ca != 0 && result == ra) { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::addzex => { + let ra = ctx.gpr[instr.ra()]; + let ca = ctx.xer_ca as u64; + let result = ra.wrapping_add(ca); + ctx.xer_ca = if result < ra { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::addmex => { + let ra = ctx.gpr[instr.ra()]; + let ca = ctx.xer_ca as u64; + let result = ra.wrapping_add(ca).wrapping_sub(1); + ctx.xer_ca = if ra != 0 || ca != 0 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::subfx => { + let ra = ctx.gpr[instr.ra()]; + let rb = ctx.gpr[instr.rb()]; + let result = rb.wrapping_sub(ra); + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::subfcx => { + let ra = ctx.gpr[instr.ra()]; + let rb = ctx.gpr[instr.rb()]; + let result = rb.wrapping_sub(ra); + ctx.xer_ca = if rb >= ra { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::subfex => { + let ra = ctx.gpr[instr.ra()]; + let rb = ctx.gpr[instr.rb()]; + let ca = ctx.xer_ca as u64; + let result = (!ra).wrapping_add(rb).wrapping_add(ca); + ctx.xer_ca = if rb > ra || (rb == ra && ca != 0) { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::subfzex => { + let ra = ctx.gpr[instr.ra()]; + let ca = ctx.xer_ca as u64; + let result = (!ra).wrapping_add(ca); + ctx.xer_ca = if !ra != 0 || ca != 0 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::subfmex => { + let ra = ctx.gpr[instr.ra()]; + let ca = ctx.xer_ca as u64; + let result = (!ra).wrapping_add(ca).wrapping_sub(1); + ctx.xer_ca = if (!ra) != 0 || ca != 0 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::negx => { + let ra = ctx.gpr[instr.ra()]; + ctx.gpr[instr.rd()] = (!ra).wrapping_add(1); + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::mullwx => { + let ra = ctx.gpr[instr.ra()] as i32 as i64; + let rb = ctx.gpr[instr.rb()] as i32 as i64; + ctx.gpr[instr.rd()] = ra.wrapping_mul(rb) as u64; + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::mulhwx => { + let ra = ctx.gpr[instr.ra()] as i32 as i64; + let rb = ctx.gpr[instr.rb()] as i32 as i64; + let result = ra.wrapping_mul(rb); + ctx.gpr[instr.rd()] = ((result >> 32) as i32 as i64 as u64) & 0xFFFF_FFFF; + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::mulhwux => { + let ra = ctx.gpr[instr.ra()] as u32 as u64; + let rb = ctx.gpr[instr.rb()] as u32 as u64; + let result = ra.wrapping_mul(rb); + ctx.gpr[instr.rd()] = (result >> 32) & 0xFFFF_FFFF; + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::divwx => { + let ra = ctx.gpr[instr.ra()] as i32; + let rb = ctx.gpr[instr.rb()] as i32; + if rb == 0 || (ra == i32::MIN && rb == -1) { + ctx.gpr[instr.rd()] = 0; + } else { + ctx.gpr[instr.rd()] = (ra / rb) as i64 as u64; + } + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::divwux => { + let ra = ctx.gpr[instr.ra()] as u32; + let rb = ctx.gpr[instr.rb()] as u32; + if rb == 0 { + ctx.gpr[instr.rd()] = 0; + } else { + ctx.gpr[instr.rd()] = (ra / rb) as u64; + } + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i32 as i64); + } + ctx.pc += 4; + } + + // ===== 64-bit Arithmetic ===== + PpcOpcode::mulldx => { + let ra = ctx.gpr[instr.ra()] as i64; + let rb = ctx.gpr[instr.rb()] as i64; + ctx.gpr[instr.rd()] = ra.wrapping_mul(rb) as u64; + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i64); + } + ctx.pc += 4; + } + PpcOpcode::mulhdx => { + let ra = ctx.gpr[instr.ra()] as i64 as i128; + let rb = ctx.gpr[instr.rb()] as i64 as i128; + ctx.gpr[instr.rd()] = (ra.wrapping_mul(rb) >> 64) as u64; + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i64); + } + ctx.pc += 4; + } + PpcOpcode::mulhdux => { + let ra = ctx.gpr[instr.ra()] as u128; + let rb = ctx.gpr[instr.rb()] as u128; + ctx.gpr[instr.rd()] = (ra.wrapping_mul(rb) >> 64) as u64; + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i64); + } + ctx.pc += 4; + } + PpcOpcode::divdx => { + let ra = ctx.gpr[instr.ra()] as i64; + let rb = ctx.gpr[instr.rb()] as i64; + if rb == 0 || (ra == i64::MIN && rb == -1) { + ctx.gpr[instr.rd()] = 0; + } else { + ctx.gpr[instr.rd()] = (ra / rb) as u64; + } + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i64); + } + ctx.pc += 4; + } + PpcOpcode::divdux => { + let ra = ctx.gpr[instr.ra()]; + let rb = ctx.gpr[instr.rb()]; + if rb == 0 { + ctx.gpr[instr.rd()] = 0; + } else { + ctx.gpr[instr.rd()] = ra / rb; + } + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i64); + } + ctx.pc += 4; + } + + // ===== Logical ===== + PpcOpcode::andix => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & (instr.uimm16() as u64); + ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); + ctx.pc += 4; + } + PpcOpcode::andisx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & ((instr.uimm16() as u64) << 16); + ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); + ctx.pc += 4; + } + PpcOpcode::ori => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] | (instr.uimm16() as u64); + ctx.pc += 4; + } + PpcOpcode::oris => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] | ((instr.uimm16() as u64) << 16); + ctx.pc += 4; + } + PpcOpcode::xori => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] ^ (instr.uimm16() as u64); + ctx.pc += 4; + } + PpcOpcode::xoris => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] ^ ((instr.uimm16() as u64) << 16); + ctx.pc += 4; + } + PpcOpcode::andx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & ctx.gpr[instr.rb()]; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::andcx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & !ctx.gpr[instr.rb()]; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::orx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] | ctx.gpr[instr.rb()]; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::orcx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] | !ctx.gpr[instr.rb()]; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::xorx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] ^ ctx.gpr[instr.rb()]; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::norx => { + ctx.gpr[instr.ra()] = !(ctx.gpr[instr.rs()] | ctx.gpr[instr.rb()]); + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::nandx => { + ctx.gpr[instr.ra()] = !(ctx.gpr[instr.rs()] & ctx.gpr[instr.rb()]); + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::eqvx => { + ctx.gpr[instr.ra()] = !(ctx.gpr[instr.rs()] ^ ctx.gpr[instr.rb()]); + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + + // ===== Extend/Count ===== + PpcOpcode::extsbx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] as i8 as i64 as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::extshx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] as i16 as i64 as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::extswx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] as i32 as i64 as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::cntlzwx => { + ctx.gpr[instr.ra()] = (ctx.gpr[instr.rs()] as u32).leading_zeros() as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::cntlzdx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()].leading_zeros() as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + + // ===== Shift ===== + PpcOpcode::slwx => { + let sh = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = if sh < 32 { + ((ctx.gpr[instr.rs()] as u32) << sh) as u64 + } else { 0 }; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::srwx => { + let sh = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = if sh < 32 { + ((ctx.gpr[instr.rs()] as u32) >> sh) as u64 + } else { 0 }; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::srawx => { + let rs = ctx.gpr[instr.rs()] as i32; + let sh = ctx.gpr[instr.rb()] as u32 & 0x3F; + if sh == 0 { + ctx.gpr[instr.ra()] = rs as i64 as u64; + ctx.xer_ca = 0; + } else if sh < 32 { + let result = rs >> sh; + ctx.xer_ca = if rs < 0 && (rs as u32) << (32 - sh) != 0 { 1 } else { 0 }; + ctx.gpr[instr.ra()] = result as i64 as u64; + } else { + ctx.gpr[instr.ra()] = if rs < 0 { u64::MAX } else { 0 }; + ctx.xer_ca = if rs < 0 { 1 } else { 0 }; + } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::srawix => { + let rs = ctx.gpr[instr.rs()] as i32; + let sh = instr.sh(); + if sh == 0 { + ctx.gpr[instr.ra()] = rs as i64 as u64; + ctx.xer_ca = 0; + } else { + let result = rs >> sh; + ctx.xer_ca = if rs < 0 && (rs as u32) << (32 - sh) != 0 { 1 } else { 0 }; + ctx.gpr[instr.ra()] = result as i64 as u64; + } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::sldx => { + let sh = ctx.gpr[instr.rb()] & 0x7F; + ctx.gpr[instr.ra()] = if sh < 64 { + ctx.gpr[instr.rs()] << sh + } else { 0 }; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::srdx => { + let sh = ctx.gpr[instr.rb()] & 0x7F; + ctx.gpr[instr.ra()] = if sh < 64 { + ctx.gpr[instr.rs()] >> sh + } else { 0 }; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::sradx => { + let rs = ctx.gpr[instr.rs()] as i64; + let sh = ctx.gpr[instr.rb()] & 0x7F; + if sh == 0 { + ctx.gpr[instr.ra()] = rs as u64; + ctx.xer_ca = 0; + } else if sh < 64 { + let result = rs >> sh; + ctx.xer_ca = if rs < 0 && (rs as u64) << (64 - sh) != 0 { 1 } else { 0 }; + ctx.gpr[instr.ra()] = result as u64; + } else { + ctx.gpr[instr.ra()] = if rs < 0 { u64::MAX } else { 0 }; + ctx.xer_ca = if rs < 0 { 1 } else { 0 }; + } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::sradix => { + let rs = ctx.gpr[instr.rs()] as i64; + let sh = instr.sh64(); + if sh == 0 { + ctx.gpr[instr.ra()] = rs as u64; + ctx.xer_ca = 0; + } else { + let result = rs >> sh; + ctx.xer_ca = if rs < 0 && (rs as u64) << (64 - sh) != 0 { 1 } else { 0 }; + ctx.gpr[instr.ra()] = result as u64; + } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + + // ===== Rotate ===== + PpcOpcode::rlwinmx => { + let rs = ctx.gpr[instr.rs()] as u32; + let sh = instr.sh(); + let mb = instr.mb(); + let me = instr.me(); + let rotated = rs.rotate_left(sh); + let mask = rlw_mask(mb, me); + ctx.gpr[instr.ra()] = (rotated & mask) as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::rlwimix => { + let rs = ctx.gpr[instr.rs()] as u32; + let sh = instr.sh(); + let mb = instr.mb(); + let me = instr.me(); + let rotated = rs.rotate_left(sh); + let mask = rlw_mask(mb, me); + let ra = ctx.gpr[instr.ra()] as u32; + ctx.gpr[instr.ra()] = ((rotated & mask) | (ra & !mask)) as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::rlwnmx => { + let rs = ctx.gpr[instr.rs()] as u32; + let sh = ctx.gpr[instr.rb()] as u32 & 0x1F; + let mb = instr.mb(); + let me = instr.me(); + let rotated = rs.rotate_left(sh); + let mask = rlw_mask(mb, me); + ctx.gpr[instr.ra()] = (rotated & mask) as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::rldiclx => { + let rs = ctx.gpr[instr.rs()]; + let sh = instr.sh64(); + let mb = (instr.mb() << 1) | ((instr.raw >> 1) & 1); // 6-bit mb + let rotated = rs.rotate_left(sh); + let mask = rld_mask_left(mb); + ctx.gpr[instr.ra()] = rotated & mask; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::rldicrx => { + let rs = ctx.gpr[instr.rs()]; + let sh = instr.sh64(); + let me = (instr.mb() << 1) | ((instr.raw >> 1) & 1); // 6-bit me + let rotated = rs.rotate_left(sh); + let mask = rld_mask_right(me); + ctx.gpr[instr.ra()] = rotated & mask; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::rldicx => { + let rs = ctx.gpr[instr.rs()]; + let sh = instr.sh64(); + let mb = (instr.mb() << 1) | ((instr.raw >> 1) & 1); + let rotated = rs.rotate_left(sh); + let mask = rld_mask_left(mb) & rld_mask_right(63 - sh); + ctx.gpr[instr.ra()] = rotated & mask; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::rldimix => { + let rs = ctx.gpr[instr.rs()]; + let sh = instr.sh64(); + let mb = (instr.mb() << 1) | ((instr.raw >> 1) & 1); + let rotated = rs.rotate_left(sh); + let mask = rld_mask_left(mb) & rld_mask_right(63 - sh); + ctx.gpr[instr.ra()] = (rotated & mask) | (ctx.gpr[instr.ra()] & !mask); + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::rldclx => { + let rs = ctx.gpr[instr.rs()]; + let sh = ctx.gpr[instr.rb()] & 0x3F; + let mb = (instr.mb() << 1) | ((instr.raw >> 1) & 1); + let rotated = rs.rotate_left(sh as u32); + let mask = rld_mask_left(mb); + ctx.gpr[instr.ra()] = rotated & mask; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::rldcrx => { + let rs = ctx.gpr[instr.rs()]; + let sh = ctx.gpr[instr.rb()] & 0x3F; + let me = (instr.mb() << 1) | ((instr.raw >> 1) & 1); + let rotated = rs.rotate_left(sh as u32); + let mask = rld_mask_right(me); + ctx.gpr[instr.ra()] = rotated & mask; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + + // ===== Compare ===== + PpcOpcode::cmpi => { + let bf = instr.crfd(); + if instr.l() { + // 64-bit compare + let ra = ctx.gpr[instr.ra()] as i64; + let imm = instr.simm16() as i64; + ctx.update_cr_signed(bf, ra - imm); + if ra == imm { ctx.cr[bf].eq = true; } + } else { + let ra = ctx.gpr[instr.ra()] as i32; + let imm = instr.simm16() as i32; + ctx.update_cr_signed(bf, (ra as i64) - (imm as i64)); + if ra == imm { ctx.cr[bf].eq = true; } + } + ctx.pc += 4; + } + PpcOpcode::cmpli => { + let bf = instr.crfd(); + if instr.l() { + let ra = ctx.gpr[instr.ra()]; + let imm = instr.uimm16() as u64; + ctx.update_cr_unsigned(bf, ra, imm); + } else { + let ra = ctx.gpr[instr.ra()] as u32 as u64; + let imm = instr.uimm16() as u64; + ctx.update_cr_unsigned(bf, ra, imm); + } + ctx.pc += 4; + } + PpcOpcode::cmp => { + let bf = instr.crfd(); + if instr.l() { + let ra = ctx.gpr[instr.ra()] as i64; + let rb = ctx.gpr[instr.rb()] as i64; + ctx.update_cr_signed(bf, ra.wrapping_sub(rb)); + if ra == rb { ctx.cr[bf].eq = true; } + } else { + let ra = ctx.gpr[instr.ra()] as i32; + let rb = ctx.gpr[instr.rb()] as i32; + ctx.update_cr_signed(bf, (ra as i64).wrapping_sub(rb as i64)); + if ra == rb { ctx.cr[bf].eq = true; } + } + ctx.pc += 4; + } + PpcOpcode::cmpl => { + let bf = instr.crfd(); + if instr.l() { + ctx.update_cr_unsigned(bf, ctx.gpr[instr.ra()], ctx.gpr[instr.rb()]); + } else { + ctx.update_cr_unsigned(bf, ctx.gpr[instr.ra()] as u32 as u64, ctx.gpr[instr.rb()] as u32 as u64); + } + ctx.pc += 4; + } + + // ===== Branch ===== + PpcOpcode::bx => { + let target = if instr.aa() { + instr.li() as u32 + } else { + ctx.pc.wrapping_add(instr.li() as u32) + }; + if instr.lk() { + ctx.lr = (ctx.pc + 4) as u64; + } + ctx.pc = target; + } + PpcOpcode::bcx => { + let bo = instr.bo(); + let bi = instr.bi(); + + // Decrement CTR if needed + if bo & 0b00100 == 0 { + ctx.ctr = ctx.ctr.wrapping_sub(1); + } + + let ctr_ok = (bo & 0b00100) != 0 + || ((ctx.ctr != 0) ^ ((bo & 0b00010) != 0)); + let cond_ok = (bo & 0b10000) != 0 + || (ctx.get_cr_bit(bi) == ((bo & 0b01000) != 0)); + + if ctr_ok && cond_ok { + let target = if instr.aa() { + instr.bd() as u32 + } else { + ctx.pc.wrapping_add(instr.bd() as u32) + }; + if instr.lk() { + ctx.lr = (ctx.pc + 4) as u64; + } + ctx.pc = target; + } else { + if instr.lk() { + ctx.lr = (ctx.pc + 4) as u64; + } + ctx.pc += 4; + } + } + PpcOpcode::bclrx => { + let bo = instr.bo(); + let bi = instr.bi(); + + if bo & 0b00100 == 0 { + ctx.ctr = ctx.ctr.wrapping_sub(1); + } + + let ctr_ok = (bo & 0b00100) != 0 + || ((ctx.ctr != 0) ^ ((bo & 0b00010) != 0)); + let cond_ok = (bo & 0b10000) != 0 + || (ctx.get_cr_bit(bi) == ((bo & 0b01000) != 0)); + + let next_pc = ctx.pc + 4; + if ctr_ok && cond_ok { + ctx.pc = (ctx.lr as u32) & !3; + } else { + ctx.pc = next_pc; + } + if instr.lk() { + ctx.lr = next_pc as u64; + } + } + PpcOpcode::bcctrx => { + let bo = instr.bo(); + let bi = instr.bi(); + + let cond_ok = (bo & 0b10000) != 0 + || (ctx.get_cr_bit(bi) == ((bo & 0b01000) != 0)); + + if cond_ok { + let next_pc = ctx.pc + 4; + ctx.pc = (ctx.ctr as u32) & !3; + if instr.lk() { + ctx.lr = next_pc as u64; + } + } else { + if instr.lk() { + ctx.lr = (ctx.pc + 4) as u64; + } + ctx.pc += 4; + } + } + + // ===== System call ===== + PpcOpcode::sc => { + ctx.pc += 4; + return StepResult::SystemCall; + } + + // ===== Load instructions ===== + PpcOpcode::lwz => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; + ctx.pc += 4; + } + PpcOpcode::lwzu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lwzx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; + ctx.pc += 4; + } + PpcOpcode::lwzux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lbz => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u8(ea) as u64; + ctx.pc += 4; + } + PpcOpcode::lbzu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u8(ea) as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lbzx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u8(ea) as u64; + ctx.pc += 4; + } + PpcOpcode::lbzux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u8(ea) as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lhz => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as u64; + ctx.pc += 4; + } + PpcOpcode::lhzu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lhzx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as u64; + ctx.pc += 4; + } + PpcOpcode::lha => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.pc += 4; + } + PpcOpcode::lhax => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.pc += 4; + } + PpcOpcode::lhzux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lhau => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lhaux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::ld => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.ds() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u64(ea); + ctx.pc += 4; + } + PpcOpcode::ldx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u64(ea); + ctx.pc += 4; + } + PpcOpcode::lwa => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.ds() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as i32 as i64 as u64; + ctx.pc += 4; + } + PpcOpcode::lwax => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as i32 as i64 as u64; + ctx.pc += 4; + } + PpcOpcode::lwaux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as i32 as i64 as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::ldu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.ds() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u64(ea); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::ldux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u64(ea); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + + // FP loads + PpcOpcode::lfs => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + ctx.fpr[instr.rd()] = mem.read_f32(ea) as f64; + ctx.pc += 4; + } + PpcOpcode::lfsx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.fpr[instr.rd()] = mem.read_f32(ea) as f64; + ctx.pc += 4; + } + PpcOpcode::lfd => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + ctx.fpr[instr.rd()] = mem.read_f64(ea); + ctx.pc += 4; + } + PpcOpcode::lfdx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.fpr[instr.rd()] = mem.read_f64(ea); + ctx.pc += 4; + } + PpcOpcode::lfsu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + ctx.fpr[instr.rd()] = mem.read_f32(ea) as f64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lfsux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.fpr[instr.rd()] = mem.read_f32(ea) as f64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lfdu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + ctx.fpr[instr.rd()] = mem.read_f64(ea); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lfdux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.fpr[instr.rd()] = mem.read_f64(ea); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + + // Reservation (lwarx/stwcx) + PpcOpcode::lwarx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + let val = mem.read_u32(ea); + ctx.gpr[instr.rd()] = val as u64; + ctx.reserved_addr = ea; + ctx.reserved_val = val as u64; + ctx.has_reservation = true; + ctx.pc += 4; + } + PpcOpcode::stwcx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + if ctx.has_reservation && ctx.reserved_addr == ea { + mem.write_u32(ea, ctx.gpr[instr.rs()] as u32); + ctx.cr[0] = crate::context::CrField { lt: false, gt: false, eq: true, so: ctx.xer_so != 0 }; + } else { + ctx.cr[0] = crate::context::CrField { lt: false, gt: false, eq: false, so: ctx.xer_so != 0 }; + } + ctx.has_reservation = false; + ctx.pc += 4; + } + + // ===== Store instructions ===== + PpcOpcode::stw => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_u32(ea, ctx.gpr[instr.rs()] as u32); + ctx.pc += 4; + } + PpcOpcode::stwu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_u32(ea, ctx.gpr[instr.rs()] as u32); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::stwx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u32(ea, ctx.gpr[instr.rs()] as u32); + ctx.pc += 4; + } + PpcOpcode::stwux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u32(ea, ctx.gpr[instr.rs()] as u32); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::stb => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_u8(ea, ctx.gpr[instr.rs()] as u8); + ctx.pc += 4; + } + PpcOpcode::stbu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_u8(ea, ctx.gpr[instr.rs()] as u8); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::stbx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u8(ea, ctx.gpr[instr.rs()] as u8); + ctx.pc += 4; + } + PpcOpcode::stbux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u8(ea, ctx.gpr[instr.rs()] as u8); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::sth => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_u16(ea, ctx.gpr[instr.rs()] as u16); + ctx.pc += 4; + } + PpcOpcode::sthu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_u16(ea, ctx.gpr[instr.rs()] as u16); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::sthx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u16(ea, ctx.gpr[instr.rs()] as u16); + ctx.pc += 4; + } + PpcOpcode::sthux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u16(ea, ctx.gpr[instr.rs()] as u16); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::std => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.ds() as i64 as u64) as u32; + mem.write_u64(ea, ctx.gpr[instr.rs()]); + ctx.pc += 4; + } + PpcOpcode::stdx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u64(ea, ctx.gpr[instr.rs()]); + ctx.pc += 4; + } + PpcOpcode::stdu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.ds() as i64 as u64) as u32; + mem.write_u64(ea, ctx.gpr[instr.rs()]); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::stdux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u64(ea, ctx.gpr[instr.rs()]); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + + // FP stores + PpcOpcode::stfs => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_f32(ea, ctx.fpr[instr.rs()] as f32); + ctx.pc += 4; + } + PpcOpcode::stfsu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_f32(ea, ctx.fpr[instr.rs()] as f32); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::stfsx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_f32(ea, ctx.fpr[instr.rs()] as f32); + ctx.pc += 4; + } + PpcOpcode::stfsux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_f32(ea, ctx.fpr[instr.rs()] as f32); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::stfd => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_f64(ea, ctx.fpr[instr.rs()]); + ctx.pc += 4; + } + PpcOpcode::stfdu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_f64(ea, ctx.fpr[instr.rs()]); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::stfdx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_f64(ea, ctx.fpr[instr.rs()]); + ctx.pc += 4; + } + PpcOpcode::stfdux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_f64(ea, ctx.fpr[instr.rs()]); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::stfiwx => { + // Store FP as integer word: stores low 32 bits of FPR as-is + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u32(ea, ctx.fpr[instr.rs()].to_bits() as u32); + ctx.pc += 4; + } + + // String load/store + PpcOpcode::lswi => { + let mut ea = if instr.ra() == 0 { 0u32 } else { ctx.gpr[instr.ra()] as u32 }; + let nb = if instr.rb() == 0 { 32 } else { instr.rb() as u32 }; + let mut rd = instr.rd(); + let mut bytes_left = nb; + while bytes_left > 0 { + let mut val = 0u32; + for byte_idx in 0..4 { + if bytes_left == 0 { break; } + let b = mem.read_u8(ea) as u32; + val |= b << (24 - byte_idx * 8); + ea = ea.wrapping_add(1); + bytes_left -= 1; + } + ctx.gpr[rd] = val as u64; + rd = (rd + 1) % 32; + } + ctx.pc += 4; + } + PpcOpcode::stswi => { + let mut ea = if instr.ra() == 0 { 0u32 } else { ctx.gpr[instr.ra()] as u32 }; + let nb = if instr.rb() == 0 { 32 } else { instr.rb() as u32 }; + let mut rs = instr.rs(); + let mut bytes_left = nb; + while bytes_left > 0 { + let val = ctx.gpr[rs] as u32; + for byte_idx in 0..4 { + if bytes_left == 0 { break; } + mem.write_u8(ea, (val >> (24 - byte_idx * 8)) as u8); + ea = ea.wrapping_add(1); + bytes_left -= 1; + } + rs = (rs + 1) % 32; + } + ctx.pc += 4; + } + + // ===== Special register moves ===== + PpcOpcode::mfspr => { + let spr = instr.spr(); + ctx.gpr[instr.rd()] = match spr { + crate::context::spr::XER => ctx.xer() as u64, + crate::context::spr::LR => ctx.lr, + crate::context::spr::CTR => ctx.ctr, + crate::context::spr::TBL => ctx.timebase & 0xFFFF_FFFF, + crate::context::spr::TBU => ctx.timebase >> 32, + _ => { + tracing::warn!("mfspr: unimplemented SPR {}", spr); + 0 + } + }; + ctx.pc += 4; + } + PpcOpcode::mtspr => { + let spr = instr.spr(); + let val = ctx.gpr[instr.rs()]; + match spr { + crate::context::spr::XER => ctx.set_xer(val as u32), + crate::context::spr::LR => ctx.lr = val, + crate::context::spr::CTR => ctx.ctr = val, + _ => { + tracing::warn!("mtspr: unimplemented SPR {}", spr); + } + } + ctx.pc += 4; + } + PpcOpcode::mfcr => { + ctx.gpr[instr.rd()] = ctx.cr() as u64; + ctx.pc += 4; + } + PpcOpcode::mtcrf => { + let crm = instr.crm(); + let val = ctx.gpr[instr.rs()] as u32; + let old = ctx.cr(); + let mut new = old; + for i in 0..8u32 { + if crm & (1 << (7 - i)) != 0 { + let mask = 0xF << (28 - i * 4); + new = (new & !mask) | (val & mask); + } + } + ctx.set_cr(new); + ctx.pc += 4; + } + PpcOpcode::mfmsr => { + ctx.gpr[instr.rd()] = ctx.msr; + ctx.pc += 4; + } + PpcOpcode::mtmsr | PpcOpcode::mtmsrd => { + ctx.msr = ctx.gpr[instr.rs()]; + ctx.pc += 4; + } + PpcOpcode::mftb => { + let tbr = instr.spr(); + ctx.gpr[instr.rd()] = match tbr { + 268 => ctx.timebase & 0xFFFF_FFFF, + 269 => ctx.timebase >> 32, + _ => 0, + }; + ctx.pc += 4; + } + + // CR logical + PpcOpcode::crand => { cr_logical(ctx, instr, |a, b| a & b); ctx.pc += 4; } + PpcOpcode::crandc => { cr_logical(ctx, instr, |a, b| a & !b); ctx.pc += 4; } + PpcOpcode::creqv => { cr_logical(ctx, instr, |a, b| !(a ^ b)); ctx.pc += 4; } + PpcOpcode::crnand => { cr_logical(ctx, instr, |a, b| !(a & b)); ctx.pc += 4; } + PpcOpcode::crnor => { cr_logical(ctx, instr, |a, b| !(a | b)); ctx.pc += 4; } + PpcOpcode::cror => { cr_logical(ctx, instr, |a, b| a | b); ctx.pc += 4; } + PpcOpcode::crorc => { cr_logical(ctx, instr, |a, b| a | !b); ctx.pc += 4; } + PpcOpcode::crxor => { cr_logical(ctx, instr, |a, b| a ^ b); ctx.pc += 4; } + PpcOpcode::mcrf => { + ctx.cr[instr.crfd()] = ctx.cr[instr.crfs()]; + ctx.pc += 4; + } + + // ===== Cache/sync (no-ops in interpreter) ===== + PpcOpcode::dcbf | PpcOpcode::dcbi | PpcOpcode::dcbst | + PpcOpcode::dcbt | PpcOpcode::dcbtst | PpcOpcode::icbi | + PpcOpcode::sync | PpcOpcode::eieio | PpcOpcode::isync => { + ctx.pc += 4; + } + PpcOpcode::dcbz => { + // Zero 32 bytes at effective address + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = (ea.wrapping_add(ctx.gpr[instr.rb()]) as u32) & !31; + for i in 0..8 { + mem.write_u32(ea + i * 4, 0); + } + ctx.pc += 4; + } + PpcOpcode::dcbz128 => { + // Zero 128 bytes + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = (ea.wrapping_add(ctx.gpr[instr.rb()]) as u32) & !127; + for i in 0..32 { + mem.write_u32(ea + i * 4, 0); + } + ctx.pc += 4; + } + + // ===== Load multiple ===== + PpcOpcode::lmw => { + let mut ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + ea = ea.wrapping_add(instr.d() as i64 as u64); + for r in instr.rd()..32 { + ctx.gpr[r] = mem.read_u32(ea as u32) as u64; + ea = ea.wrapping_add(4); + } + ctx.pc += 4; + } + PpcOpcode::stmw => { + let mut ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + ea = ea.wrapping_add(instr.d() as i64 as u64); + for r in instr.rs()..32 { + mem.write_u32(ea as u32, ctx.gpr[r] as u32); + ea = ea.wrapping_add(4); + } + ctx.pc += 4; + } + + // ===== Trap ===== + PpcOpcode::tdi | PpcOpcode::twi | PpcOpcode::td | PpcOpcode::tw => { + // For now, just trace and continue + tracing::warn!("Trap instruction at {:#010x}: {:?}", ctx.pc, instr.opcode); + ctx.pc += 4; + return StepResult::Trap; + } + + // ===== Byte-reverse loads ===== + PpcOpcode::lwbrx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + let val = mem.read_u32(ea); + ctx.gpr[instr.rd()] = val.swap_bytes() as u64; + ctx.pc += 4; + } + PpcOpcode::lhbrx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + let val = mem.read_u16(ea); + ctx.gpr[instr.rd()] = val.swap_bytes() as u64; + ctx.pc += 4; + } + PpcOpcode::stwbrx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u32(ea, (ctx.gpr[instr.rs()] as u32).swap_bytes()); + ctx.pc += 4; + } + PpcOpcode::sthbrx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u16(ea, (ctx.gpr[instr.rs()] as u16).swap_bytes()); + ctx.pc += 4; + } + + // ===== VMX/VMX128: Vector Load/Store ===== + PpcOpcode::lvx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = (ea.wrapping_add(ctx.gpr[instr.rb()]) & !0xF) as u32; // aligned + let mut bytes = [0u8; 16]; + for i in 0..16 { bytes[i] = mem.read_u8(ea + i as u32); } + ctx.vr[instr.rd()] = xenia_types::Vec128::from_bytes(bytes); + ctx.pc += 4; + } + PpcOpcode::lvx128 => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = (ea.wrapping_add(ctx.gpr[instr.rb()]) & !0xF) as u32; + let mut bytes = [0u8; 16]; + for i in 0..16 { bytes[i] = mem.read_u8(ea + i as u32); } + ctx.vr[instr.vd128()] = xenia_types::Vec128::from_bytes(bytes); + ctx.pc += 4; + } + PpcOpcode::stvx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = (ea.wrapping_add(ctx.gpr[instr.rb()]) & !0xF) as u32; + let bytes = ctx.vr[instr.rs()].as_bytes(); + for i in 0..16 { mem.write_u8(ea + i as u32, bytes[i]); } + ctx.pc += 4; + } + PpcOpcode::stvx128 => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = (ea.wrapping_add(ctx.gpr[instr.rb()]) & !0xF) as u32; + let bytes = ctx.vr[instr.vs128()].as_bytes(); + for i in 0..16 { mem.write_u8(ea + i as u32, bytes[i]); } + ctx.pc += 4; + } + // lvewx, lvebx, lvehx all load aligned 16 bytes (per xenia reference) + PpcOpcode::lvewx | PpcOpcode::lvebx | PpcOpcode::lvehx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = (ea.wrapping_add(ctx.gpr[instr.rb()]) & !0xF) as u32; + let mut bytes = [0u8; 16]; + for i in 0..16 { bytes[i] = mem.read_u8(ea + i as u32); } + ctx.vr[instr.rd()] = xenia_types::Vec128::from_bytes(bytes); + ctx.pc += 4; + } + PpcOpcode::stvewx | PpcOpcode::stvebx | PpcOpcode::stvehx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = (ea.wrapping_add(ctx.gpr[instr.rb()]) & !0xF) as u32; + let bytes = ctx.vr[instr.rs()].as_bytes(); + for i in 0..16 { mem.write_u8(ea + i as u32, bytes[i]); } + ctx.pc += 4; + } + PpcOpcode::lvxl | PpcOpcode::lvxl128 => { + // Same as lvx but with cache hint (ignored) + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = (ea.wrapping_add(ctx.gpr[instr.rb()]) & !0xF) as u32; + let mut bytes = [0u8; 16]; + for i in 0..16 { bytes[i] = mem.read_u8(ea + i as u32); } + let vd = if matches!(instr.opcode, PpcOpcode::lvxl128) { instr.vd128() } else { instr.rd() }; + ctx.vr[vd] = xenia_types::Vec128::from_bytes(bytes); + ctx.pc += 4; + } + PpcOpcode::stvxl | PpcOpcode::stvxl128 => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = (ea.wrapping_add(ctx.gpr[instr.rb()]) & !0xF) as u32; + let vs = if matches!(instr.opcode, PpcOpcode::stvxl128) { instr.vs128() } else { instr.rs() }; + let bytes = ctx.vr[vs].as_bytes(); + for i in 0..16 { mem.write_u8(ea + i as u32, bytes[i]); } + ctx.pc += 4; + } + + // ===== VMX: Float Arithmetic ===== + PpcOpcode::vaddfp => { + let a = ctx.vr[instr.ra()].as_f32x4(); + let b = ctx.vr[instr.rb()].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = a[i] + b[i]; } + ctx.vr[instr.rd()] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vaddfp128 => { + let a = ctx.vr[instr.va128()].as_f32x4(); + let b = ctx.vr[instr.vb128()].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = a[i] + b[i]; } + ctx.vr[instr.vd128()] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vsubfp => { + let a = ctx.vr[instr.ra()].as_f32x4(); + let b = ctx.vr[instr.rb()].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = a[i] - b[i]; } + ctx.vr[instr.rd()] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vsubfp128 => { + let a = ctx.vr[instr.va128()].as_f32x4(); + let b = ctx.vr[instr.vb128()].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = a[i] - b[i]; } + ctx.vr[instr.vd128()] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vmaddfp => { + // vD = (vA * vC) + vB + let a = ctx.vr[instr.ra()].as_f32x4(); + let b = ctx.vr[instr.rb()].as_f32x4(); + let c = ctx.vr[instr.rc()].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = a[i].mul_add(c[i], b[i]); } + ctx.vr[instr.rd()] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vmaddfp128 => { + let a = ctx.vr[instr.va128()].as_f32x4(); + let b = ctx.vr[instr.vb128()].as_f32x4(); + let d = ctx.vr[instr.vd128()].as_f32x4(); // vD is also source (accumulator) + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = a[i].mul_add(b[i], d[i]); } + ctx.vr[instr.vd128()] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vnmsubfp => { + // vD = -(vA * vC - vB) = vB - vA * vC + let a = ctx.vr[instr.ra()].as_f32x4(); + let b = ctx.vr[instr.rb()].as_f32x4(); + let c = ctx.vr[instr.rc()].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = b[i] - a[i] * c[i]; } + ctx.vr[instr.rd()] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vnmsubfp128 => { + let a = ctx.vr[instr.va128()].as_f32x4(); + let b = ctx.vr[instr.vb128()].as_f32x4(); + let d = ctx.vr[instr.vd128()].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = d[i] - a[i] * b[i]; } + ctx.vr[instr.vd128()] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vmulfp128 => { + let a = ctx.vr[instr.va128()].as_f32x4(); + let b = ctx.vr[instr.vb128()].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = a[i] * b[i]; } + ctx.vr[instr.vd128()] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vmaxfp => { + let a = ctx.vr[instr.ra()].as_f32x4(); + let b = ctx.vr[instr.rb()].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = if a[i] > b[i] { a[i] } else { b[i] }; } + ctx.vr[instr.rd()] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vmaxfp128 => { + let a = ctx.vr[instr.va128()].as_f32x4(); + let b = ctx.vr[instr.vb128()].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = if a[i] > b[i] { a[i] } else { b[i] }; } + ctx.vr[instr.vd128()] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vminfp => { + let a = ctx.vr[instr.ra()].as_f32x4(); + let b = ctx.vr[instr.rb()].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = if a[i] < b[i] { a[i] } else { b[i] }; } + ctx.vr[instr.rd()] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vminfp128 => { + let a = ctx.vr[instr.va128()].as_f32x4(); + let b = ctx.vr[instr.vb128()].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = if a[i] < b[i] { a[i] } else { b[i] }; } + ctx.vr[instr.vd128()] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vrefp | PpcOpcode::vrefp128 => { + let vb = if matches!(instr.opcode, PpcOpcode::vrefp128) { instr.vb128() } else { instr.rb() }; + let vd = if matches!(instr.opcode, PpcOpcode::vrefp128) { instr.vd128() } else { instr.rd() }; + let b = ctx.vr[vb].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = 1.0 / b[i]; } + ctx.vr[vd] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vrsqrtefp | PpcOpcode::vrsqrtefp128 => { + let vb = if matches!(instr.opcode, PpcOpcode::vrsqrtefp128) { instr.vb128() } else { instr.rb() }; + let vd = if matches!(instr.opcode, PpcOpcode::vrsqrtefp128) { instr.vd128() } else { instr.rd() }; + let b = ctx.vr[vb].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = 1.0 / b[i].sqrt(); } + ctx.vr[vd] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + + // ===== VMX: Float Compare ===== + PpcOpcode::vcmpeqfp | PpcOpcode::vcmpeqfp128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_f32x4(); + let b = ctx.vr[vb].as_f32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { r[i] = if a[i] == b[i] { 0xFFFF_FFFF } else { 0 }; } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + if instr.rc_bit() { update_cr6_from_vmask(&r, ctx); } + ctx.pc += 4; + } + PpcOpcode::vcmpgefp | PpcOpcode::vcmpgefp128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_f32x4(); + let b = ctx.vr[vb].as_f32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { r[i] = if a[i] >= b[i] { 0xFFFF_FFFF } else { 0 }; } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + if instr.rc_bit() { update_cr6_from_vmask(&r, ctx); } + ctx.pc += 4; + } + PpcOpcode::vcmpgtfp | PpcOpcode::vcmpgtfp128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_f32x4(); + let b = ctx.vr[vb].as_f32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { r[i] = if a[i] > b[i] { 0xFFFF_FFFF } else { 0 }; } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + if instr.rc_bit() { update_cr6_from_vmask(&r, ctx); } + ctx.pc += 4; + } + + // ===== VMX: Logical ===== + PpcOpcode::vand | PpcOpcode::vand128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_u32x4(); + let b = ctx.vr[vb].as_u32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { r[i] = a[i] & b[i]; } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vandc | PpcOpcode::vandc128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_u32x4(); + let b = ctx.vr[vb].as_u32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { r[i] = a[i] & !b[i]; } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vor | PpcOpcode::vor128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_u32x4(); + let b = ctx.vr[vb].as_u32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { r[i] = a[i] | b[i]; } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vxor | PpcOpcode::vxor128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_u32x4(); + let b = ctx.vr[vb].as_u32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { r[i] = a[i] ^ b[i]; } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vnor | PpcOpcode::vnor128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_u32x4(); + let b = ctx.vr[vb].as_u32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { r[i] = !(a[i] | b[i]); } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vsel | PpcOpcode::vsel128 => { + // vD = (vA & ~vC) | (vB & vC) + let (va, vb, vd); + let vc; + if matches!(instr.opcode, PpcOpcode::vsel128) { + va = instr.va128(); + vb = instr.vb128(); + vd = instr.vd128(); + vc = vd; // for 128, vC is encoded in vD field + } else { + va = instr.ra(); + vb = instr.rb(); + vd = instr.rd(); + vc = instr.rc(); + } + let a = ctx.vr[va].as_u32x4(); + let b = ctx.vr[vb].as_u32x4(); + let c = ctx.vr[vc].as_u32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { r[i] = (a[i] & !c[i]) | (b[i] & c[i]); } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + ctx.pc += 4; + } + + // ===== VMX: Permute/Splat/Shift ===== + PpcOpcode::vperm | PpcOpcode::vperm128 => { + let (va, vb, vd); + let vc; + if matches!(instr.opcode, PpcOpcode::vperm128) { + va = instr.va128(); + vb = instr.vb128(); + vd = instr.vd128(); + // For vperm128, the permutation control is in vC (third source) + // which is typically encoded via a different field + vc = instr.vd128(); // vperm128 uses vD as permute mask + } else { + va = instr.ra(); + vb = instr.rb(); + vd = instr.rd(); + vc = instr.rc(); + } + let a_bytes = ctx.vr[va].as_bytes(); + let b_bytes = ctx.vr[vb].as_bytes(); + let c_bytes = ctx.vr[vc].as_bytes(); + let mut r = [0u8; 16]; + for i in 0..16 { + let idx = (c_bytes[i] & 0x1F) as usize; + r[i] = if idx < 16 { a_bytes[idx] } else { b_bytes[idx - 16] }; + } + ctx.vr[vd] = xenia_types::Vec128::from_bytes(r); + ctx.pc += 4; + } + PpcOpcode::vsldoi => { + let a_bytes = ctx.vr[instr.ra()].as_bytes(); + let b_bytes = ctx.vr[instr.rb()].as_bytes(); + let sh = ((instr.raw >> 6) & 0xF) as usize; // SH field bits 6-9 + let mut concat = [0u8; 32]; + concat[..16].copy_from_slice(&a_bytes); + concat[16..].copy_from_slice(&b_bytes); + let mut r = [0u8; 16]; + r.copy_from_slice(&concat[sh..sh + 16]); + ctx.vr[instr.rd()] = xenia_types::Vec128::from_bytes(r); + ctx.pc += 4; + } + PpcOpcode::vsldoi128 => { + let a_bytes = ctx.vr[instr.va128()].as_bytes(); + let b_bytes = ctx.vr[instr.vb128()].as_bytes(); + let sh = ((instr.raw >> 6) & 0x7) as usize | (((instr.raw >> 4) & 0x1) as usize) << 3; // extract shift + let mut concat = [0u8; 32]; + concat[..16].copy_from_slice(&a_bytes); + concat[16..].copy_from_slice(&b_bytes); + let mut r = [0u8; 16]; + let sh = sh.min(16); + r.copy_from_slice(&concat[sh..sh + 16]); + ctx.vr[instr.vd128()] = xenia_types::Vec128::from_bytes(r); + ctx.pc += 4; + } + PpcOpcode::vspltw => { + let uimm = ((instr.raw >> 16) & 0x3) as usize; // UIMM (2 bits for word index) + let b = ctx.vr[instr.rb()].as_u32x4(); + let val = b[uimm]; + ctx.vr[instr.rd()] = xenia_types::Vec128::from_u32x4(val, val, val, val); + ctx.pc += 4; + } + PpcOpcode::vspltw128 => { + let uimm = ((instr.raw >> 16) & 0x3) as usize; + let b = ctx.vr[instr.vb128()].as_u32x4(); + let val = b[uimm]; + ctx.vr[instr.vd128()] = xenia_types::Vec128::from_u32x4(val, val, val, val); + ctx.pc += 4; + } + PpcOpcode::vsplth => { + let uimm = ((instr.raw >> 16) & 0x7) as usize; + let b = ctx.vr[instr.rb()].as_u16x8(); + let val = b[uimm]; + ctx.vr[instr.rd()] = xenia_types::Vec128::from_u16x8_array([val; 8]); + ctx.pc += 4; + } + PpcOpcode::vspltb => { + let uimm = ((instr.raw >> 16) & 0xF) as usize; + let b = ctx.vr[instr.rb()].as_bytes(); + let val = b[uimm]; + ctx.vr[instr.rd()] = xenia_types::Vec128::from_bytes([val; 16]); + ctx.pc += 4; + } + PpcOpcode::vspltisw | PpcOpcode::vspltisw128 => { + let simm = ((instr.raw >> 16) & 0x1F) as i32; + let simm = if simm & 0x10 != 0 { simm | !0x1F } else { simm }; // sign extend 5-bit + let val = simm as u32; + let vd = if matches!(instr.opcode, PpcOpcode::vspltisw128) { instr.vd128() } else { instr.rd() }; + ctx.vr[vd] = xenia_types::Vec128::from_u32x4(val, val, val, val); + ctx.pc += 4; + } + PpcOpcode::vspltisb => { + let simm = ((instr.raw >> 16) & 0x1F) as i8; + let simm = if simm & 0x10 != 0 { simm | !0x1F } else { simm }; + ctx.vr[instr.rd()] = xenia_types::Vec128::from_bytes([simm as u8; 16]); + ctx.pc += 4; + } + PpcOpcode::vspltish => { + let simm = ((instr.raw >> 16) & 0x1F) as i16; + let simm = if simm & 0x10 != 0 { simm | !0x1F } else { simm }; + ctx.vr[instr.rd()] = xenia_types::Vec128::from_u16x8_array([simm as u16; 8]); + ctx.pc += 4; + } + + // ===== VMX: Merge/Shuffle ===== + PpcOpcode::vmrghw | PpcOpcode::vmrghw128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_u32x4(); + let b = ctx.vr[vb].as_u32x4(); + // Merge high words: [a0, b0, a1, b1] + ctx.vr[vd] = xenia_types::Vec128::from_u32x4(a[0], b[0], a[1], b[1]); + ctx.pc += 4; + } + PpcOpcode::vmrglw | PpcOpcode::vmrglw128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_u32x4(); + let b = ctx.vr[vb].as_u32x4(); + // Merge low words: [a2, b2, a3, b3] + ctx.vr[vd] = xenia_types::Vec128::from_u32x4(a[2], b[2], a[3], b[3]); + ctx.pc += 4; + } + + // ===== VMX: Integer Arithmetic ===== + PpcOpcode::vadduwm => { + let a = ctx.vr[instr.ra()].as_u32x4(); + let b = ctx.vr[instr.rb()].as_u32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { r[i] = a[i].wrapping_add(b[i]); } + ctx.vr[instr.rd()] = xenia_types::Vec128::from_u32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vsubuwm => { + let a = ctx.vr[instr.ra()].as_u32x4(); + let b = ctx.vr[instr.rb()].as_u32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { r[i] = a[i].wrapping_sub(b[i]); } + ctx.vr[instr.rd()] = xenia_types::Vec128::from_u32x4_array(r); + ctx.pc += 4; + } + + // ===== VMX: Shift ===== + PpcOpcode::vslw | PpcOpcode::vslw128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_u32x4(); + let b = ctx.vr[vb].as_u32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { + let sh = b[i] & 0x1F; + r[i] = a[i] << sh; + } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vsrw | PpcOpcode::vsrw128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_u32x4(); + let b = ctx.vr[vb].as_u32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { + let sh = b[i] & 0x1F; + r[i] = a[i] >> sh; + } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vsraw | PpcOpcode::vsraw128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_u32x4(); + let b = ctx.vr[vb].as_u32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { + let sh = b[i] & 0x1F; + r[i] = (a[i] as i32 >> sh) as u32; + } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vrlw | PpcOpcode::vrlw128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_u32x4(); + let b = ctx.vr[vb].as_u32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { + let sh = b[i] & 0x1F; + r[i] = a[i].rotate_left(sh); + } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + ctx.pc += 4; + } + + // VMX: Round/Convert + PpcOpcode::vrfiz | PpcOpcode::vrfiz128 => { + let vb = if matches!(instr.opcode, PpcOpcode::vrfiz128) { instr.vb128() } else { instr.rb() }; + let vd = if matches!(instr.opcode, PpcOpcode::vrfiz128) { instr.vd128() } else { instr.rd() }; + let b = ctx.vr[vb].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = b[i].trunc(); } + ctx.vr[vd] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vrfin | PpcOpcode::vrfin128 => { + let vb = if matches!(instr.opcode, PpcOpcode::vrfin128) { instr.vb128() } else { instr.rb() }; + let vd = if matches!(instr.opcode, PpcOpcode::vrfin128) { instr.vd128() } else { instr.rd() }; + let b = ctx.vr[vb].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = b[i].round(); } + ctx.vr[vd] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vrfip | PpcOpcode::vrfip128 => { + let vb = if matches!(instr.opcode, PpcOpcode::vrfip128) { instr.vb128() } else { instr.rb() }; + let vd = if matches!(instr.opcode, PpcOpcode::vrfip128) { instr.vd128() } else { instr.rd() }; + let b = ctx.vr[vb].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = b[i].ceil(); } + ctx.vr[vd] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + PpcOpcode::vrfim | PpcOpcode::vrfim128 => { + let vb = if matches!(instr.opcode, PpcOpcode::vrfim128) { instr.vb128() } else { instr.rb() }; + let vd = if matches!(instr.opcode, PpcOpcode::vrfim128) { instr.vd128() } else { instr.rd() }; + let b = ctx.vr[vb].as_f32x4(); + let mut r = [0f32; 4]; + for i in 0..4 { r[i] = b[i].floor(); } + ctx.vr[vd] = xenia_types::Vec128::from_f32x4_array(r); + ctx.pc += 4; + } + + // VMX: MFVSCR/MTVSCR + PpcOpcode::mfvscr => { + ctx.vr[instr.rd()] = xenia_types::Vec128::from_u32x4(0, 0, 0, ctx.vscr_sat as u32); + ctx.pc += 4; + } + PpcOpcode::mtvscr => { + let val = ctx.vr[instr.rb()].as_u32x4(); + ctx.vscr_sat = (val[3] & 1) as u8; + ctx.pc += 4; + } + + // ===== VMX: lvsl/lvsr (generate permute vectors) ===== + PpcOpcode::lvsl | PpcOpcode::lvsl128 => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]); + let sh = (ea & 0xF) as u8; + let mut r = [0u8; 16]; + for i in 0..16 { r[i] = sh + i as u8; } + let vd = if matches!(instr.opcode, PpcOpcode::lvsl128) { instr.vd128() } else { instr.rd() }; + ctx.vr[vd] = xenia_types::Vec128::from_bytes(r); + ctx.pc += 4; + } + PpcOpcode::lvsr | PpcOpcode::lvsr128 => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]); + let sh = (ea & 0xF) as u8; + let mut r = [0u8; 16]; + for i in 0..16 { r[i] = (16 - sh) + i as u8; } + let vd = if matches!(instr.opcode, PpcOpcode::lvsr128) { instr.vd128() } else { instr.rd() }; + ctx.vr[vd] = xenia_types::Vec128::from_bytes(r); + ctx.pc += 4; + } + + // ===== VMX: Integer compare ===== + PpcOpcode::vcmpequw | PpcOpcode::vcmpequw128 => { + let (va, vb, vd) = vmx_reg_triple(instr); + let a = ctx.vr[va].as_u32x4(); + let b = ctx.vr[vb].as_u32x4(); + let mut r = [0u32; 4]; + for i in 0..4 { r[i] = if a[i] == b[i] { 0xFFFF_FFFF } else { 0 }; } + ctx.vr[vd] = xenia_types::Vec128::from_u32x4_array(r); + if instr.rc_bit() { update_cr6_from_vmask(&r, ctx); } + ctx.pc += 4; + } + + // ===== FPU: Arithmetic ===== + PpcOpcode::faddx => { + ctx.fpr[instr.rd()] = ctx.fpr[instr.ra()] + ctx.fpr[instr.rb()]; + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::faddsx => { + ctx.fpr[instr.rd()] = to_single(ctx.fpr[instr.ra()] + ctx.fpr[instr.rb()]); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fsubx => { + ctx.fpr[instr.rd()] = ctx.fpr[instr.ra()] - ctx.fpr[instr.rb()]; + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fsubsx => { + ctx.fpr[instr.rd()] = to_single(ctx.fpr[instr.ra()] - ctx.fpr[instr.rb()]); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fmulx => { + // A-form: frD = frA * frC (frC is at rc() field, bits 21-25) + ctx.fpr[instr.rd()] = ctx.fpr[instr.ra()] * ctx.fpr[instr.rc()]; + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fmulsx => { + ctx.fpr[instr.rd()] = to_single(ctx.fpr[instr.ra()] * ctx.fpr[instr.rc()]); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fdivx => { + ctx.fpr[instr.rd()] = ctx.fpr[instr.ra()] / ctx.fpr[instr.rb()]; + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fdivsx => { + ctx.fpr[instr.rd()] = to_single(ctx.fpr[instr.ra()] / ctx.fpr[instr.rb()]); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + + // ===== FPU: Multiply-Add ===== + PpcOpcode::fmaddx => { + // frD = (frA * frC) + frB + ctx.fpr[instr.rd()] = ctx.fpr[instr.ra()].mul_add(ctx.fpr[instr.rc()], ctx.fpr[instr.rb()]); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fmaddsx => { + ctx.fpr[instr.rd()] = to_single(ctx.fpr[instr.ra()].mul_add(ctx.fpr[instr.rc()], ctx.fpr[instr.rb()])); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fmsubx => { + // frD = (frA * frC) - frB + ctx.fpr[instr.rd()] = ctx.fpr[instr.ra()].mul_add(ctx.fpr[instr.rc()], -ctx.fpr[instr.rb()]); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fmsubsx => { + ctx.fpr[instr.rd()] = to_single(ctx.fpr[instr.ra()].mul_add(ctx.fpr[instr.rc()], -ctx.fpr[instr.rb()])); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fnmaddx => { + // frD = -((frA * frC) + frB) + ctx.fpr[instr.rd()] = -(ctx.fpr[instr.ra()].mul_add(ctx.fpr[instr.rc()], ctx.fpr[instr.rb()])); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fnmaddsx => { + ctx.fpr[instr.rd()] = to_single(-(ctx.fpr[instr.ra()].mul_add(ctx.fpr[instr.rc()], ctx.fpr[instr.rb()]))); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fnmsubx => { + // frD = -((frA * frC) - frB) + ctx.fpr[instr.rd()] = -(ctx.fpr[instr.ra()].mul_add(ctx.fpr[instr.rc()], -ctx.fpr[instr.rb()])); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fnmsubsx => { + ctx.fpr[instr.rd()] = to_single(-(ctx.fpr[instr.ra()].mul_add(ctx.fpr[instr.rc()], -ctx.fpr[instr.rb()]))); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + + // ===== FPU: Move/Sign ===== + PpcOpcode::fmrx => { + ctx.fpr[instr.rd()] = ctx.fpr[instr.rb()]; + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fabsx => { + ctx.fpr[instr.rd()] = ctx.fpr[instr.rb()].abs(); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fnegx => { + ctx.fpr[instr.rd()] = -ctx.fpr[instr.rb()]; + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fnabsx => { + ctx.fpr[instr.rd()] = -(ctx.fpr[instr.rb()].abs()); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + + // ===== FPU: Select ===== + PpcOpcode::fselx => { + // frD = if frA >= 0.0 then frC else frB + ctx.fpr[instr.rd()] = if ctx.fpr[instr.ra()] >= 0.0 { + ctx.fpr[instr.rc()] + } else { + ctx.fpr[instr.rb()] + }; + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + + // ===== FPU: Square root / Reciprocal ===== + PpcOpcode::fsqrtx => { + ctx.fpr[instr.rd()] = ctx.fpr[instr.rb()].sqrt(); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fsqrtsx => { + ctx.fpr[instr.rd()] = to_single(ctx.fpr[instr.rb()].sqrt()); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fresx => { + // Single-precision reciprocal estimate: frD = 1.0 / frB + ctx.fpr[instr.rd()] = to_single(1.0 / ctx.fpr[instr.rb()]); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::frsqrtex => { + // Reciprocal square root estimate: frD = 1.0 / sqrt(frB) + ctx.fpr[instr.rd()] = 1.0 / ctx.fpr[instr.rb()].sqrt(); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + + // ===== FPU: Rounding/Conversion ===== + PpcOpcode::frspx => { + // Round to single precision + ctx.fpr[instr.rd()] = to_single(ctx.fpr[instr.rb()]); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fcfidx => { + // Convert from integer doubleword: frD = (double)(int64_t)frD_as_bits + let bits = ctx.fpr[instr.rb()].to_bits(); + ctx.fpr[instr.rd()] = bits as i64 as f64; + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fctidx => { + // Convert to integer doubleword (round per FPSCR[RN]) + let val = ctx.fpr[instr.rb()]; + let result = if val.is_nan() { + 0x8000_0000_0000_0000u64 + } else { + let rounded = val.round(); + (rounded as i64) as u64 + }; + ctx.fpr[instr.rd()] = f64::from_bits(result); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fctidzx => { + // Convert to integer doubleword (round toward zero) + let val = ctx.fpr[instr.rb()]; + let result = if val.is_nan() { + 0x8000_0000_0000_0000u64 + } else { + (val as i64) as u64 + }; + ctx.fpr[instr.rd()] = f64::from_bits(result); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fctiwx => { + // Convert to integer word (round per FPSCR[RN]) + let val = ctx.fpr[instr.rb()]; + let result = if val.is_nan() { + 0x8000_0000u64 + } else { + let rounded = val.round(); + let clamped = rounded.clamp(i32::MIN as f64, i32::MAX as f64); + (clamped as i32 as u32) as u64 + }; + ctx.fpr[instr.rd()] = f64::from_bits(result); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::fctiwzx => { + // Convert to integer word (round toward zero) -- most common + let val = ctx.fpr[instr.rb()]; + let result = if val.is_nan() { + 0x8000_0000u64 + } else { + let clamped = val.clamp(i32::MIN as f64, i32::MAX as f64); + (clamped as i32 as u32) as u64 + }; + ctx.fpr[instr.rd()] = f64::from_bits(result); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + + // ===== FPU: Compare ===== + PpcOpcode::fcmpu => { + let fra = ctx.fpr[instr.ra()]; + let frb = ctx.fpr[instr.rb()]; + let crfd = instr.crfd(); + if fra.is_nan() || frb.is_nan() { + ctx.cr[crfd].lt = false; + ctx.cr[crfd].gt = false; + ctx.cr[crfd].eq = false; + ctx.cr[crfd].so = true; + } else if fra < frb { + ctx.cr[crfd].lt = true; + ctx.cr[crfd].gt = false; + ctx.cr[crfd].eq = false; + ctx.cr[crfd].so = false; + } else if fra > frb { + ctx.cr[crfd].lt = false; + ctx.cr[crfd].gt = true; + ctx.cr[crfd].eq = false; + ctx.cr[crfd].so = false; + } else { + ctx.cr[crfd].lt = false; + ctx.cr[crfd].gt = false; + ctx.cr[crfd].eq = true; + ctx.cr[crfd].so = false; + } + ctx.pc += 4; + } + PpcOpcode::fcmpo => { + // Same as fcmpu but sets FPSCR exception bits for QNaN (not modeled yet) + let fra = ctx.fpr[instr.ra()]; + let frb = ctx.fpr[instr.rb()]; + let crfd = instr.crfd(); + if fra.is_nan() || frb.is_nan() { + ctx.cr[crfd].lt = false; + ctx.cr[crfd].gt = false; + ctx.cr[crfd].eq = false; + ctx.cr[crfd].so = true; + } else if fra < frb { + ctx.cr[crfd].lt = true; + ctx.cr[crfd].gt = false; + ctx.cr[crfd].eq = false; + ctx.cr[crfd].so = false; + } else if fra > frb { + ctx.cr[crfd].lt = false; + ctx.cr[crfd].gt = true; + ctx.cr[crfd].eq = false; + ctx.cr[crfd].so = false; + } else { + ctx.cr[crfd].lt = false; + ctx.cr[crfd].gt = false; + ctx.cr[crfd].eq = true; + ctx.cr[crfd].so = false; + } + ctx.pc += 4; + } + + // ===== FPU: Status/Control ===== + PpcOpcode::mffsx => { + // Move from FPSCR: frD = FPSCR as double (low 32 bits) + ctx.fpr[instr.rd()] = f64::from_bits(ctx.fpscr as u64); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::mtfsfx => { + // Move to FPSCR fields: fm mask in bits 7-14, frB value + let fm = ((instr.raw >> 17) & 0xFF) as u32; + let val = ctx.fpr[instr.rb()].to_bits() as u32; + let mut mask = 0u32; + for i in 0..8 { + if fm & (1 << (7 - i)) != 0 { + mask |= 0xF << (28 - i * 4); + } + } + ctx.fpscr = (ctx.fpscr & !mask) | (val & mask); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::mtfsb0x => { + // Clear FPSCR bit crbd + let bit = instr.crbd(); + ctx.fpscr &= !(1 << (31 - bit as u32)); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::mtfsb1x => { + // Set FPSCR bit crbd + let bit = instr.crbd(); + ctx.fpscr |= 1 << (31 - bit as u32); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + PpcOpcode::mtfsfix => { + // Move to FPSCR field immediate: crfD = IMM (4 bits) + let crfd = instr.crfd(); + let imm = ((instr.raw >> 12) & 0xF) as u32; + let shift = 28 - crfd as u32 * 4; + ctx.fpscr = (ctx.fpscr & !(0xF << shift)) | (imm << shift); + if instr.rc_bit() { update_cr1_from_fpscr(ctx); } + ctx.pc += 4; + } + + // Anything not yet implemented + _ => { + tracing::warn!("Unimplemented opcode at {:#010x}: {:?} [{:08X}]", ctx.pc, instr.opcode, instr.raw); + ctx.pc += 4; + return StepResult::Unimplemented(instr.opcode); + } + } + StepResult::Continue +} + +/// Helper for CR logical operations. +fn cr_logical(ctx: &mut PpcContext, instr: &DecodedInstr, op: fn(bool, bool) -> bool) { + let a = ctx.get_cr_bit(instr.crba()); + let b = ctx.get_cr_bit(instr.crbb()); + ctx.set_cr_bit(instr.crbd(), op(a, b)); +} + +/// Generate 32-bit rotate mask for rlwinm/rlwimi/rlwnm. +fn rlw_mask(mb: u32, me: u32) -> u32 { + if mb <= me { + (u32::MAX >> mb) & (u32::MAX << (31 - me)) + } else { + (u32::MAX >> mb) | (u32::MAX << (31 - me)) + } +} + +/// Generate 64-bit mask clearing bits 0..mb-1 (left mask for rldicl). +fn rld_mask_left(mb: u32) -> u64 { + if mb == 0 { u64::MAX } else { u64::MAX >> mb } +} + +/// Generate 64-bit mask clearing bits me+1..63 (right mask for rldicr). +fn rld_mask_right(me: u32) -> u64 { + if me >= 63 { u64::MAX } else { u64::MAX << (63 - me) } +} + +/// Extract VMX register indices, handling both standard (opcode 4) and 128-bit forms. +#[inline] +fn vmx_reg_triple(instr: &DecodedInstr) -> (usize, usize, usize) { + // Check if this is a VMX128 form (opcode 4 with extended register fields) + // Standard Altivec: vD=rd, vA=ra, vB=rb + // VMX128: vD=vd128, vA=va128, vB=vb128 + let is_128 = matches!( + instr.opcode, + PpcOpcode::vand128 | PpcOpcode::vandc128 | PpcOpcode::vor128 | + PpcOpcode::vxor128 | PpcOpcode::vnor128 | PpcOpcode::vsel128 | + PpcOpcode::vcmpeqfp128 | PpcOpcode::vcmpgefp128 | PpcOpcode::vcmpgtfp128 | + PpcOpcode::vmrghw128 | PpcOpcode::vmrglw128 | + PpcOpcode::vslw128 | PpcOpcode::vsrw128 | PpcOpcode::vsraw128 | PpcOpcode::vrlw128 | + PpcOpcode::vcmpequw128 + ); + if is_128 { + (instr.va128(), instr.vb128(), instr.vd128()) + } else { + (instr.ra(), instr.rb(), instr.rd()) + } +} + +/// Update CR6 from vector compare result mask (used when Rc=1 on vector compares). +/// CR6: bit 0 (LT) = all elements true, bit 2 (EQ) = all elements false +#[inline] +fn update_cr6_from_vmask(r: &[u32; 4], ctx: &mut PpcContext) { + let all_true = r.iter().all(|&v| v == 0xFFFF_FFFF); + let all_false = r.iter().all(|&v| v == 0); + ctx.cr[6].lt = all_true; + ctx.cr[6].gt = false; + ctx.cr[6].eq = all_false; + ctx.cr[6].so = false; +} + +/// Round a double to single precision and back (matches xenia's ToSingle). +#[inline] +fn to_single(val: f64) -> f64 { + val as f32 as f64 +} + +/// Update CR1 from FPSCR (used when Rc=1 on FPU instructions). +/// CR1 = FPSCR[FX, FEX, VX, OX] (bits 0-3). +#[inline] +fn update_cr1_from_fpscr(ctx: &mut PpcContext) { + ctx.cr[1].lt = (ctx.fpscr >> 31) & 1 != 0; // FX + ctx.cr[1].gt = (ctx.fpscr >> 30) & 1 != 0; // FEX + ctx.cr[1].eq = (ctx.fpscr >> 29) & 1 != 0; // VX + ctx.cr[1].so = (ctx.fpscr >> 28) & 1 != 0; // OX +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Simple test memory (64KB) + struct TestMem { + data: Vec, + } + + impl TestMem { + fn new() -> Self { + Self { data: vec![0; 65536] } + } + } + + impl MemoryAccess for TestMem { + fn read_u8(&self, addr: u32) -> u8 { self.data[addr as usize] } + fn read_u16(&self, addr: u32) -> u16 { + let a = addr as usize; + u16::from_be_bytes([self.data[a], self.data[a+1]]) + } + fn read_u32(&self, addr: u32) -> u32 { + let a = addr as usize; + u32::from_be_bytes([self.data[a], self.data[a+1], self.data[a+2], self.data[a+3]]) + } + fn read_u64(&self, addr: u32) -> u64 { + let a = addr as usize; + u64::from_be_bytes([ + self.data[a], self.data[a+1], self.data[a+2], self.data[a+3], + self.data[a+4], self.data[a+5], self.data[a+6], self.data[a+7], + ]) + } + fn write_u8(&mut self, addr: u32, val: u8) { self.data[addr as usize] = val; } + fn write_u16(&mut self, addr: u32, val: u16) { + let a = addr as usize; + self.data[a..a+2].copy_from_slice(&val.to_be_bytes()); + } + fn write_u32(&mut self, addr: u32, val: u32) { + let a = addr as usize; + self.data[a..a+4].copy_from_slice(&val.to_be_bytes()); + } + fn write_u64(&mut self, addr: u32, val: u64) { + let a = addr as usize; + self.data[a..a+8].copy_from_slice(&val.to_be_bytes()); + } + fn translate(&self, _addr: u32) -> Option<*const u8> { None } + fn translate_mut(&mut self, _addr: u32) -> Option<*mut u8> { None } + } + + fn write_instr(mem: &mut TestMem, addr: u32, raw: u32) { + mem.write_u32(addr, raw); + } + + #[test] + fn test_addi() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // addi r3, r0, 42 + write_instr(&mut mem, 0, (14 << 26) | (3 << 21) | (0 << 16) | 42); + ctx.pc = 0; + let result = step(&mut ctx, &mut mem); + assert_eq!(result, StepResult::Continue); + assert_eq!(ctx.gpr[3], 42); + assert_eq!(ctx.pc, 4); + } + + #[test] + fn test_addis() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // addis r3, r0, 1 => r3 = 0x10000 + write_instr(&mut mem, 0, (15 << 26) | (3 << 21) | (0 << 16) | 1); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[3], 0x10000); + } + + #[test] + fn test_lwz_stw() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // Store 0xDEADBEEF at address 0x100 + mem.write_u32(0x100, 0xDEADBEEF); + // addi r1, r0, 0x100 + write_instr(&mut mem, 0, (14 << 26) | (1 << 21) | (0 << 16) | 0x100); + // lwz r3, 0(r1) + write_instr(&mut mem, 4, (32 << 26) | (3 << 21) | (1 << 16) | 0); + ctx.pc = 0; + step(&mut ctx, &mut mem); + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[3], 0xDEADBEEF); + } + + #[test] + fn test_branch() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // b +0x10 (from addr 0x100) + write_instr(&mut mem, 0x100, (18 << 26) | (4 << 2)); // LI=4, shifted=0x10 + ctx.pc = 0x100; + step(&mut ctx, &mut mem); + assert_eq!(ctx.pc, 0x110); + } + + #[test] + fn test_bl_updates_lr() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // bl +0x10 (from addr 0x200) + write_instr(&mut mem, 0x200, (18 << 26) | (4 << 2) | 1); // LK=1 + ctx.pc = 0x200; + step(&mut ctx, &mut mem); + assert_eq!(ctx.pc, 0x210); + assert_eq!(ctx.lr, 0x204); + } + + #[test] + fn test_cmp_and_bc() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 10; + // cmpi cr0, 0, r3, 10 (32-bit compare) + write_instr(&mut mem, 0, (11 << 26) | (0 << 23) | (0 << 21) | (3 << 16) | (10u32 & 0xFFFF)); + // bc 12,2,+8 (branch if CR0.EQ, bo=12, bi=2) + write_instr(&mut mem, 4, (16 << 26) | (12 << 21) | (2 << 16) | (2 << 2)); + ctx.pc = 0; + step(&mut ctx, &mut mem); // cmpi + assert!(ctx.cr[0].eq); + step(&mut ctx, &mut mem); // bc - should branch + assert_eq!(ctx.pc, 12); // 4 + 8 + } + + #[test] + fn test_rlwinm() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 0xFF00_FF00; + // rlwinm r4, r3, 8, 0, 31 (rotate left 8, full mask = shift left 8) + let raw = (21 << 26) | (3 << 21) | (4 << 16) | (8 << 11) | (0 << 6) | (31 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[4], 0x00FF_00FF); + } + + #[test] + fn test_ori_nop() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // ori r0, r0, 0 (NOP) + write_instr(&mut mem, 0, 0x60000000); + ctx.pc = 0; + ctx.gpr[0] = 0xDEAD; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[0], 0xDEAD); + assert_eq!(ctx.pc, 4); + } + + #[test] + fn test_fadd() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[1] = 3.14; + ctx.fpr[2] = 2.86; + // fadd f3, f1, f2: opcode 63, subop 21 (bits 1-5), frD=3, frA=1, frB=2 + // 63<<26 | 3<<21 | 1<<16 | 2<<11 | 21<<1 + let raw = (63 << 26) | (3 << 21) | (1 << 16) | (2 << 11) | (21 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert!((ctx.fpr[3] - 6.0).abs() < 1e-10); + } + + #[test] + fn test_fmul() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[1] = 3.0; + ctx.fpr[2] = 4.0; + // fmul f3, f1, f2: opcode 63, subop 25, frD=3, frA=1, frC=2 (bits 21-25) + // 63<<26 | 3<<21 | 1<<16 | 0<<11 | 2<<6 | 25<<1 + let raw = (63 << 26) | (3 << 21) | (1 << 16) | (0 << 11) | (2 << 6) | (25 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert!((ctx.fpr[3] - 12.0).abs() < 1e-10); + } + + #[test] + fn test_fcmpu() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[1] = 5.0; + ctx.fpr[2] = 3.0; + // fcmpu cr0, f1, f2: opcode 63, subop 0 (X-form), crfD=0, frA=1, frB=2 + // 63<<26 | 0<<23 | 0<<21 | 1<<16 | 2<<11 | 0<<1 + let raw = (63 << 26) | (0 << 23) | (0 << 21) | (1 << 16) | (2 << 11) | (0 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert!(ctx.cr[0].gt); // 5.0 > 3.0 + assert!(!ctx.cr[0].lt); + assert!(!ctx.cr[0].eq); + } + + #[test] + fn test_fctiwzx() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[1] = 42.7; + // fctiwz f2, f1: opcode 63, subop 15 (X-form), frD=2, frB=1 + // 63<<26 | 2<<21 | 0<<16 | 1<<11 | 15<<1 + let raw = (63 << 26) | (2 << 21) | (0 << 16) | (1 << 11) | (15 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + // Result stored as bits in FPR: should be 42 as int + let bits = ctx.fpr[2].to_bits(); + assert_eq!(bits as u32, 42); + } + + #[test] + fn test_fmadd() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[1] = 2.0; // frA + ctx.fpr[2] = 3.0; // frB (addend) + ctx.fpr[3] = 5.0; // frC (multiplier) + // fmadd f4, f1, f3, f2: frD=4, frA=1, frB=2, frC=3 + // opcode 63, subop 29 (bits 1-5) + // 63<<26 | 4<<21 | 1<<16 | 2<<11 | 3<<6 | 29<<1 + let raw = (63 << 26) | (4 << 21) | (1 << 16) | (2 << 11) | (3 << 6) | (29 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + // (2.0 * 5.0) + 3.0 = 13.0 + assert!((ctx.fpr[4] - 13.0).abs() < 1e-10); + } +} diff --git a/crates/xenia-cpu/src/lib.rs b/crates/xenia-cpu/src/lib.rs new file mode 100644 index 0000000..b84cb73 --- /dev/null +++ b/crates/xenia-cpu/src/lib.rs @@ -0,0 +1,9 @@ +pub mod context; +pub mod decoder; +pub mod disasm; +pub mod interpreter; +pub mod opcode; + +pub use context::PpcContext; +pub use decoder::decode; +pub use opcode::PpcOpcode; diff --git a/crates/xenia-cpu/src/opcode.rs b/crates/xenia-cpu/src/opcode.rs new file mode 100644 index 0000000..01fb77c --- /dev/null +++ b/crates/xenia-cpu/src/opcode.rs @@ -0,0 +1,196 @@ +/// All PPC opcodes supported by the Xbox 360, including VMX128 extensions. +/// Directly mirrors the C++ PPCOpcode enum from ppc_opcode.h. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +#[allow(non_camel_case_types)] +pub enum PpcOpcode { + // ALU + addcx, addex, addi, addic, addicx, addis, addmex, addx, addzex, + andcx, andisx, andix, andx, + // Branch + bcctrx, bclrx, bcx, bx, + // Compare + cmp, cmpi, cmpl, cmpli, + // Count leading zeros + cntlzdx, cntlzwx, + // Condition register + crand, crandc, creqv, crnand, crnor, cror, crorc, crxor, + // Data cache + dcbf, dcbi, dcbst, dcbt, dcbtst, dcbz, dcbz128, + // Division + divdux, divdx, divwux, divwx, + // Sync/barrier + eieio, + // Logical + eqvx, extsbx, extshx, extswx, + // FPU + fabsx, faddsx, faddx, fcfidx, fcmpo, fcmpu, fctidx, fctidzx, fctiwx, fctiwzx, + fdivsx, fdivx, fmaddsx, fmaddx, fmrx, fmsubsx, fmsubx, fmulsx, fmulx, + fnabsx, fnegx, fnmaddsx, fnmaddx, fnmsubsx, fnmsubx, fresx, frspx, frsqrtex, + fselx, fsqrtsx, fsqrtx, fsubsx, fsubx, + // Instruction cache + icbi, isync, + // Load byte + lbz, lbzu, lbzux, lbzx, + // Load doubleword + ld, ldarx, ldbrx, ldu, ldux, ldx, + // Load float + lfd, lfdu, lfdux, lfdx, lfs, lfsu, lfsux, lfsx, + // Load halfword + lha, lhau, lhaux, lhax, lhbrx, lhz, lhzu, lhzux, lhzx, + // Load multiple/string + lmw, lswi, lswx, + // Load vector + lvebx, lvehx, lvewx, lvewx128, lvlx, lvlx128, lvlxl, lvlxl128, + lvrx, lvrx128, lvrxl, lvrxl128, + lvsl, lvsl128, lvsr, lvsr128, + lvx, lvx128, lvxl, lvxl128, + // Load word + lwa, lwarx, lwaux, lwax, lwbrx, lwz, lwzu, lwzux, lwzx, + // Move CR + mcrf, mcrfs, mcrxr, + // Move from special + mfcr, mffsx, mfmsr, mfspr, mftb, mfvscr, + // Move to special + mtcrf, mtfsb0x, mtfsb1x, mtfsfix, mtfsfx, mtmsr, mtmsrd, mtspr, mtvscr, + // Multiply + mulhdux, mulhdx, mulhwux, mulhwx, mulldx, mulli, mullwx, + // Logical + nandx, negx, norx, orcx, ori, oris, orx, + // Rotate + rldclx, rldcrx, rldiclx, rldicrx, rldicx, rldimix, rlwimix, rlwinmx, rlwnmx, + // System call + sc, + // Shift + sldx, slwx, sradix, sradx, srawix, srawx, srdx, srwx, + // Store byte + stb, stbu, stbux, stbx, + // Store doubleword + std, stdbrx, stdcx, stdu, stdux, stdx, + // Store float + stfd, stfdu, stfdux, stfdx, stfiwx, stfs, stfsu, stfsux, stfsx, + // Store halfword + sth, sthbrx, sthu, sthux, sthx, + // Store multiple/string + stmw, stswi, stswx, + // Store vector + stvebx, stvehx, stvewx, stvewx128, stvlx, stvlx128, stvlxl, stvlxl128, + stvrx, stvrx128, stvrxl, stvrxl128, + stvx, stvx128, stvxl, stvxl128, + // Store word + stw, stwbrx, stwcx, stwu, stwux, stwx, + // Subtract + subfcx, subfex, subficx, subfmex, subfx, subfzex, + // Sync + sync, + // Trap + td, tdi, tw, twi, + // VMX integer + vaddcuw, vaddfp, vaddfp128, vaddsbs, vaddshs, vaddsws, + vaddubm, vaddubs, vadduhm, vadduhs, vadduwm, vadduws, + vand, vand128, vandc, vandc128, + vavgsb, vavgsh, vavgsw, vavgub, vavguh, vavguw, + vcfpsxws128, vcfpuxws128, vcfsx, vcfux, + vcmpbfp, vcmpbfp128, vcmpeqfp, vcmpeqfp128, + vcmpequb, vcmpequh, vcmpequw, vcmpequw128, + vcmpgefp, vcmpgefp128, vcmpgtfp, vcmpgtfp128, + vcmpgtsb, vcmpgtsh, vcmpgtsw, vcmpgtub, vcmpgtuh, vcmpgtuw, + vcsxwfp128, vctsxs, vctuxs, vcuxwfp128, + vexptefp, vexptefp128, vlogefp, vlogefp128, + vmaddcfp128, vmaddfp, vmaddfp128, + vmaxfp, vmaxfp128, vmaxsb, vmaxsh, vmaxsw, vmaxub, vmaxuh, vmaxuw, + vmhaddshs, vmhraddshs, + vminfp, vminfp128, vminsb, vminsh, vminsw, vminub, vminuh, vminuw, + vmladduhm, + vmrghb, vmrghh, vmrghw, vmrghw128, vmrglb, vmrglh, vmrglw, vmrglw128, + vmsum3fp128, vmsum4fp128, + vmsummbm, vmsumshm, vmsumshs, vmsumubm, vmsumuhm, vmsumuhs, + vmulesb, vmulesh, vmuleub, vmuleuh, vmulfp128, + vmulosb, vmulosh, vmuloub, vmulouh, + vnmsubfp, vnmsubfp128, vnor, vnor128, + vor, vor128, + vperm, vperm128, vpermwi128, vpkd3d128, + vpkpx, vpkshss, vpkshss128, vpkshus, vpkshus128, + vpkswss, vpkswss128, vpkswus, vpkswus128, + vpkuhum, vpkuhum128, vpkuhus, vpkuhus128, + vpkuwum, vpkuwum128, vpkuwus, vpkuwus128, + vrefp, vrefp128, + vrfim, vrfim128, vrfin, vrfin128, vrfip, vrfip128, vrfiz, vrfiz128, + vrlb, vrlh, vrlimi128, vrlw, vrlw128, + vrsqrtefp, vrsqrtefp128, + vsel, vsel128, + vsl, vslb, vsldoi, vsldoi128, vslh, vslo, vslo128, vslw, vslw128, + vspltb, vsplth, vspltisb, vspltish, vspltisw, vspltisw128, vspltw, vspltw128, + vsr, vsrab, vsrah, vsraw, vsraw128, vsrb, vsrh, vsro, vsro128, vsrw, vsrw128, + vsubcuw, vsubfp, vsubfp128, vsubsbs, vsubshs, vsubsws, + vsububm, vsububs, vsubuhm, vsubuhs, vsubuwm, vsubuws, + vsum2sws, vsum4sbs, vsum4shs, vsum4ubs, vsumsws, + vupkd3d128, vupkhpx, vupkhsb, vupkhsb128, vupkhsh, + vupklpx, vupklsb, vupklsb128, vupklsh, + vxor, vxor128, + // XOR immediate + xori, xoris, xorx, + // Invalid + Invalid, +} + +impl PpcOpcode { + /// Returns true if this opcode is a branch instruction. + pub fn is_branch(&self) -> bool { + matches!(self, Self::bx | Self::bcx | Self::bclrx | Self::bcctrx) + } + + /// Returns true if this opcode is a system call. + pub fn is_syscall(&self) -> bool { + matches!(self, Self::sc) + } + + /// Returns true if this is a load instruction. + pub fn is_load(&self) -> bool { + matches!(self, + Self::lbz | Self::lbzu | Self::lbzux | Self::lbzx | + Self::lhz | Self::lhzu | Self::lhzux | Self::lhzx | + Self::lha | Self::lhau | Self::lhaux | Self::lhax | + Self::lwz | Self::lwzu | Self::lwzux | Self::lwzx | + Self::lwa | Self::lwax | Self::lwaux | + Self::ld | Self::ldu | Self::ldux | Self::ldx | + Self::lfs | Self::lfsu | Self::lfsux | Self::lfsx | + Self::lfd | Self::lfdu | Self::lfdux | Self::lfdx | + Self::lhbrx | Self::lwbrx | Self::ldbrx | + Self::lmw | Self::lswi | Self::lswx | + Self::lwarx | Self::ldarx + ) + } + + /// Returns true if this is a store instruction. + pub fn is_store(&self) -> bool { + matches!(self, + Self::stb | Self::stbu | Self::stbux | Self::stbx | + Self::sth | Self::sthu | Self::sthux | Self::sthx | + Self::stw | Self::stwu | Self::stwux | Self::stwx | + Self::std | Self::stdu | Self::stdux | Self::stdx | + Self::stfs | Self::stfsu | Self::stfsux | Self::stfsx | + Self::stfd | Self::stfdu | Self::stfdux | Self::stfdx | + Self::sthbrx | Self::stwbrx | Self::stdbrx | + Self::stmw | Self::stswi | Self::stswx | + Self::stwcx | Self::stdcx | Self::stfiwx + ) + } + + pub fn name(&self) -> &'static str { + match self { + Self::Invalid => "invalid", + _ => { + // Use debug formatting to get the variant name + // This is a placeholder - in practice we'd have a lookup table + "?" + } + } + } +} + +impl std::fmt::Display for PpcOpcode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(self, f) + } +} diff --git a/crates/xenia-debugger/Cargo.toml b/crates/xenia-debugger/Cargo.toml new file mode 100644 index 0000000..e85a1c1 --- /dev/null +++ b/crates/xenia-debugger/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "xenia-debugger" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +xenia-memory = { workspace = true } +xenia-cpu = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } diff --git a/crates/xenia-debugger/src/breakpoint.rs b/crates/xenia-debugger/src/breakpoint.rs new file mode 100644 index 0000000..7559035 --- /dev/null +++ b/crates/xenia-debugger/src/breakpoint.rs @@ -0,0 +1,7 @@ +/// A code breakpoint at a specific guest address. +#[derive(Debug, Clone)] +pub struct Breakpoint { + pub addr: u32, + pub enabled: bool, + pub condition: Option, +} diff --git a/crates/xenia-debugger/src/lib.rs b/crates/xenia-debugger/src/lib.rs new file mode 100644 index 0000000..0cf6d2b --- /dev/null +++ b/crates/xenia-debugger/src/lib.rs @@ -0,0 +1,125 @@ +pub mod breakpoint; +pub mod trace; + +use std::collections::HashMap; +use xenia_cpu::context::PpcContext; +use xenia_memory::MemoryAccess; + +pub use breakpoint::Breakpoint; +pub use trace::TraceEntry; + +/// The debugger. Hooks into every instruction step for observation. +pub struct Debugger { + pub breakpoints: HashMap, + pub trace_log: Vec, + pub trace_enabled: bool, + pub max_trace_entries: usize, + pub paused: bool, + pub step_mode: StepMode, + break_pending: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StepMode { + /// Run freely until breakpoint or pause + Run, + /// Execute one instruction then pause + StepInto, + /// Run but break after current function returns (when LR changes) + StepOver { return_addr: u32 }, +} + +impl Debugger { + pub fn new() -> Self { + Self { + breakpoints: HashMap::new(), + trace_log: Vec::new(), + trace_enabled: true, + max_trace_entries: 100_000, + paused: true, // Start paused for debugging + step_mode: StepMode::StepInto, + break_pending: false, + } + } + + /// Called before each instruction executes. + pub fn pre_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) { + // Check breakpoints + if let Some(bp) = self.breakpoints.get(&ctx.pc) { + if bp.enabled { + self.break_pending = true; + tracing::info!("Breakpoint hit at {:#010x}", ctx.pc); + } + } + } + + /// Called after each instruction executes. + pub fn post_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) { + // Log to trace + if self.trace_enabled { + if self.trace_log.len() >= self.max_trace_entries { + self.trace_log.remove(0); + } + self.trace_log.push(TraceEntry { + pc: ctx.pc, + cycle: ctx.cycle_count, + gpr_snapshot: [ctx.gpr[0], ctx.gpr[1], ctx.gpr[3], ctx.gpr[4]], + lr: ctx.lr, + }); + } + + // Handle step mode + match self.step_mode { + StepMode::StepInto => { + self.break_pending = true; + } + StepMode::StepOver { return_addr } => { + if ctx.pc == return_addr { + self.break_pending = true; + } + } + StepMode::Run => {} + } + } + + /// Should we break execution? + pub fn should_break(&self) -> bool { + self.break_pending || self.paused + } + + /// Add a breakpoint at the given address. + pub fn add_breakpoint(&mut self, addr: u32) { + self.breakpoints.insert(addr, Breakpoint { addr, enabled: true, condition: None }); + } + + /// Remove a breakpoint. + pub fn remove_breakpoint(&mut self, addr: u32) { + self.breakpoints.remove(&addr); + } + + /// Continue execution. + pub fn continue_execution(&mut self) { + self.paused = false; + self.break_pending = false; + self.step_mode = StepMode::Run; + } + + /// Step one instruction. + pub fn step_into(&mut self) { + self.paused = false; + self.break_pending = false; + self.step_mode = StepMode::StepInto; + } + + /// Clear break state after handling. + pub fn acknowledge_break(&mut self) { + self.break_pending = false; + self.paused = true; + } +} + +impl Default for Debugger { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/xenia-debugger/src/trace.rs b/crates/xenia-debugger/src/trace.rs new file mode 100644 index 0000000..091c6c4 --- /dev/null +++ b/crates/xenia-debugger/src/trace.rs @@ -0,0 +1,9 @@ +/// A single entry in the instruction trace log. +#[derive(Debug, Clone)] +pub struct TraceEntry { + pub pc: u32, + pub cycle: u64, + /// Snapshot of key GPRs: [r0, r1(sp), r3(arg0/retval), r4(arg1)] + pub gpr_snapshot: [u64; 4], + pub lr: u64, +} diff --git a/crates/xenia-gpu/Cargo.toml b/crates/xenia-gpu/Cargo.toml new file mode 100644 index 0000000..fe02e00 --- /dev/null +++ b/crates/xenia-gpu/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "xenia-gpu" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +xenia-memory = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +byteorder = { workspace = true } diff --git a/crates/xenia-gpu/src/command_processor.rs b/crates/xenia-gpu/src/command_processor.rs new file mode 100644 index 0000000..ee854a3 --- /dev/null +++ b/crates/xenia-gpu/src/command_processor.rs @@ -0,0 +1,17 @@ +/// PM4 command processor stub. +/// Will parse the GPU command ring buffer and dispatch to render operations. +pub struct CommandProcessor { + pub enabled: bool, +} + +impl CommandProcessor { + pub fn new() -> Self { + Self { enabled: false } + } +} + +impl Default for CommandProcessor { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/xenia-gpu/src/lib.rs b/crates/xenia-gpu/src/lib.rs new file mode 100644 index 0000000..8adce9c --- /dev/null +++ b/crates/xenia-gpu/src/lib.rs @@ -0,0 +1,21 @@ +pub mod command_processor; +pub mod register_file; + +/// Stub GPU system for initial implementation. +pub struct GpuSystem { + pub register_file: register_file::RegisterFile, +} + +impl GpuSystem { + pub fn new() -> Self { + Self { + register_file: register_file::RegisterFile::new(), + } + } +} + +impl Default for GpuSystem { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/xenia-gpu/src/register_file.rs b/crates/xenia-gpu/src/register_file.rs new file mode 100644 index 0000000..24fc52e --- /dev/null +++ b/crates/xenia-gpu/src/register_file.rs @@ -0,0 +1,28 @@ +/// Xenos GPU register file. 0x6000 32-bit registers. +pub struct RegisterFile { + pub regs: Vec, +} + +impl RegisterFile { + pub fn new() -> Self { + Self { + regs: vec![0u32; 0x6000], + } + } + + pub fn read(&self, index: u32) -> u32 { + self.regs.get(index as usize).copied().unwrap_or(0) + } + + pub fn write(&mut self, index: u32, value: u32) { + if let Some(r) = self.regs.get_mut(index as usize) { + *r = value; + } + } +} + +impl Default for RegisterFile { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/xenia-hid/Cargo.toml b/crates/xenia-hid/Cargo.toml new file mode 100644 index 0000000..b47a0a5 --- /dev/null +++ b/crates/xenia-hid/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "xenia-hid" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/xenia-hid/src/lib.rs b/crates/xenia-hid/src/lib.rs new file mode 100644 index 0000000..576fc5f --- /dev/null +++ b/crates/xenia-hid/src/lib.rs @@ -0,0 +1,47 @@ +/// Human input device system stub. +pub struct InputSystem { + pub gamepad: GamepadState, +} + +#[derive(Default, Clone, Copy)] +pub struct GamepadState { + pub buttons: u16, + pub left_trigger: u8, + pub right_trigger: u8, + pub left_stick_x: i16, + pub left_stick_y: i16, + pub right_stick_x: i16, + pub right_stick_y: i16, +} + +/// Xbox 360 button flags +pub mod buttons { + pub const DPAD_UP: u16 = 0x0001; + pub const DPAD_DOWN: u16 = 0x0002; + pub const DPAD_LEFT: u16 = 0x0004; + pub const DPAD_RIGHT: u16 = 0x0008; + pub const START: u16 = 0x0010; + pub const BACK: u16 = 0x0020; + pub const LEFT_THUMB: u16 = 0x0040; + pub const RIGHT_THUMB: u16 = 0x0080; + pub const LEFT_SHOULDER: u16 = 0x0100; + pub const RIGHT_SHOULDER: u16 = 0x0200; + pub const A: u16 = 0x1000; + pub const B: u16 = 0x2000; + pub const X: u16 = 0x4000; + pub const Y: u16 = 0x8000; +} + +impl InputSystem { + pub fn new() -> Self { + Self { + gamepad: GamepadState::default(), + } + } +} + +impl Default for InputSystem { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/xenia-kernel/Cargo.toml b/crates/xenia-kernel/Cargo.toml new file mode 100644 index 0000000..50de041 --- /dev/null +++ b/crates/xenia-kernel/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "xenia-kernel" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +xenia-memory = { workspace = true } +xenia-cpu = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs new file mode 100644 index 0000000..15a8b5e --- /dev/null +++ b/crates/xenia-kernel/src/exports.rs @@ -0,0 +1,763 @@ +//! HLE kernel export implementations (xboxkrnl.exe). +//! Each export mirrors a function from xboxkrnl_table.inc. + +use crate::objects::KernelObject; +use crate::state::{KernelState, ModuleId}; +use xenia_cpu::PpcContext; +use xenia_memory::{GuestMemory, MemoryAccess}; + +pub fn register_exports(state: &mut KernelState) { + use ModuleId::Xboxkrnl; + + // Debug + state.register_export(Xboxkrnl, 0x01, "DbgBreakPoint", dbg_break_point); + state.register_export(Xboxkrnl, 0x03, "DbgPrint", dbg_print); + + // ExCreateThread and friends + state.register_export(Xboxkrnl, 0x0D, "ExCreateThread", ex_create_thread); + state.register_export(Xboxkrnl, 0x10, "ExGetXConfigSetting", ex_get_xconfig_setting); + state.register_export(Xboxkrnl, 0x15, "ExRegisterTitleTerminateNotification", stub_success); + state.register_export(Xboxkrnl, 0x19, "ExTerminateThread", ex_terminate_thread); + + // Hal + state.register_export(Xboxkrnl, 0x28, "HalReturnToFirmware", hal_return_to_firmware); + + // I/O + state.register_export(Xboxkrnl, 0x3C, "IoDismountVolumeByFileHandle", stub_success); + + // Ke* Threading/Sync + state.register_export(Xboxkrnl, 0x4D, "KeAcquireSpinLockAtRaisedIrql", stub_return_zero); + state.register_export(Xboxkrnl, 0x52, "KeBugCheck", ke_bug_check); + state.register_export(Xboxkrnl, 0x53, "KeBugCheckEx", ke_bug_check_ex); + state.register_export(Xboxkrnl, 0x5A, "KeDelayExecutionThread", stub_success); + state.register_export(Xboxkrnl, 0x5D, "KeEnableFpuExceptions", stub_success); + state.register_export(Xboxkrnl, 0x5F, "KeEnterCriticalRegion", stub_success); + state.register_export(Xboxkrnl, 0x66, "KeGetCurrentProcessType", ke_get_current_process_type); + state.register_export(Xboxkrnl, 0x6B, "KeLockL2", stub_success); + state.register_export(Xboxkrnl, 0x6C, "KeUnlockL2", stub_success); + state.register_export(Xboxkrnl, 0x74, "KeInitializeSemaphore", ke_initialize_semaphore); + state.register_export(Xboxkrnl, 0x7D, "KeLeaveCriticalRegion", stub_success); + state.register_export(Xboxkrnl, 0x81, "KeQueryBasePriorityThread", stub_return_zero); + state.register_export(Xboxkrnl, 0x83, "KeQueryPerformanceFrequency", ke_query_performance_frequency); + state.register_export(Xboxkrnl, 0x84, "KeQuerySystemTime", ke_query_system_time); + state.register_export(Xboxkrnl, 0x85, "KeRaiseIrqlToDpcLevel", stub_return_zero); + state.register_export(Xboxkrnl, 0x88, "KeReleaseSemaphore", stub_return_zero); + state.register_export(Xboxkrnl, 0x89, "KeReleaseSpinLockFromRaisedIrql", stub_success); + state.register_export(Xboxkrnl, 0x8F, "KeResetEvent", stub_return_zero); + state.register_export(Xboxkrnl, 0x92, "KeResumeThread", stub_return_zero); + state.register_export(Xboxkrnl, 0x97, "KeSetAffinityThread", stub_return_zero); + state.register_export(Xboxkrnl, 0x99, "KeSetBasePriorityThread", stub_return_zero); + state.register_export(Xboxkrnl, 0x9B, "KeSetCurrentStackPointers", stub_success); + state.register_export(Xboxkrnl, 0x9D, "KeSetEvent", stub_return_zero); + state.register_export(Xboxkrnl, 0xAE, "KeTryToAcquireSpinLockAtRaisedIrql", ke_try_acquire_spinlock); + state.register_export(Xboxkrnl, 0xAF, "KeWaitForMultipleObjects", stub_success); + state.register_export(Xboxkrnl, 0xB0, "KeWaitForSingleObject", stub_success); + state.register_export(Xboxkrnl, 0xB1, "KfAcquireSpinLock", stub_return_zero); + state.register_export(Xboxkrnl, 0xB3, "KfLowerIrql", stub_success); + state.register_export(Xboxkrnl, 0xB4, "KfReleaseSpinLock", stub_success); + state.register_export(Xboxkrnl, 0x0152, "KeTlsAlloc", ke_tls_alloc); + state.register_export(Xboxkrnl, 0x0153, "KeTlsFree", stub_success); + state.register_export(Xboxkrnl, 0x0154, "KeTlsGetValue", ke_tls_get_value); + state.register_export(Xboxkrnl, 0x0155, "KeTlsSetValue", ke_tls_set_value); + state.register_export(Xboxkrnl, 0x01DF, "KiApcNormalRoutineNop", stub_success); + + // Memory + state.register_export(Xboxkrnl, 0xBA, "MmAllocatePhysicalMemoryEx", mm_allocate_physical_memory_ex); + state.register_export(Xboxkrnl, 0xBB, "MmCreateKernelStack", mm_create_kernel_stack); + state.register_export(Xboxkrnl, 0xBC, "MmDeleteKernelStack", stub_success); + state.register_export(Xboxkrnl, 0xBD, "MmFreePhysicalMemory", stub_success); + state.register_export(Xboxkrnl, 0xBE, "MmGetPhysicalAddress", mm_get_physical_address); + state.register_export(Xboxkrnl, 0xC4, "MmQueryAddressProtect", mm_query_address_protect); + state.register_export(Xboxkrnl, 0xC6, "MmQueryStatistics", mm_query_statistics); + + // Nt* + state.register_export(Xboxkrnl, 0xCC, "NtAllocateVirtualMemory", nt_allocate_virtual_memory); + state.register_export(Xboxkrnl, 0xCD, "NtCancelTimer", stub_success); + state.register_export(Xboxkrnl, 0xCE, "NtClearEvent", stub_success); + state.register_export(Xboxkrnl, 0xCF, "NtClose", nt_close); + state.register_export(Xboxkrnl, 0xD1, "NtCreateEvent", nt_create_event); + state.register_export(Xboxkrnl, 0xD2, "NtCreateFile", nt_create_file); + state.register_export(Xboxkrnl, 0xD5, "NtCreateSemaphore", nt_create_semaphore); + state.register_export(Xboxkrnl, 0xD7, "NtCreateTimer", nt_create_timer); + state.register_export(Xboxkrnl, 0xD9, "NtDeviceIoControlFile", stub_success); + state.register_export(Xboxkrnl, 0xDA, "NtDuplicateObject", stub_success); + state.register_export(Xboxkrnl, 0xDB, "NtFlushBuffersFile", stub_success); + state.register_export(Xboxkrnl, 0xDC, "NtFreeVirtualMemory", stub_success); + state.register_export(Xboxkrnl, 0xDF, "NtOpenFile", nt_open_file); + state.register_export(Xboxkrnl, 0xE4, "NtQueryDirectoryFile", nt_query_directory_file); + state.register_export(Xboxkrnl, 0xE7, "NtQueryFullAttributesFile", nt_query_full_attributes_file); + state.register_export(Xboxkrnl, 0xE8, "NtQueryInformationFile", stub_success); + state.register_export(Xboxkrnl, 0xEE, "NtQueryVirtualMemory", stub_success); + state.register_export(Xboxkrnl, 0xEF, "NtQueryVolumeInformationFile", stub_success); + state.register_export(Xboxkrnl, 0xF0, "NtReadFile", nt_read_file); + state.register_export(Xboxkrnl, 0xF3, "NtReleaseSemaphore", stub_return_zero); + state.register_export(Xboxkrnl, 0xF5, "NtResumeThread", stub_return_zero); + state.register_export(Xboxkrnl, 0xF6, "NtSetEvent", stub_success); + state.register_export(Xboxkrnl, 0xF7, "NtSetInformationFile", stub_success); + state.register_export(Xboxkrnl, 0xFA, "NtSetTimerEx", stub_success); + state.register_export(Xboxkrnl, 0xFC, "NtSuspendThread", stub_return_zero); + state.register_export(Xboxkrnl, 0xFD, "NtWaitForSingleObjectEx", stub_success); + state.register_export(Xboxkrnl, 0xFE, "NtWaitForMultipleObjectsEx", stub_success); + state.register_export(Xboxkrnl, 0xFF, "NtWriteFile", nt_write_file); + state.register_export(Xboxkrnl, 0x0101, "NtYieldExecution", stub_success); + + // Object + state.register_export(Xboxkrnl, 0x0103, "ObCreateSymbolicLink", stub_success); + state.register_export(Xboxkrnl, 0x0104, "ObDeleteSymbolicLink", stub_success); + state.register_export(Xboxkrnl, 0x0105, "ObDereferenceObject", stub_success); + state.register_export(Xboxkrnl, 0x010B, "ObLookupThreadByThreadId", stub_success); + state.register_export(Xboxkrnl, 0x010E, "ObOpenObjectByPointer", stub_success); + state.register_export(Xboxkrnl, 0x0110, "ObReferenceObjectByHandle", stub_success); + + // RTL + state.register_export(Xboxkrnl, 0x0119, "RtlCaptureContext", rtl_capture_context); + state.register_export(Xboxkrnl, 0x011B, "RtlCompareMemoryUlong", rtl_compare_memory_ulong); + state.register_export(Xboxkrnl, 0x0125, "RtlEnterCriticalSection", rtl_enter_critical_section); + state.register_export(Xboxkrnl, 0x0126, "RtlFillMemoryUlong", rtl_fill_memory_ulong); + state.register_export(Xboxkrnl, 0x0127, "RtlFreeAnsiString", stub_success); + state.register_export(Xboxkrnl, 0x012B, "RtlImageXexHeaderField", rtl_image_xex_header_field); + state.register_export(Xboxkrnl, 0x012C, "RtlInitAnsiString", rtl_init_ansi_string); + state.register_export(Xboxkrnl, 0x012D, "RtlInitUnicodeString", rtl_init_unicode_string); + state.register_export(Xboxkrnl, 0x012E, "RtlInitializeCriticalSection", rtl_initialize_critical_section); + state.register_export(Xboxkrnl, 0x012F, "RtlInitializeCriticalSectionAndSpinCount", rtl_initialize_critical_section); + state.register_export(Xboxkrnl, 0x0130, "RtlLeaveCriticalSection", rtl_leave_critical_section); + state.register_export(Xboxkrnl, 0x0133, "RtlMultiByteToUnicodeN", rtl_multi_byte_to_unicode_n); + state.register_export(Xboxkrnl, 0x0135, "RtlNtStatusToDosError", rtl_nt_status_to_dos_error); + state.register_export(Xboxkrnl, 0x0136, "RtlRaiseException", rtl_raise_exception); + state.register_export(Xboxkrnl, 0x013B, "sprintf", stub_sprintf); + state.register_export(Xboxkrnl, 0x013F, "RtlTimeFieldsToTime", stub_success); + state.register_export(Xboxkrnl, 0x0140, "RtlTimeToTimeFields", stub_success); + state.register_export(Xboxkrnl, 0x0141, "RtlTryEnterCriticalSection", rtl_try_enter_critical_section); + state.register_export(Xboxkrnl, 0x0142, "RtlUnicodeStringToAnsiString", stub_success); + state.register_export(Xboxkrnl, 0x0143, "RtlUnicodeToMultiByteN", stub_success); + state.register_export(Xboxkrnl, 0x0147, "RtlUnwind", rtl_unwind); + state.register_export(Xboxkrnl, 0x014D, "_vsnprintf", stub_vsnprintf); + + // Stfs + state.register_export(Xboxkrnl, 0x0259, "StfsCreateDevice", stub_success); + state.register_export(Xboxkrnl, 0x025A, "StfsControlDevice", stub_success); + + // Video + state.register_export(Xboxkrnl, 0x01B1, "VdCallGraphicsNotificationRoutines", stub_success); + state.register_export(Xboxkrnl, 0x01B4, "VdEnableDisableClockGating", stub_success); + state.register_export(Xboxkrnl, 0x01B6, "VdEnableRingBufferRPtrWriteBack", stub_success); + state.register_export(Xboxkrnl, 0x01B9, "VdGetCurrentDisplayGamma", stub_return_zero); + state.register_export(Xboxkrnl, 0x01BA, "VdGetCurrentDisplayInformation", stub_success); + state.register_export(Xboxkrnl, 0x01BD, "VdGetSystemCommandBuffer", vd_get_system_command_buffer); + state.register_export(Xboxkrnl, 0x01C2, "VdInitializeEngines", stub_success); + state.register_export(Xboxkrnl, 0x01C3, "VdInitializeRingBuffer", stub_success); + state.register_export(Xboxkrnl, 0x01C5, "VdInitializeScalerCommandBuffer", stub_success); + state.register_export(Xboxkrnl, 0x01C6, "VdIsHSIOTrainingSucceeded", vd_is_hsio_training_succeeded); + state.register_export(Xboxkrnl, 0x01C7, "VdPersistDisplay", stub_success); + state.register_export(Xboxkrnl, 0x01C9, "VdQueryVideoFlags", stub_return_zero); + state.register_export(Xboxkrnl, 0x01CA, "VdQueryVideoMode", vd_query_video_mode); + state.register_export(Xboxkrnl, 0x0269, "VdRetrainEDRAM", stub_success); + state.register_export(Xboxkrnl, 0x026A, "VdRetrainEDRAMWorker", stub_success); + state.register_export(Xboxkrnl, 0x01D3, "VdSetDisplayMode", stub_success); + state.register_export(Xboxkrnl, 0x01D5, "VdSetGraphicsInterruptCallback", stub_success); + state.register_export(Xboxkrnl, 0x01D9, "VdSetSystemCommandBufferGpuIdentifierAddress", stub_success); + state.register_export(Xboxkrnl, 0x01DC, "VdShutdownEngines", stub_success); + state.register_export(Xboxkrnl, 0x025B, "VdSwap", vd_swap); + + // Audio + state.register_export(Xboxkrnl, 0x01F3, "XAudioRegisterRenderDriverClient", xaudio_register_render_driver); + state.register_export(Xboxkrnl, 0x01F4, "XAudioUnregisterRenderDriverClient", stub_success); + state.register_export(Xboxkrnl, 0x01F5, "XAudioSubmitRenderDriverFrame", stub_success); + state.register_export(Xboxkrnl, 0x01F7, "XAudioGetVoiceCategoryVolumeChangeMask", stub_return_zero); + state.register_export(Xboxkrnl, 0x01F8, "XAudioGetVoiceCategoryVolume", stub_success); + state.register_export(Xboxkrnl, 0x0224, "XMACreateContext", xma_create_context); + state.register_export(Xboxkrnl, 0x0226, "XMAReleaseContext", stub_success); + + // Crypto + state.register_export(Xboxkrnl, 0x0192, "XeCryptSha", stub_success); + state.register_export(Xboxkrnl, 0x0256, "XeKeysConsolePrivateKeySign", stub_success); + state.register_export(Xboxkrnl, 0x0257, "XeKeysConsoleSignatureVerification", stub_success); + + // Xex module + state.register_export(Xboxkrnl, 0x0194, "XexCheckExecutablePrivilege", stub_return_zero); + state.register_export(Xboxkrnl, 0x0195, "XexGetModuleHandle", stub_return_zero); + state.register_export(Xboxkrnl, 0x0197, "XexGetProcedureAddress", xex_get_procedure_address); + + // Exception handling + state.register_export(Xboxkrnl, 0x01A5, "__C_specific_handler", c_specific_handler); +} + +// ===== Generic stubs ===== + +fn stub_success(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0; // STATUS_SUCCESS +} + +fn stub_return_zero(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0; +} + +// ===== Debug ===== + +fn dbg_break_point(_ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + tracing::warn!("DbgBreakPoint hit"); +} + +fn dbg_print(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + let str_ptr = ctx.gpr[3] as u32; + if str_ptr != 0 { + let s = read_cstring(mem, str_ptr); + tracing::info!("DbgPrint: {}", s); + } + ctx.gpr[3] = 0; +} + +// ===== Threading ===== + +fn ex_create_thread(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) { + // r3 = handle_ptr, r4 = stack_size, r5 = thread_id_ptr, r6 = xapi_startup + // r7 = start_address, r8 = start_context, r9 = creation_flags + let handle_ptr = ctx.gpr[3] as u32; + let thread_id_ptr = ctx.gpr[5] as u32; + + let tid = state.next_thread_id; + state.next_thread_id += 1; + let handle = state.alloc_handle_for(KernelObject::Thread { id: tid }); + + if handle_ptr != 0 { + mem.write_u32(handle_ptr, handle); + } + if thread_id_ptr != 0 { + mem.write_u32(thread_id_ptr, tid); + } + tracing::info!("ExCreateThread: handle={:#x} tid={}", handle, tid); + ctx.gpr[3] = 0; // STATUS_SUCCESS +} + +fn ex_terminate_thread(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + tracing::info!("ExTerminateThread: exit_status={:#x}", ctx.gpr[3]); + ctx.gpr[3] = 0; +} + +fn hal_return_to_firmware(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + tracing::warn!("HalReturnToFirmware: reason={:#x}", ctx.gpr[3]); + ctx.gpr[3] = 0; +} + +// ===== Ke* ===== + +fn ke_bug_check(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + tracing::error!("KeBugCheck: code={:#x}", ctx.gpr[3]); + ctx.gpr[3] = 0; +} + +fn ke_bug_check_ex(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + tracing::error!("KeBugCheckEx: code={:#x} p1={:#x} p2={:#x} p3={:#x}", + ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.gpr[6]); + ctx.gpr[3] = 0; +} + +fn ke_get_current_process_type(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 1; // PROC_USER +} + +fn ke_query_performance_frequency(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 50_000_000; // 50 MHz +} + +fn ke_query_system_time(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + let time_ptr = ctx.gpr[3] as u32; + if time_ptr != 0 { + let fake_time: u64 = 132_500_000_000_000_000; // ~2021 FILETIME + mem.write_u32(time_ptr, (fake_time >> 32) as u32); + mem.write_u32(time_ptr + 4, fake_time as u32); + } +} + +fn ke_initialize_semaphore(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = semaphore_ptr, r4 = count, r5 = limit + let sem_ptr = ctx.gpr[3] as u32; + if sem_ptr != 0 { + // Zero-init the KSEMAPHORE structure (0x14 bytes) + for i in (0..0x14).step_by(4) { + mem.write_u32(sem_ptr + i, 0); + } + } +} + +fn ke_try_acquire_spinlock(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 1; // TRUE (acquired successfully in single-threaded mode) +} + +fn ke_tls_alloc(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) { + ctx.gpr[3] = state.tls_alloc() as u64; +} + +fn ke_tls_get_value(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) { + let index = ctx.gpr[3] as u32; + ctx.gpr[3] = state.tls_get(index); +} + +fn ke_tls_set_value(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) { + let index = ctx.gpr[3] as u32; + let value = ctx.gpr[4]; + state.tls_set(index, value); + ctx.gpr[3] = 1; // TRUE +} + +fn ex_get_xconfig_setting(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0; // STATUS_SUCCESS (writes nothing) +} + +// ===== Memory ===== + +fn nt_allocate_virtual_memory(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) { + // r3 = base_addr_ptr (in/out), r4 = region_size_ptr (in/out) + // r5 = alloc_type, r6 = protect + let base_ptr = ctx.gpr[3] as u32; + let size_ptr = ctx.gpr[4] as u32; + + let requested_base = mem.read_u32(base_ptr); + let requested_size = mem.read_u32(size_ptr); + + let aligned_size = (requested_size + 0xFFF) & !0xFFF; + if aligned_size == 0 { + ctx.gpr[3] = 0xC000_0010; // STATUS_INVALID_PARAMETER + return; + } + + let base = if requested_base != 0 { + // Try to allocate at the requested address + let protect = xenia_memory::page_table::MemoryProtect::READ + | xenia_memory::page_table::MemoryProtect::WRITE; + if mem.alloc(requested_base, aligned_size, protect).is_ok() { + requested_base + } else { + // Already allocated? Treat as success (common for re-commit) + requested_base + } + } else { + // Allocate from heap + match state.heap_alloc(aligned_size, mem) { + Some(addr) => addr, + None => { + tracing::warn!("NtAllocateVirtualMemory: heap exhausted (size={:#x})", aligned_size); + ctx.gpr[3] = 0xC000_0017; // STATUS_NO_MEMORY + return; + } + } + }; + + mem.write_u32(base_ptr, base); + mem.write_u32(size_ptr, aligned_size); + tracing::info!("NtAllocateVirtualMemory: base={:#010x} size={:#x}", base, aligned_size); + ctx.gpr[3] = 0; // STATUS_SUCCESS +} + +fn mm_allocate_physical_memory_ex(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) { + // r3 = size, r4 = protect, r5 = min_addr, r6 = max_addr, r7 = alignment + let size = ctx.gpr[3] as u32; + match state.heap_alloc(size, mem) { + Some(addr) => ctx.gpr[3] = addr as u64, + None => ctx.gpr[3] = 0, + } +} + +fn mm_create_kernel_stack(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) { + // r3 = stack_size, r4 = reserved + let size = std::cmp::max(ctx.gpr[3] as u32, 0x4000); // Min 16KB + match state.stack_alloc(size, mem) { + Some(top) => { + tracing::info!("MmCreateKernelStack: top={:#010x} size={:#x}", top, size); + ctx.gpr[3] = top as u64; + } + None => ctx.gpr[3] = 0, + } +} + +fn mm_get_physical_address(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = virtual address -> return physical address + ctx.gpr[3] = ctx.gpr[3] & 0x1FFF_FFFF; // Mask to 512MB physical +} + +fn mm_query_address_protect(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + // Return PAGE_READWRITE (0x04) + ctx.gpr[3] = 0x04; +} + +fn mm_query_statistics(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = stats_ptr — write fake memory statistics + let ptr = ctx.gpr[3] as u32; + if ptr != 0 { + // Total physical = 512MB + mem.write_u32(ptr + 0x04, 512 * 1024 * 1024); // TotalPhysicalPages (in bytes) + mem.write_u32(ptr + 0x10, 256 * 1024 * 1024); // AvailablePages + } + ctx.gpr[3] = 0; +} + +// ===== File I/O ===== + +fn nt_create_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) { + let handle = state.alloc_handle_for(KernelObject::File { path: String::new() }); + tracing::info!("NtCreateFile: handle={:#x}", handle); + ctx.gpr[3] = 0; +} + +fn nt_open_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) { + let handle = state.alloc_handle_for(KernelObject::File { path: String::new() }); + tracing::info!("NtOpenFile: handle={:#x}", handle); + ctx.gpr[3] = 0; +} + +fn nt_read_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0xC000_0011; // STATUS_END_OF_FILE +} + +fn nt_write_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0; // STATUS_SUCCESS (discard data) +} + +fn nt_query_full_attributes_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0xC000_0034; // STATUS_OBJECT_NAME_NOT_FOUND +} + +fn nt_query_directory_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0xC000_0034; // STATUS_OBJECT_NAME_NOT_FOUND +} + +fn nt_close(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) { + let handle = ctx.gpr[3] as u32; + state.objects.remove(&handle); + ctx.gpr[3] = 0; +} + +fn nt_create_event(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) { + // r3 = handle_ptr, r4 = obj_attrs, r5 = event_type, r6 = initial_state + let handle_ptr = ctx.gpr[3] as u32; + let manual_reset = ctx.gpr[5] != 0; + let signaled = ctx.gpr[6] != 0; + let handle = state.alloc_handle_for(KernelObject::Event { manual_reset, signaled }); + if handle_ptr != 0 { + mem.write_u32(handle_ptr, handle); + } + ctx.gpr[3] = 0; +} + +fn nt_create_semaphore(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) { + // r3 = handle_ptr, r4 = obj_attrs, r5 = initial_count, r6 = max_count + let handle_ptr = ctx.gpr[3] as u32; + let count = ctx.gpr[5] as i32; + let max = ctx.gpr[6] as i32; + let handle = state.alloc_handle_for(KernelObject::Semaphore { count, max }); + if handle_ptr != 0 { + mem.write_u32(handle_ptr, handle); + } + ctx.gpr[3] = 0; +} + +fn nt_create_timer(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) { + let handle_ptr = ctx.gpr[3] as u32; + let handle = state.alloc_handle_for(KernelObject::Timer); + if handle_ptr != 0 { + mem.write_u32(handle_ptr, handle); + } + ctx.gpr[3] = 0; +} + +// ===== RTL ===== + +fn rtl_initialize_critical_section(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = critical_section_ptr (28 bytes on Xbox 360) + let cs_ptr = ctx.gpr[3] as u32; + if cs_ptr != 0 { + for i in (0..28).step_by(4) { + mem.write_u32(cs_ptr + i, 0); + } + // Set recursion count to -1 (unlocked) + mem.write_u32(cs_ptr + 8, 0xFFFF_FFFF_u32); + } + ctx.gpr[3] = 0; +} + +fn rtl_enter_critical_section(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = critical_section_ptr + // For single-threaded: increment lock count, always succeed + let cs_ptr = ctx.gpr[3] as u32; + if cs_ptr != 0 { + let lock_count = mem.read_u32(cs_ptr + 4) as i32; + mem.write_u32(cs_ptr + 4, (lock_count + 1) as u32); + let recursion = mem.read_u32(cs_ptr + 8) as i32; + mem.write_u32(cs_ptr + 8, (recursion + 1) as u32); + } + ctx.gpr[3] = 0; +} + +fn rtl_leave_critical_section(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + let cs_ptr = ctx.gpr[3] as u32; + if cs_ptr != 0 { + let lock_count = mem.read_u32(cs_ptr + 4) as i32; + mem.write_u32(cs_ptr + 4, (lock_count - 1) as u32); + let recursion = mem.read_u32(cs_ptr + 8) as i32; + mem.write_u32(cs_ptr + 8, (recursion - 1) as u32); + } + ctx.gpr[3] = 0; +} + +fn rtl_try_enter_critical_section(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + // Always succeed in single-threaded mode + let cs_ptr = ctx.gpr[3] as u32; + if cs_ptr != 0 { + let lock_count = mem.read_u32(cs_ptr + 4) as i32; + mem.write_u32(cs_ptr + 4, (lock_count + 1) as u32); + let recursion = mem.read_u32(cs_ptr + 8) as i32; + mem.write_u32(cs_ptr + 8, (recursion + 1) as u32); + } + ctx.gpr[3] = 1; // TRUE +} + +fn rtl_init_ansi_string(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + let dest_ptr = ctx.gpr[3] as u32; + let src_ptr = ctx.gpr[4] as u32; + if src_ptr != 0 { + let mut len: u16 = 0; + let mut addr = src_ptr; + while mem.read_u8(addr) != 0 { + len += 1; + addr += 1; + } + mem.write_u16(dest_ptr, len); + mem.write_u16(dest_ptr + 2, len + 1); + mem.write_u32(dest_ptr + 4, src_ptr); + } else { + mem.write_u16(dest_ptr, 0); + mem.write_u16(dest_ptr + 2, 0); + mem.write_u32(dest_ptr + 4, 0); + } +} + +fn rtl_init_unicode_string(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + let dest_ptr = ctx.gpr[3] as u32; + let src_ptr = ctx.gpr[4] as u32; + if src_ptr != 0 { + let mut len: u16 = 0; + let mut addr = src_ptr; + while mem.read_u16(addr) != 0 { + len += 2; + addr += 2; + } + mem.write_u16(dest_ptr, len); + mem.write_u16(dest_ptr + 2, len + 2); + mem.write_u32(dest_ptr + 4, src_ptr); + } else { + mem.write_u16(dest_ptr, 0); + mem.write_u16(dest_ptr + 2, 0); + mem.write_u32(dest_ptr + 4, 0); + } +} + +fn rtl_capture_context(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = context_ptr — write CPU registers to CONTEXT structure + let ptr = ctx.gpr[3] as u32; + if ptr != 0 { + // Write GPRs at offset 0 (simplified) + for i in 0..32 { + mem.write_u64(ptr + (i * 8) as u32, ctx.gpr[i]); + } + } +} + +fn rtl_compare_memory_ulong(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = source, r4 = length, r5 = pattern + let source = ctx.gpr[3] as u32; + let length = ctx.gpr[4] as u32; + let pattern = ctx.gpr[5] as u32; + let mut matched: u32 = 0; + let count = length / 4; + for i in 0..count { + let val = mem.read_u32(source + i * 4); + if val != pattern { + break; + } + matched += 4; + } + ctx.gpr[3] = matched as u64; +} + +fn rtl_fill_memory_ulong(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = destination, r4 = length, r5 = pattern + let dest = ctx.gpr[3] as u32; + let length = ctx.gpr[4] as u32; + let pattern = ctx.gpr[5] as u32; + let count = length / 4; + for i in 0..count { + mem.write_u32(dest + i * 4, pattern); + } +} + +fn rtl_image_xex_header_field(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = xex_header_ptr, r4 = field_id + // Return 0 for all fields + ctx.gpr[3] = 0; +} + +fn rtl_multi_byte_to_unicode_n(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = unicode_str, r4 = max_bytes_out, r5 = bytes_written_ptr + // r6 = multi_byte_str, r7 = multi_byte_len + let uni_ptr = ctx.gpr[3] as u32; + let max_bytes = ctx.gpr[4] as u32; + let written_ptr = ctx.gpr[5] as u32; + let mb_ptr = ctx.gpr[6] as u32; + let mb_len = ctx.gpr[7] as u32; + + let max_chars = max_bytes / 2; + let count = std::cmp::min(mb_len, max_chars); + for i in 0..count { + let byte = mem.read_u8(mb_ptr + i); + mem.write_u16(uni_ptr + i * 2, byte as u16); + } + if written_ptr != 0 { + mem.write_u32(written_ptr, count * 2); + } + ctx.gpr[3] = 0; +} + +fn rtl_nt_status_to_dos_error(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + // Simple mapping for common cases + let status = ctx.gpr[3] as u32; + ctx.gpr[3] = match status { + 0 => 0, // ERROR_SUCCESS + 0xC000_0034 => 2, // ERROR_FILE_NOT_FOUND + 0xC000_0011 => 38, // ERROR_HANDLE_EOF + _ => status as u64, // Pass through + }; +} + +fn rtl_raise_exception(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + tracing::warn!("RtlRaiseException: record_ptr={:#010x}", ctx.gpr[3]); + // Don't halt — just log and return +} + +fn rtl_unwind(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + tracing::warn!("RtlUnwind: target_frame={:#010x}", ctx.gpr[3]); + // Stub — in a real implementation this would walk the stack +} + +fn stub_sprintf(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + let dest = ctx.gpr[3] as u32; + let fmt = ctx.gpr[4] as u32; + if fmt != 0 && dest != 0 { + let mut addr = fmt; + let mut daddr = dest; + loop { + let c = mem.read_u8(addr); + mem.write_u8(daddr, c); + if c == 0 { break; } + addr += 1; + daddr += 1; + } + } + ctx.gpr[3] = 0; +} + +fn stub_vsnprintf(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = buffer, r4 = count, r5 = format, r6 = va_list + let dest = ctx.gpr[3] as u32; + let fmt = ctx.gpr[5] as u32; + if fmt != 0 && dest != 0 { + let mut addr = fmt; + let mut daddr = dest; + loop { + let c = mem.read_u8(addr); + mem.write_u8(daddr, c); + if c == 0 { break; } + addr += 1; + daddr += 1; + } + } + ctx.gpr[3] = 0; +} + +// ===== Video ===== + +fn vd_query_video_mode(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + let mode_ptr = ctx.gpr[3] as u32; + if mode_ptr != 0 { + mem.write_u32(mode_ptr, 1280); + mem.write_u32(mode_ptr + 4, 720); + mem.write_u32(mode_ptr + 8, 0); // is_interlaced + mem.write_u32(mode_ptr + 12, 1); // is_widescreen + mem.write_u32(mode_ptr + 16, 60); // refresh_rate + } + ctx.gpr[3] = 0; +} + +fn vd_get_system_command_buffer(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) { + // r3 = cmd_buffer_ptr_ptr, r4 = cmd_buffer_size_ptr + let buf_ptr_ptr = ctx.gpr[3] as u32; + let buf_size_ptr = ctx.gpr[4] as u32; + + if state.gpu_command_buffer == 0 { + // Allocate a 64KB command buffer + if let Some(addr) = state.heap_alloc(0x10000, mem) { + state.gpu_command_buffer = addr; + } + } + + if buf_ptr_ptr != 0 { + mem.write_u32(buf_ptr_ptr, state.gpu_command_buffer); + } + if buf_size_ptr != 0 { + mem.write_u32(buf_size_ptr, 0x10000); + } + ctx.gpr[3] = 0; +} + +fn vd_is_hsio_training_succeeded(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 1; // TRUE +} + +fn vd_swap(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + tracing::info!("VdSwap (frame boundary)"); + ctx.gpr[3] = 0; +} + +// ===== Audio ===== + +fn xaudio_register_render_driver(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) { + let handle = state.alloc_handle(); + tracing::info!("XAudioRegisterRenderDriverClient: handle={:#x}", handle); + // r3 = callback_ptr, r4 = driver_ptr -> write handle + ctx.gpr[3] = 0; +} + +fn xma_create_context(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) { + let handle = state.alloc_handle(); + tracing::info!("XMACreateContext: handle={:#x}", handle); + ctx.gpr[3] = handle as u64; +} + +// ===== Xex ===== + +fn xex_get_procedure_address(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + let ordinal = ctx.gpr[4] as u32; + tracing::warn!("XexGetProcedureAddress: ordinal={:#x} not found", ordinal); + ctx.gpr[3] = 0xC000_0034; // STATUS_OBJECT_NAME_NOT_FOUND +} + +// ===== Exception handling ===== + +fn c_specific_handler(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + tracing::warn!("__C_specific_handler called (exception handling stub)"); + ctx.gpr[3] = 1; // ExceptionContinueSearch +} + +// ===== Helpers ===== + +fn read_cstring(mem: &GuestMemory, addr: u32) -> String { + let mut s = String::new(); + let mut a = addr; + loop { + let c = mem.read_u8(a); + if c == 0 { break; } + s.push(c as char); + a += 1; + if s.len() > 512 { break; } // Safety limit + } + s +} diff --git a/crates/xenia-kernel/src/lib.rs b/crates/xenia-kernel/src/lib.rs new file mode 100644 index 0000000..457b815 --- /dev/null +++ b/crates/xenia-kernel/src/lib.rs @@ -0,0 +1,6 @@ +pub mod exports; +pub mod objects; +pub mod state; +pub mod xam; + +pub use state::{KernelState, ModuleId}; diff --git a/crates/xenia-kernel/src/objects.rs b/crates/xenia-kernel/src/objects.rs new file mode 100644 index 0000000..117399b --- /dev/null +++ b/crates/xenia-kernel/src/objects.rs @@ -0,0 +1,12 @@ +//! Kernel object tracking for HLE. + +/// Kernel object types tracked by handle. +#[derive(Debug)] +pub enum KernelObject { + Event { manual_reset: bool, signaled: bool }, + Semaphore { count: i32, max: i32 }, + File { path: String }, + Thread { id: u32 }, + Timer, + Mutex, +} diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs new file mode 100644 index 0000000..cc47e4e --- /dev/null +++ b/crates/xenia-kernel/src/state.rs @@ -0,0 +1,159 @@ +use std::collections::HashMap; +use xenia_cpu::PpcContext; +use xenia_memory::GuestMemory; + +use crate::objects::KernelObject; + +/// Function signature for HLE kernel exports. +pub type KernelExportFn = fn(&mut PpcContext, &mut GuestMemory, &mut KernelState); + +/// Module identifier for kernel exports. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ModuleId { + Xboxkrnl, + Xam, + Xbdm, +} + +/// Central kernel state tracking all guest OS state. +pub struct KernelState { + exports: HashMap<(ModuleId, u32), (&'static str, KernelExportFn)>, + next_handle: u32, + pub tls_slots: HashMap, + next_tls_index: u32, + /// Kernel object table: handle → object + pub objects: HashMap, + /// Bump allocator for guest heap (NtAllocateVirtualMemory etc.) + pub heap_cursor: u32, + /// Stack allocator cursor for MmCreateKernelStack + pub stack_cursor: u32, + /// GPU command buffer address (set by VdGetSystemCommandBuffer) + pub gpu_command_buffer: u32, + /// Image base of the loaded XEX (for XexExecutableModuleHandle etc.) + pub image_base: u32, + /// Next thread ID + pub next_thread_id: u32, +} + +impl KernelState { + pub fn new() -> Self { + let mut state = Self { + exports: HashMap::new(), + next_handle: 0x1000, + tls_slots: HashMap::new(), + next_tls_index: 0, + objects: HashMap::new(), + heap_cursor: 0x4000_0000, // Start of user heap region + stack_cursor: 0x7100_0000, // Above main stack + gpu_command_buffer: 0, + image_base: 0, + next_thread_id: 1, + }; + crate::exports::register_exports(&mut state); + crate::xam::register_exports(&mut state); + state + } + + pub fn register_export( + &mut self, + module: ModuleId, + ordinal: u32, + name: &'static str, + func: KernelExportFn, + ) { + self.exports.insert((module, ordinal), (name, func)); + } + + pub fn call_export( + &mut self, + module: ModuleId, + ordinal: u32, + ctx: &mut PpcContext, + mem: &mut GuestMemory, + ) -> bool { + if let Some(&(name, func)) = self.exports.get(&(module, ordinal)) { + tracing::info!( + "Kernel call: {:?}:{:#x} ({}) args=[{:#x}, {:#x}, {:#x}, {:#x}]", + module, ordinal, name, + ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.gpr[6] + ); + func(ctx, mem, self); + tracing::info!(" -> returned {:#x}", ctx.gpr[3]); + true + } else { + tracing::warn!( + "Unimplemented kernel export: {:?}:{:#x}", + module, ordinal + ); + // Return 0 (STATUS_SUCCESS) by default for unimplemented calls + ctx.gpr[3] = 0; + false + } + } + + pub fn export_name(&self, module: ModuleId, ordinal: u32) -> Option<&'static str> { + self.exports.get(&(module, ordinal)).map(|&(name, _)| name) + } + + pub fn alloc_handle(&mut self) -> u32 { + let h = self.next_handle; + self.next_handle += 4; + h + } + + pub fn alloc_handle_for(&mut self, obj: KernelObject) -> u32 { + let h = self.alloc_handle(); + self.objects.insert(h, obj); + h + } + + pub fn tls_get(&self, index: u32) -> u64 { + self.tls_slots.get(&index).copied().unwrap_or(0) + } + + pub fn tls_set(&mut self, index: u32, value: u64) { + self.tls_slots.insert(index, value); + } + + pub fn tls_alloc(&mut self) -> u32 { + let idx = self.next_tls_index; + self.next_tls_index += 1; + idx + } + + /// Allocate guest memory from the heap bump allocator. + /// Returns the base address of the allocated region. + pub fn heap_alloc(&mut self, size: u32, mem: &mut GuestMemory) -> Option { + let aligned_size = (size + 0xFFF) & !0xFFF; // Page-align + let base = self.heap_cursor; + if base.checked_add(aligned_size).is_none() || base + aligned_size > 0x6FFF_FFFF { + return None; + } + let protect = xenia_memory::page_table::MemoryProtect::READ + | xenia_memory::page_table::MemoryProtect::WRITE; + if mem.alloc(base, aligned_size, protect).is_err() { + return None; + } + self.heap_cursor += aligned_size; + Some(base) + } + + /// Allocate a kernel stack. + pub fn stack_alloc(&mut self, size: u32, mem: &mut GuestMemory) -> Option { + let aligned_size = (size + 0xFFF) & !0xFFF; + let base = self.stack_cursor; + let protect = xenia_memory::page_table::MemoryProtect::READ + | xenia_memory::page_table::MemoryProtect::WRITE; + if mem.alloc(base, aligned_size, protect).is_err() { + return None; + } + self.stack_cursor += aligned_size; + Some(base + aligned_size) // Return top of stack + } +} + +impl Default for KernelState { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/xenia-kernel/src/xam.rs b/crates/xenia-kernel/src/xam.rs new file mode 100644 index 0000000..29d210b --- /dev/null +++ b/crates/xenia-kernel/src/xam.rs @@ -0,0 +1,253 @@ +//! HLE kernel export implementations (xam.xex). + +use crate::state::{KernelState, ModuleId}; +use xenia_cpu::PpcContext; +use xenia_memory::{GuestMemory, MemoryAccess}; + +pub fn register_exports(state: &mut KernelState) { + use ModuleId::Xam; + + // Net + state.register_export(Xam, 0x01, "NetDll_WSAStartup", stub_success); + state.register_export(Xam, 0x02, "NetDll_WSACleanup", stub_success); + + // Input + state.register_export(Xam, 0x0190, "XamInputGetCapabilities", xam_input_not_connected); + state.register_export(Xam, 0x0191, "XamInputGetState", xam_input_not_connected); + state.register_export(Xam, 0x0192, "XamInputSetState", xam_input_not_connected); + state.register_export(Xam, 0x0198, "XamInputGetKeystrokeEx", xam_input_not_connected); + + // Inactivity + state.register_export(Xam, 0x01A0, "XamEnableInactivityProcessing", stub_success); + state.register_export(Xam, 0x01A1, "XamResetInactivity", stub_success); + + // Loader + state.register_export(Xam, 0x01A4, "XamLoaderLaunchTitle", xam_loader_launch_title); + state.register_export(Xam, 0x01A9, "XamLoaderTerminateTitle", xam_loader_terminate_title); + + // Task + state.register_export(Xam, 0x01AF, "XamTaskSchedule", xam_task_schedule); + state.register_export(Xam, 0x01B1, "XamTaskCloseHandle", stub_success); + state.register_export(Xam, 0x01B3, "XamTaskShouldExit", stub_return_zero); + + // Alloc + state.register_export(Xam, 0x01EA, "XamAlloc", xam_alloc); + state.register_export(Xam, 0x01EC, "XamFree", stub_success); + + // Msg + state.register_export(Xam, 0x01F4, "XMsgInProcessCall", stub_success); + state.register_export(Xam, 0x01F7, "XMsgStartIORequest", stub_success); + state.register_export(Xam, 0x01FC, "XMsgStartIORequestEx", stub_success); + + // User + state.register_export(Xam, 0x020A, "XamUserGetXUID", xam_user_get_xuid); + state.register_export(Xam, 0x020E, "XamUserGetName", xam_user_get_name); + state.register_export(Xam, 0x0210, "XamUserGetSigninState", stub_return_zero); + state.register_export(Xam, 0x0219, "XamUserReadProfileSettings", xam_user_read_profile_settings); + state.register_export(Xam, 0x021A, "XamUserWriteProfileSettings", stub_success); + + // Enum + state.register_export(Xam, 0x0250, "XamEnumerate", stub_error_no_more_files); + + // Content + state.register_export(Xam, 0x0258, "XamContentCreate", stub_success); + state.register_export(Xam, 0x025A, "XamContentClose", stub_success); + state.register_export(Xam, 0x025B, "XamContentDelete", stub_success); + state.register_export(Xam, 0x025C, "XamContentCreateEnumerator", stub_success); + state.register_export(Xam, 0x025E, "XamContentGetDeviceData", stub_success); + state.register_export(Xam, 0x025F, "XamContentGetDeviceName", stub_success); + state.register_export(Xam, 0x0260, "XamContentSetThumbnail", stub_success); + state.register_export(Xam, 0x0262, "XamContentGetCreator", stub_success); + state.register_export(Xam, 0x0265, "XamContentGetDeviceState", stub_success); + + // System + state.register_export(Xam, 0x0280, "XamGetExecutionId", xam_get_execution_id); + state.register_export(Xam, 0x0282, "XamGetSystemVersion", xam_get_system_version); + + // Notify + state.register_export(Xam, 0x028A, "XamNotifyCreateListener", xam_notify_create_listener); + state.register_export(Xam, 0x028B, "XNotifyGetNext", xnotify_get_next); + state.register_export(Xam, 0x028C, "XNotifyPositionUI", stub_success); + + // Achievements/Stats + state.register_export(Xam, 0x02EE, "XamUserCreateAchievementEnumerator", stub_success); + state.register_export(Xam, 0x02F7, "XamUserCreateStatsEnumerator", stub_success); + + // UI + state.register_export(Xam, 0x02BC, "XamShowSigninUI", stub_success); + state.register_export(Xam, 0x02C1, "XamShowKeyboardUI", stub_success); + state.register_export(Xam, 0x02CB, "XamShowDeviceSelectorUI", stub_success); + state.register_export(Xam, 0x02D5, "XamShowGamerCardUIForXUID", stub_success); + state.register_export(Xam, 0x02D9, "XamShowDirtyDiscErrorUI", stub_success); + state.register_export(Xam, 0x02DC, "XamShowMessageBoxUIEx", stub_success); + + // Session + state.register_export(Xam, 0x0316, "XamSessionCreateHandle", xam_session_create_handle); + state.register_export(Xam, 0x0317, "XamSessionRefObjByHandle", stub_success); + + // Locale + state.register_export(Xam, 0x03CB, "XGetAVPack", xget_avpack); + state.register_export(Xam, 0x03CC, "XGetGameRegion", xget_game_region); + state.register_export(Xam, 0x03CD, "XGetLanguage", xget_language); + state.register_export(Xam, 0x03D1, "XGetVideoMode", xget_video_mode); +} + +// ===== Generic stubs ===== + +fn stub_success(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0; +} + +fn stub_return_zero(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0; +} + +fn stub_error_no_more_files(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0x12; // ERROR_NO_MORE_FILES +} + +// ===== Input ===== + +fn xam_input_not_connected(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0x48F; // ERROR_DEVICE_NOT_CONNECTED +} + +// ===== Loader ===== + +fn xam_loader_launch_title(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + tracing::warn!("XamLoaderLaunchTitle called"); + ctx.gpr[3] = 0; +} + +fn xam_loader_terminate_title(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + tracing::warn!("XamLoaderTerminateTitle called"); + ctx.gpr[3] = 0; +} + +// ===== Task ===== + +fn xam_task_schedule(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) { + let handle = state.alloc_handle(); + tracing::info!("XamTaskSchedule: handle={:#x}", handle); + ctx.gpr[3] = 0; +} + +// ===== Alloc ===== + +fn xam_alloc(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) { + // r3 = flags, r4 = size, r5 = out_ptr_ptr + let size = ctx.gpr[4] as u32; + let out_ptr = ctx.gpr[5] as u32; + + match state.heap_alloc(size, mem) { + Some(addr) => { + if out_ptr != 0 { + mem.write_u32(out_ptr, addr); + } + ctx.gpr[3] = 0; // SUCCESS + } + None => { + ctx.gpr[3] = 0x8007_000E; // E_OUTOFMEMORY + } + } +} + +// ===== User ===== + +fn xam_user_get_xuid(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = user_index, r4 = xuid_ptr + let xuid_ptr = ctx.gpr[4] as u32; + if xuid_ptr != 0 { + mem.write_u64(xuid_ptr, 0); // No XUID + } + ctx.gpr[3] = 0; +} + +fn xam_user_get_name(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = user_index, r4 = buffer, r5 = buffer_size + let buffer = ctx.gpr[4] as u32; + if buffer != 0 { + mem.write_u8(buffer, 0); // Empty string + } + ctx.gpr[3] = 0; +} + +fn xam_user_read_profile_settings(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + // Return error — no profile + ctx.gpr[3] = 0x0000_048B; // ERROR_NOT_FOUND +} + +// ===== System ===== + +fn xam_get_execution_id(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) { + // r3 = execution_id_ptr_ptr — write pointer to execution info + let ptr_ptr = ctx.gpr[3] as u32; + if ptr_ptr != 0 { + // Allocate and fill a fake XEX_EXECUTION_ID structure + if let Some(exec_id_addr) = state.heap_alloc(0x18, mem) { + mem.write_u32(exec_id_addr, 0x535107D4); // title_id (Project Sylpheed) + mem.write_u32(exec_id_addr + 4, 0x2D2E2EEB); // media_id + mem.write_u16(exec_id_addr + 8, 0); // version + mem.write_u16(exec_id_addr + 10, 0); // base_version + mem.write_u16(exec_id_addr + 12, 1); // disc_number + mem.write_u16(exec_id_addr + 14, 1); // disc_count + mem.write_u32(ptr_ptr, exec_id_addr); + } + } + ctx.gpr[3] = 0; +} + +fn xam_get_system_version(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0x2000_0000; // System version +} + +// ===== Notify ===== + +fn xam_notify_create_listener(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) { + let handle = state.alloc_handle(); + ctx.gpr[3] = handle as u64; +} + +fn xnotify_get_next(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = handle, r4 = id_ptr, r5 = param_ptr + ctx.gpr[3] = 0; // FALSE (no notifications) +} + +// ===== Session ===== + +fn xam_session_create_handle(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) { + // r3 = handle_ptr + let handle_ptr = ctx.gpr[3] as u32; + let handle = state.alloc_handle(); + if handle_ptr != 0 { + mem.write_u32(handle_ptr, handle); + } + ctx.gpr[3] = 0; +} + +// ===== Locale ===== + +fn xget_avpack(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0x16; // HDMI +} + +fn xget_game_region(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0xFF; // All regions +} + +fn xget_language(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 1; // English +} + +fn xget_video_mode(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + // r3 = video_mode_ptr + let ptr = ctx.gpr[3] as u32; + if ptr != 0 { + mem.write_u32(ptr, 1280); // width + mem.write_u32(ptr + 4, 720); // height + mem.write_u32(ptr + 8, 0); // is_interlaced + mem.write_u32(ptr + 12, 1); // is_widescreen + mem.write_u32(ptr + 16, 60); // refresh_rate + } + ctx.gpr[3] = 0; +} diff --git a/crates/xenia-memory/Cargo.toml b/crates/xenia-memory/Cargo.toml new file mode 100644 index 0000000..9b515b5 --- /dev/null +++ b/crates/xenia-memory/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "xenia-memory" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +tracing = { workspace = true } +bitflags = { workspace = true } +thiserror = { workspace = true } + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.59", features = ["Win32_System_Memory", "Win32_Foundation"] } diff --git a/crates/xenia-memory/src/access.rs b/crates/xenia-memory/src/access.rs new file mode 100644 index 0000000..7ee3855 --- /dev/null +++ b/crates/xenia-memory/src/access.rs @@ -0,0 +1,47 @@ +/// Trait for all guest memory access. Every load/store goes through this, +/// enabling MMIO checking and debugger observation on every access. +/// This is the key abstraction that eliminates the need for MMIO exception handlers. +pub trait MemoryAccess { + fn read_u8(&self, addr: u32) -> u8; + fn read_u16(&self, addr: u32) -> u16; + fn read_u32(&self, addr: u32) -> u32; + fn read_u64(&self, addr: u32) -> u64; + fn read_f32(&self, addr: u32) -> f32 { + f32::from_bits(self.read_u32(addr)) + } + fn read_f64(&self, addr: u32) -> f64 { + f64::from_bits(self.read_u64(addr)) + } + + fn write_u8(&mut self, addr: u32, val: u8); + fn write_u16(&mut self, addr: u32, val: u16); + fn write_u32(&mut self, addr: u32, val: u32); + fn write_u64(&mut self, addr: u32, val: u64); + fn write_f32(&mut self, addr: u32, val: f32) { + self.write_u32(addr, val.to_bits()); + } + fn write_f64(&mut self, addr: u32, val: f64) { + self.write_u64(addr, val.to_bits()); + } + + /// Read a block of bytes from guest memory. + fn read_bytes(&self, addr: u32, buf: &mut [u8]) { + for (i, byte) in buf.iter_mut().enumerate() { + *byte = self.read_u8(addr.wrapping_add(i as u32)); + } + } + + /// Write a block of bytes to guest memory. + fn write_bytes(&mut self, addr: u32, buf: &[u8]) { + for (i, &byte) in buf.iter().enumerate() { + self.write_u8(addr.wrapping_add(i as u32), byte); + } + } + + /// Get a direct host pointer for the given guest address. + /// Returns None if the address is invalid or in an MMIO region. + fn translate(&self, addr: u32) -> Option<*const u8>; + + /// Get a mutable direct host pointer for the given guest address. + fn translate_mut(&mut self, addr: u32) -> Option<*mut u8>; +} diff --git a/crates/xenia-memory/src/heap.rs b/crates/xenia-memory/src/heap.rs new file mode 100644 index 0000000..b34a33f --- /dev/null +++ b/crates/xenia-memory/src/heap.rs @@ -0,0 +1,265 @@ +use crate::access::MemoryAccess; +use crate::mmio::MmioRegion; +use crate::page_table::{AllocationState, MemoryProtect, PageEntry}; +use crate::MemoryError; + +const PAGE_SIZE: u32 = 4096; +/// Total guest address space: 4GB. +const GUEST_ADDRESS_SPACE: usize = 0x1_0000_0000; +/// Number of 4K pages in the 4GB address space. +const PAGE_COUNT: usize = GUEST_ADDRESS_SPACE / PAGE_SIZE as usize; +/// Physical memory mask (512MB physical address space). +const PHYSICAL_ADDR_MASK: u32 = 0x1FFF_FFFF; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HeapType { + GuestVirtual, + GuestXex, + GuestPhysical, +} + +/// The core guest memory system. Manages a 4GB virtual address space +/// via mmap/VirtualAlloc, with page-level tracking and MMIO dispatch. +pub struct GuestMemory { + /// Host pointer to the base of the 4GB guest address space. + membase: *mut u8, + /// Page table tracking allocation state for each 4K page. + page_table: Vec, + /// Registered MMIO regions (sorted by base address for binary search). + mmio_regions: Vec, + /// Whether the memory mapping is owned (should be unmapped on drop). + owned: bool, +} + +unsafe impl Send for GuestMemory {} +unsafe impl Sync for GuestMemory {} + +impl GuestMemory { + /// Create a new guest memory space by reserving a 4GB virtual address region. + pub fn new() -> Result { + let membase = crate::platform::reserve_address_space(GUEST_ADDRESS_SPACE)?; + Ok(Self { + membase, + page_table: vec![PageEntry::default(); PAGE_COUNT], + mmio_regions: Vec::new(), + owned: true, + }) + } + + /// Get the host base pointer for the guest address space. + pub fn membase(&self) -> *const u8 { + self.membase + } + + /// Get a mutable host base pointer. + pub fn membase_mut(&mut self) -> *mut u8 { + self.membase + } + + /// Translate a guest virtual address to a host pointer. + pub fn translate_virtual(&self, guest_addr: u32) -> *const u8 { + unsafe { self.membase.add(guest_addr as usize) } + } + + /// Translate a guest virtual address to a mutable host pointer. + pub fn translate_virtual_mut(&mut self, guest_addr: u32) -> *mut u8 { + unsafe { self.membase.add(guest_addr as usize) } + } + + /// Translate a guest physical address to a host pointer. + pub fn translate_physical(&self, guest_addr: u32) -> *const u8 { + let phys = guest_addr & PHYSICAL_ADDR_MASK; + unsafe { self.membase.add(phys as usize) } + } + + /// Register an MMIO region. + pub fn add_mmio_region(&mut self, region: MmioRegion) { + let base = region.base_address; + let idx = self + .mmio_regions + .binary_search_by_key(&base, |r| r.base_address) + .unwrap_or_else(|i| i); + self.mmio_regions.insert(idx, region); + } + + /// Check if an address is in a registered MMIO region. + fn find_mmio(&self, addr: u32) -> Option<&MmioRegion> { + self.mmio_regions.iter().find(|r| r.contains(addr)) + } + + /// Allocate a region in the guest address space. + pub fn alloc( + &mut self, + base: u32, + size: u32, + protect: MemoryProtect, + ) -> Result { + let page_start = (base / PAGE_SIZE) as usize; + let page_count = ((size + PAGE_SIZE - 1) / PAGE_SIZE) as usize; + + // Commit pages via platform + let host_ptr = unsafe { self.membase.add(base as usize) }; + crate::platform::commit_memory(host_ptr, (page_count * PAGE_SIZE as usize) as usize)?; + + // Update page table + for i in 0..page_count { + let idx = page_start + i; + if idx < self.page_table.len() { + let entry = &mut self.page_table[idx]; + entry.set_base_address(page_start as u32); + entry.set_region_page_count(page_count as u32); + entry.set_allocation_protect(protect); + entry.set_current_protect(protect); + entry.set_state(AllocationState::RESERVE | AllocationState::COMMIT); + } + } + + Ok(base) + } + + /// Read a slice of bytes from guest memory (bypassing MMIO for bulk reads). + pub fn read_bulk(&self, addr: u32, buf: &mut [u8]) { + let ptr = self.translate_virtual(addr); + unsafe { + std::ptr::copy_nonoverlapping(ptr, buf.as_mut_ptr(), buf.len()); + } + } + + /// Write a slice of bytes to guest memory (bypassing MMIO for bulk writes). + pub fn write_bulk(&mut self, addr: u32, buf: &[u8]) { + let ptr = self.translate_virtual_mut(addr); + unsafe { + std::ptr::copy_nonoverlapping(buf.as_ptr(), ptr, buf.len()); + } + } + + /// Check if a guest address has been allocated/committed. + pub fn is_mapped(&self, addr: u32) -> bool { + let page = (addr / PAGE_SIZE) as usize; + if page >= self.page_table.len() { + return false; + } + self.page_table[page].state().contains(AllocationState::COMMIT) + } + + /// Get a page table entry for a given address. + pub fn page_entry(&self, addr: u32) -> &PageEntry { + let page = (addr / PAGE_SIZE) as usize; + &self.page_table[page] + } +} + +impl MemoryAccess for GuestMemory { + fn read_u8(&self, addr: u32) -> u8 { + if !self.is_mapped(addr) { return 0; } + let ptr = self.translate_virtual(addr); + unsafe { *ptr } + } + + fn read_u16(&self, addr: u32) -> u16 { + if let Some(mmio) = self.find_mmio(addr) { + (mmio.read_callback)(addr) as u16 + } else if !self.is_mapped(addr) { + 0 + } else { + let ptr = self.translate_virtual(addr) as *const [u8; 2]; + u16::from_be_bytes(unsafe { *ptr }) + } + } + + fn read_u32(&self, addr: u32) -> u32 { + if let Some(mmio) = self.find_mmio(addr) { + (mmio.read_callback)(addr) + } else if !self.is_mapped(addr) { + 0 + } else { + let ptr = self.translate_virtual(addr) as *const [u8; 4]; + u32::from_be_bytes(unsafe { *ptr }) + } + } + + fn read_u64(&self, addr: u32) -> u64 { + if let Some(mmio) = self.find_mmio(addr) { + let hi = (mmio.read_callback)(addr) as u64; + let lo = (mmio.read_callback)(addr.wrapping_add(4)) as u64; + (hi << 32) | lo + } else if !self.is_mapped(addr) { + 0 + } else { + let ptr = self.translate_virtual(addr) as *const [u8; 8]; + u64::from_be_bytes(unsafe { *ptr }) + } + } + + fn write_u8(&mut self, addr: u32, val: u8) { + if !self.is_mapped(addr) { return; } + let ptr = self.translate_virtual_mut(addr); + unsafe { *ptr = val }; + } + + fn write_u16(&mut self, addr: u32, val: u16) { + if let Some(mmio) = self.find_mmio(addr) { + (mmio.write_callback)(addr, val as u32); + } else if !self.is_mapped(addr) { + return; + } else { + let ptr = self.translate_virtual_mut(addr); + unsafe { + std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 2); + } + } + } + + fn write_u32(&mut self, addr: u32, val: u32) { + if let Some(mmio) = self.find_mmio(addr) { + (mmio.write_callback)(addr, val); + } else if !self.is_mapped(addr) { + return; + } else { + let ptr = self.translate_virtual_mut(addr); + unsafe { + std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 4); + } + } + } + + fn write_u64(&mut self, addr: u32, val: u64) { + if let Some(mmio) = self.find_mmio(addr) { + (mmio.write_callback)(addr, (val >> 32) as u32); + (mmio.write_callback)(addr.wrapping_add(4), val as u32); + } else if !self.is_mapped(addr) { + return; + } else { + let ptr = self.translate_virtual_mut(addr); + unsafe { + std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 8); + } + } + } + + fn translate(&self, addr: u32) -> Option<*const u8> { + if self.find_mmio(addr).is_some() || !self.is_mapped(addr) { + None + } else { + Some(self.translate_virtual(addr)) + } + } + + fn translate_mut(&mut self, addr: u32) -> Option<*mut u8> { + if self.find_mmio(addr).is_some() { + None + } else { + Some(self.translate_virtual_mut(addr)) + } + } +} + +impl Drop for GuestMemory { + fn drop(&mut self) { + if self.owned && !self.membase.is_null() { + unsafe { + crate::platform::release_address_space(self.membase, GUEST_ADDRESS_SPACE); + } + } + } +} diff --git a/crates/xenia-memory/src/lib.rs b/crates/xenia-memory/src/lib.rs new file mode 100644 index 0000000..f7d991a --- /dev/null +++ b/crates/xenia-memory/src/lib.rs @@ -0,0 +1,31 @@ +pub mod access; +pub mod heap; +pub mod mmio; +pub mod page_table; + +mod platform; + +use thiserror::Error; + +pub use access::MemoryAccess; +pub use heap::{GuestMemory, HeapType}; +pub use mmio::MmioRegion; +pub use page_table::PageEntry; + +#[derive(Debug, Error)] +pub enum MemoryError { + #[error("Failed to allocate guest address space: {0}")] + AllocationFailed(String), + + #[error("Invalid guest address: {0:#010x}")] + InvalidAddress(u32), + + #[error("MMIO access at {0:#010x}")] + MmioAccess(u32), + + #[error("Protection violation at {0:#010x}")] + ProtectionViolation(u32), + + #[error("Out of memory in heap {0:?}")] + OutOfMemory(HeapType), +} diff --git a/crates/xenia-memory/src/mmio.rs b/crates/xenia-memory/src/mmio.rs new file mode 100644 index 0000000..fed3198 --- /dev/null +++ b/crates/xenia-memory/src/mmio.rs @@ -0,0 +1,27 @@ +/// Represents a mapped MMIO region with read/write callbacks. +/// Instead of trapping access violations (as the C++ JIT does), the interpreter +/// explicitly checks each memory access against registered MMIO regions. +pub struct MmioRegion { + pub base_address: u32, + pub mask: u32, + pub size: u32, + pub read_callback: Box u32 + Send + Sync>, + pub write_callback: Box, +} + +impl MmioRegion { + pub fn contains(&self, addr: u32) -> bool { + let masked = addr & self.mask; + masked >= self.base_address && masked < self.base_address + self.size + } +} + +impl std::fmt::Debug for MmioRegion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MmioRegion") + .field("base_address", &format_args!("{:#010x}", self.base_address)) + .field("mask", &format_args!("{:#010x}", self.mask)) + .field("size", &format_args!("{:#x}", self.size)) + .finish() + } +} diff --git a/crates/xenia-memory/src/page_table.rs b/crates/xenia-memory/src/page_table.rs new file mode 100644 index 0000000..8d116eb --- /dev/null +++ b/crates/xenia-memory/src/page_table.rs @@ -0,0 +1,122 @@ +use bitflags::bitflags; + +/// Describes a single page in the page table. +/// Mirrors the C++ `PageEntry` union from memory.h:82-99. +#[derive(Clone, Copy, Default)] +pub struct PageEntry(u64); + +impl PageEntry { + /// Base address of the allocated region in 4K pages (20 bits). + pub fn base_address(&self) -> u32 { + (self.0 & 0xFFFFF) as u32 + } + + pub fn set_base_address(&mut self, val: u32) { + self.0 = (self.0 & !0xFFFFF) | (val as u64 & 0xFFFFF); + } + + /// Total number of pages in the allocated region (20 bits). + pub fn region_page_count(&self) -> u32 { + ((self.0 >> 20) & 0xFFFFF) as u32 + } + + pub fn set_region_page_count(&mut self, val: u32) { + self.0 = (self.0 & !(0xFFFFF << 20)) | ((val as u64 & 0xFFFFF) << 20); + } + + /// Protection bits specified during region allocation (4 bits). + pub fn allocation_protect(&self) -> MemoryProtect { + MemoryProtect::from_bits_truncate(((self.0 >> 40) & 0xF) as u32) + } + + pub fn set_allocation_protect(&mut self, val: MemoryProtect) { + self.0 = (self.0 & !(0xF << 40)) | ((val.bits() as u64 & 0xF) << 40); + } + + /// Current protection bits (4 bits). + pub fn current_protect(&self) -> MemoryProtect { + MemoryProtect::from_bits_truncate(((self.0 >> 44) & 0xF) as u32) + } + + pub fn set_current_protect(&mut self, val: MemoryProtect) { + self.0 = (self.0 & !(0xF << 44)) | ((val.bits() as u64 & 0xF) << 44); + } + + /// Allocation state (2 bits). + pub fn state(&self) -> AllocationState { + AllocationState::from_bits_truncate(((self.0 >> 48) & 0x3) as u32) + } + + pub fn set_state(&mut self, val: AllocationState) { + self.0 = (self.0 & !(0x3 << 48)) | ((val.bits() as u64 & 0x3) << 48); + } + + pub fn is_committed(&self) -> bool { + self.state().contains(AllocationState::COMMIT) + } + + pub fn is_reserved(&self) -> bool { + self.state().contains(AllocationState::RESERVE) + } + + pub fn is_free(&self) -> bool { + self.state().is_empty() + } +} + +bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct MemoryProtect: u32 { + const READ = 1 << 0; + const WRITE = 1 << 1; + const NO_CACHE = 1 << 2; + const WRITE_COMBINE = 1 << 3; + } +} + +bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct AllocationState: u32 { + const RESERVE = 1 << 0; + const COMMIT = 1 << 1; + } +} + +impl std::fmt::Debug for PageEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PageEntry") + .field("base_address", &format_args!("{:#x}", self.base_address())) + .field("region_page_count", &self.region_page_count()) + .field("allocation_protect", &self.allocation_protect()) + .field("current_protect", &self.current_protect()) + .field("state", &self.state()) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_page_entry_bitfields() { + let mut entry = PageEntry::default(); + assert!(entry.is_free()); + + entry.set_base_address(0x100); + entry.set_region_page_count(0x10); + entry.set_allocation_protect(MemoryProtect::READ | MemoryProtect::WRITE); + entry.set_current_protect(MemoryProtect::READ); + entry.set_state(AllocationState::RESERVE | AllocationState::COMMIT); + + assert_eq!(entry.base_address(), 0x100); + assert_eq!(entry.region_page_count(), 0x10); + assert_eq!( + entry.allocation_protect(), + MemoryProtect::READ | MemoryProtect::WRITE + ); + assert_eq!(entry.current_protect(), MemoryProtect::READ); + assert!(entry.is_committed()); + assert!(entry.is_reserved()); + } +} diff --git a/crates/xenia-memory/src/platform.rs b/crates/xenia-memory/src/platform.rs new file mode 100644 index 0000000..a584eae --- /dev/null +++ b/crates/xenia-memory/src/platform.rs @@ -0,0 +1,98 @@ +use crate::MemoryError; + +/// Reserve a contiguous virtual address region without committing physical pages. +#[cfg(unix)] +pub fn reserve_address_space(size: usize) -> Result<*mut u8, MemoryError> { + unsafe { + let ptr = libc::mmap( + std::ptr::null_mut(), + size, + libc::PROT_NONE, + libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_NORESERVE, + -1, + 0, + ); + if ptr == libc::MAP_FAILED { + Err(MemoryError::AllocationFailed(format!( + "mmap failed for {} bytes: {}", + size, + std::io::Error::last_os_error() + ))) + } else { + Ok(ptr as *mut u8) + } + } +} + +/// Commit (make accessible) a region within a previously reserved address space. +#[cfg(unix)] +pub fn commit_memory(ptr: *mut u8, size: usize) -> Result<(), MemoryError> { + unsafe { + let result = libc::mprotect(ptr as *mut libc::c_void, size, libc::PROT_READ | libc::PROT_WRITE); + if result != 0 { + Err(MemoryError::AllocationFailed(format!( + "mprotect failed for {} bytes: {}", + size, + std::io::Error::last_os_error() + ))) + } else { + Ok(()) + } + } +} + +/// Release a previously reserved address space. +#[cfg(unix)] +pub unsafe fn release_address_space(ptr: *mut u8, size: usize) { + unsafe { libc::munmap(ptr as *mut libc::c_void, size); } +} + +#[cfg(windows)] +pub fn reserve_address_space(size: usize) -> Result<*mut u8, MemoryError> { + unsafe { + let ptr = windows_sys::Win32::System::Memory::VirtualAlloc( + std::ptr::null_mut(), + size, + windows_sys::Win32::System::Memory::MEM_RESERVE, + windows_sys::Win32::System::Memory::PAGE_NOACCESS, + ); + if ptr.is_null() { + Err(MemoryError::AllocationFailed(format!( + "VirtualAlloc reserve failed for {} bytes", + size, + ))) + } else { + Ok(ptr as *mut u8) + } + } +} + +#[cfg(windows)] +pub fn commit_memory(ptr: *mut u8, size: usize) -> Result<(), MemoryError> { + unsafe { + let result = windows_sys::Win32::System::Memory::VirtualAlloc( + ptr as *mut _, + size, + windows_sys::Win32::System::Memory::MEM_COMMIT, + windows_sys::Win32::System::Memory::PAGE_READWRITE, + ); + if result.is_null() { + Err(MemoryError::AllocationFailed(format!( + "VirtualAlloc commit failed for {} bytes", + size, + ))) + } else { + Ok(()) + } + } +} + +#[cfg(windows)] +pub unsafe fn release_address_space(ptr: *mut u8, size: usize) { + let _ = size; + windows_sys::Win32::System::Memory::VirtualFree( + ptr as *mut _, + 0, + windows_sys::Win32::System::Memory::MEM_RELEASE, + ); +} diff --git a/crates/xenia-types/Cargo.toml b/crates/xenia-types/Cargo.toml new file mode 100644 index 0000000..e95a51e --- /dev/null +++ b/crates/xenia-types/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "xenia-types" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +bitflags = { workspace = true } +byteorder = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true } diff --git a/crates/xenia-types/src/endian.rs b/crates/xenia-types/src/endian.rs new file mode 100644 index 0000000..a36b0e2 --- /dev/null +++ b/crates/xenia-types/src/endian.rs @@ -0,0 +1,122 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::marker::PhantomData; + +/// Big-endian value wrapper matching `xe::be` from the C++ codebase. +/// Stores the value in big-endian byte order and transparently converts +/// on access. Used for guest memory structures that are natively big-endian. +#[derive(Clone, Copy, Serialize, Deserialize)] +#[repr(transparent)] +pub struct Be(T::Bytes, PhantomData); + +impl Be { + pub fn new(val: T) -> Self { + Self(val.to_be_bytes(), PhantomData) + } + + pub fn get(self) -> T { + T::from_be_bytes(self.0) + } + + pub fn set(&mut self, val: T) { + self.0 = val.to_be_bytes(); + } + + pub fn raw_bytes(&self) -> &T::Bytes { + &self.0 + } +} + +impl Default for Be { + fn default() -> Self { + Self::new(T::default()) + } +} + +impl fmt::Debug for Be { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.get().fmt(f) + } +} + +impl fmt::Display for Be { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.get().fmt(f) + } +} + +impl PartialEq for Be { + fn eq(&self, other: &Self) -> bool { + // Compare raw bytes for efficiency (same byte order) + self.0.as_ref() == other.0.as_ref() + } +} + +impl Eq for Be {} + +/// Trait for types that can be converted to/from big-endian byte representations. +pub trait BeSwap: Copy { + type Bytes: Copy + AsRef<[u8]> + serde::Serialize + for<'de> serde::Deserialize<'de>; + fn to_be_bytes(self) -> Self::Bytes; + fn from_be_bytes(bytes: Self::Bytes) -> Self; +} + +macro_rules! impl_be_swap { + ($t:ty) => { + impl BeSwap for $t { + type Bytes = [u8; std::mem::size_of::<$t>()]; + fn to_be_bytes(self) -> Self::Bytes { + <$t>::to_be_bytes(self) + } + fn from_be_bytes(bytes: Self::Bytes) -> Self { + <$t>::from_be_bytes(bytes) + } + } + }; +} + +impl_be_swap!(u8); +impl_be_swap!(u16); +impl_be_swap!(u32); +impl_be_swap!(u64); +impl_be_swap!(i8); +impl_be_swap!(i16); +impl_be_swap!(i32); +impl_be_swap!(i64); +impl_be_swap!(f32); +impl_be_swap!(f64); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_be_u32() { + let v = Be::::new(0x12345678); + assert_eq!(v.get(), 0x12345678); + assert_eq!(v.raw_bytes(), &[0x12, 0x34, 0x56, 0x78]); + } + + #[test] + fn test_be_u16() { + let v = Be::::new(0xABCD); + assert_eq!(v.get(), 0xABCD); + assert_eq!(v.raw_bytes(), &[0xAB, 0xCD]); + } + + #[test] + fn test_be_mutate() { + let mut v = Be::::new(1); + assert_eq!(v.get(), 1); + v.set(42); + assert_eq!(v.get(), 42); + } + + #[test] + fn test_be_f32() { + let v = Be::::new(1.0); + assert_eq!(v.get(), 1.0); + // IEEE 754: 1.0f = 0x3F800000 => bytes [0x3F, 0x80, 0x00, 0x00] + assert_eq!(v.raw_bytes(), &[0x3F, 0x80, 0x00, 0x00]); + } +} diff --git a/crates/xenia-types/src/error.rs b/crates/xenia-types/src/error.rs new file mode 100644 index 0000000..cb0806a --- /dev/null +++ b/crates/xenia-types/src/error.rs @@ -0,0 +1,27 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum XeniaError { + #[error("Invalid XEX2 file: {0}")] + InvalidXex(String), + + #[error("Invalid XISO file: {0}")] + InvalidXiso(String), + + #[error("Memory error: {0}")] + Memory(String), + + #[error("Unimplemented opcode: {0}")] + UnimplementedOpcode(String), + + #[error("Unimplemented kernel export: module={module} ordinal={ordinal:#x}")] + UnimplementedExport { module: String, ordinal: u32 }, + + #[error("Invalid guest address: {0:#010x}")] + InvalidAddress(u32), + + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), +} + +pub type XeniaResult = Result; diff --git a/crates/xenia-types/src/lib.rs b/crates/xenia-types/src/lib.rs new file mode 100644 index 0000000..f635422 --- /dev/null +++ b/crates/xenia-types/src/lib.rs @@ -0,0 +1,6 @@ +pub mod endian; +pub mod error; +pub mod vec128; + +pub use endian::Be; +pub use vec128::Vec128; diff --git a/crates/xenia-types/src/vec128.rs b/crates/xenia-types/src/vec128.rs new file mode 100644 index 0000000..1da0912 --- /dev/null +++ b/crates/xenia-types/src/vec128.rs @@ -0,0 +1,206 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// 128-bit vector register type matching the Xbox 360's VMX128 registers. +/// Stored in big-endian byte order (matching guest memory layout). +#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[repr(C, align(16))] +pub struct Vec128 { + pub bytes: [u8; 16], +} + +impl Vec128 { + pub const ZERO: Self = Self { bytes: [0; 16] }; + + pub fn from_u32x4(a: u32, b: u32, c: u32, d: u32) -> Self { + let mut bytes = [0u8; 16]; + bytes[0..4].copy_from_slice(&a.to_be_bytes()); + bytes[4..8].copy_from_slice(&b.to_be_bytes()); + bytes[8..12].copy_from_slice(&c.to_be_bytes()); + bytes[12..16].copy_from_slice(&d.to_be_bytes()); + Self { bytes } + } + + pub fn from_f32x4(a: f32, b: f32, c: f32, d: f32) -> Self { + Self::from_u32x4(a.to_bits(), b.to_bits(), c.to_bits(), d.to_bits()) + } + + /// Read the i-th u32 element (big-endian, 0-indexed). + pub fn u32x4(&self, i: usize) -> u32 { + let off = i * 4; + u32::from_be_bytes([ + self.bytes[off], + self.bytes[off + 1], + self.bytes[off + 2], + self.bytes[off + 3], + ]) + } + + /// Write the i-th u32 element (big-endian, 0-indexed). + pub fn set_u32x4(&mut self, i: usize, val: u32) { + let off = i * 4; + self.bytes[off..off + 4].copy_from_slice(&val.to_be_bytes()); + } + + /// Read the i-th f32 element (big-endian, 0-indexed). + pub fn f32x4(&self, i: usize) -> f32 { + f32::from_bits(self.u32x4(i)) + } + + /// Write the i-th f32 element (big-endian, 0-indexed). + pub fn set_f32x4(&mut self, i: usize, val: f32) { + self.set_u32x4(i, val.to_bits()); + } + + /// Read the i-th u16 element (big-endian, 0-indexed). + pub fn u16x8(&self, i: usize) -> u16 { + let off = i * 2; + u16::from_be_bytes([self.bytes[off], self.bytes[off + 1]]) + } + + /// Write the i-th u16 element (big-endian, 0-indexed). + pub fn set_u16x8(&mut self, i: usize, val: u16) { + let off = i * 2; + self.bytes[off..off + 2].copy_from_slice(&val.to_be_bytes()); + } + + /// Read the i-th u8 element (0-indexed). + pub fn u8x16(&self, i: usize) -> u8 { + self.bytes[i] + } + + /// Write the i-th u8 element (0-indexed). + pub fn set_u8x16(&mut self, i: usize, val: u8) { + self.bytes[i] = val; + } + + /// Read as two u64 values (big-endian). + pub fn u64x2(&self, i: usize) -> u64 { + let off = i * 8; + u64::from_be_bytes([ + self.bytes[off], + self.bytes[off + 1], + self.bytes[off + 2], + self.bytes[off + 3], + self.bytes[off + 4], + self.bytes[off + 5], + self.bytes[off + 6], + self.bytes[off + 7], + ]) + } + + pub fn set_u64x2(&mut self, i: usize, val: u64) { + let off = i * 8; + self.bytes[off..off + 8].copy_from_slice(&val.to_be_bytes()); + } + + /// Get all 4 u32 elements as an array. + pub fn as_u32x4(&self) -> [u32; 4] { + [self.u32x4(0), self.u32x4(1), self.u32x4(2), self.u32x4(3)] + } + + /// Get all 4 f32 elements as an array. + pub fn as_f32x4(&self) -> [f32; 4] { + [self.f32x4(0), self.f32x4(1), self.f32x4(2), self.f32x4(3)] + } + + /// Get all 8 u16 elements as an array. + pub fn as_u16x8(&self) -> [u16; 8] { + [ + self.u16x8(0), self.u16x8(1), self.u16x8(2), self.u16x8(3), + self.u16x8(4), self.u16x8(5), self.u16x8(6), self.u16x8(7), + ] + } + + /// Get all 16 bytes as an array. + pub fn as_bytes(&self) -> [u8; 16] { + self.bytes + } + + /// Create from a byte array. + pub fn from_bytes(bytes: [u8; 16]) -> Self { + Self { bytes } + } + + /// Create from a u32 array (big-endian elements). + pub fn from_u32x4_array(arr: [u32; 4]) -> Self { + Self::from_u32x4(arr[0], arr[1], arr[2], arr[3]) + } + + /// Create from an f32 array (big-endian elements). + pub fn from_f32x4_array(arr: [f32; 4]) -> Self { + Self::from_f32x4(arr[0], arr[1], arr[2], arr[3]) + } + + /// Create from a u16 array (big-endian elements). + pub fn from_u16x8_array(arr: [u16; 8]) -> Self { + let mut v = Self::ZERO; + for i in 0..8 { v.set_u16x8(i, arr[i]); } + v + } +} + +impl Default for Vec128 { + fn default() -> Self { + Self::ZERO + } +} + +impl fmt::Debug for Vec128 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Vec128({:08X}_{:08X}_{:08X}_{:08X})", + self.u32x4(0), + self.u32x4(1), + self.u32x4(2), + self.u32x4(3), + ) + } +} + +impl fmt::Display for Vec128 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_u32x4_roundtrip() { + let v = Vec128::from_u32x4(0xDEADBEEF, 0xCAFEBABE, 0x12345678, 0x9ABCDEF0); + assert_eq!(v.u32x4(0), 0xDEADBEEF); + assert_eq!(v.u32x4(1), 0xCAFEBABE); + assert_eq!(v.u32x4(2), 0x12345678); + assert_eq!(v.u32x4(3), 0x9ABCDEF0); + } + + #[test] + fn test_f32x4_roundtrip() { + let v = Vec128::from_f32x4(1.0, -2.5, 3.14, 0.0); + assert_eq!(v.f32x4(0), 1.0); + assert_eq!(v.f32x4(1), -2.5); + assert!((v.f32x4(2) - 3.14).abs() < f32::EPSILON); + assert_eq!(v.f32x4(3), 0.0); + } + + #[test] + fn test_u16x8() { + let v = Vec128::from_u32x4(0x00010002, 0x00030004, 0x00050006, 0x00070008); + assert_eq!(v.u16x8(0), 0x0001); + assert_eq!(v.u16x8(1), 0x0002); + assert_eq!(v.u16x8(6), 0x0007); + assert_eq!(v.u16x8(7), 0x0008); + } + + #[test] + fn test_zero() { + let v = Vec128::ZERO; + for i in 0..4 { + assert_eq!(v.u32x4(i), 0); + } + } +} diff --git a/crates/xenia-vfs/Cargo.toml b/crates/xenia-vfs/Cargo.toml new file mode 100644 index 0000000..e30e2c3 --- /dev/null +++ b/crates/xenia-vfs/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "xenia-vfs" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +tracing = { workspace = true } +byteorder = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } diff --git a/crates/xenia-vfs/src/device.rs b/crates/xenia-vfs/src/device.rs new file mode 100644 index 0000000..3a83bc8 --- /dev/null +++ b/crates/xenia-vfs/src/device.rs @@ -0,0 +1,54 @@ +use crate::{VfsDevice, VfsEntry, VfsError}; +use std::path::{Path, PathBuf}; + +/// Host filesystem pass-through device. +pub struct HostPathDevice { + name: String, + root: PathBuf, +} + +impl HostPathDevice { + pub fn new(name: impl Into, root: impl AsRef) -> Self { + Self { + name: name.into(), + root: root.as_ref().to_path_buf(), + } + } +} + +impl VfsDevice for HostPathDevice { + fn name(&self) -> &str { + &self.name + } + + fn list_root(&self) -> Result, VfsError> { + let mut entries = Vec::new(); + for entry in std::fs::read_dir(&self.root)? { + let entry = entry?; + let metadata = entry.metadata()?; + entries.push(VfsEntry { + name: entry.file_name().to_string_lossy().into_owned(), + is_directory: metadata.is_dir(), + size: metadata.len(), + offset: 0, + }); + } + Ok(entries) + } + + fn read_file(&self, path: &str) -> Result, VfsError> { + let full_path = self.root.join(path); + std::fs::read(&full_path).map_err(VfsError::from) + } + + fn stat(&self, path: &str) -> Result { + let full_path = self.root.join(path); + let metadata = std::fs::metadata(&full_path)?; + Ok(VfsEntry { + name: path.to_string(), + is_directory: metadata.is_dir(), + size: metadata.len(), + offset: 0, + }) + } +} diff --git a/crates/xenia-vfs/src/disc_image.rs b/crates/xenia-vfs/src/disc_image.rs new file mode 100644 index 0000000..37f2167 --- /dev/null +++ b/crates/xenia-vfs/src/disc_image.rs @@ -0,0 +1,185 @@ +use crate::{VfsDevice, VfsEntry, VfsError}; +use std::io::{Read, Seek, SeekFrom}; + +/// XISO disc image device. Parses Xbox 360 disc images (GDFX/XISO format). +pub struct DiscImageDevice { + name: String, + path: std::path::PathBuf, + game_offset: u64, + /// Cached root directory buffer (typically small, a few KB). + root_buffer: Vec, +} + +/// XISO sector size +pub const SECTOR_SIZE: u64 = 0x800; + +/// GDFX magic string +const GDFX_MAGIC: &[u8; 20] = b"MICROSOFT*XBOX*MEDIA"; + +/// File attribute: directory +const FILE_ATTRIBUTE_DIRECTORY: u8 = 0x10; + +/// Known game partition offsets to try +const LIKELY_OFFSETS: &[u64] = &[ + 0x0000_0000, + 0x0000_FB20, + 0x0002_0600, + 0x0208_0000, + 0x0FD9_0000, +]; + +impl DiscImageDevice { + pub fn open(name: impl Into, path: &std::path::Path) -> Result { + let mut file = std::fs::File::open(path)?; + + // Find the game partition by locating the GDFX magic at sector 32 + let mut game_offset = 0u64; + let mut magic_found = false; + let mut magic_buf = [0u8; 20]; + + for &offset in LIKELY_OFFSETS { + let magic_pos = offset + 32 * SECTOR_SIZE; + if file.seek(SeekFrom::Start(magic_pos)).is_ok() + && file.read_exact(&mut magic_buf).is_ok() + && magic_buf == *GDFX_MAGIC + { + game_offset = offset; + magic_found = true; + break; + } + } + + if !magic_found { + return Err(VfsError::InvalidFormat( + "GDFX magic not found - not a valid XISO disc image".into(), + )); + } + + // Read root directory info from sector 32 header + let fs_ptr = game_offset + 32 * SECTOR_SIZE; + file.seek(SeekFrom::Start(fs_ptr + 20))?; + let mut buf4 = [0u8; 4]; + file.read_exact(&mut buf4)?; + let root_sector = u32::from_le_bytes(buf4) as u64; + file.read_exact(&mut buf4)?; + let root_size = u32::from_le_bytes(buf4) as u64; + + let root_byte_offset = game_offset + root_sector * SECTOR_SIZE; + + // Read the root directory buffer into memory (typically small) + file.seek(SeekFrom::Start(root_byte_offset))?; + let mut root_buffer = vec![0u8; root_size as usize]; + file.read_exact(&mut root_buffer)?; + + Ok(Self { + name: name.into(), + path: path.to_path_buf(), + game_offset, + root_buffer, + }) + } + + /// Read all directory entries from the root directory tree. + fn read_entries(&self) -> Vec { + let mut entries = Vec::new(); + self.read_entry(&self.root_buffer, 0, &mut entries); + entries + } + + /// Recursively read a directory entry from the binary tree structure. + fn read_entry(&self, buffer: &[u8], ordinal: u16, entries: &mut Vec) { + let p = ordinal as usize * 4; + if p + 14 > buffer.len() { + return; + } + + let node_l = u16::from_le_bytes([buffer[p], buffer[p + 1]]); + let node_r = u16::from_le_bytes([buffer[p + 2], buffer[p + 3]]); + let sector = u32::from_le_bytes([buffer[p + 4], buffer[p + 5], buffer[p + 6], buffer[p + 7]]) as u64; + let length = u32::from_le_bytes([buffer[p + 8], buffer[p + 9], buffer[p + 10], buffer[p + 11]]) as u64; + let attributes = buffer[p + 12]; + let name_length = buffer[p + 13] as usize; + + if p + 14 + name_length > buffer.len() { + return; + } + + // Traverse left subtree first (smaller names) + if node_l != 0 && node_l != 0xFFFF { + self.read_entry(buffer, node_l, entries); + } + + // Read this entry's name + let name = String::from_utf8_lossy(&buffer[p + 14..p + 14 + name_length]).to_string(); + let is_directory = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + + let file_offset = self.game_offset + sector * SECTOR_SIZE; + + entries.push(VfsEntry { + name, + is_directory, + size: length, + offset: file_offset, + }); + + // Traverse right subtree (larger names) + if node_r != 0 && node_r != 0xFFFF { + self.read_entry(buffer, node_r, entries); + } + } +} + +impl VfsDevice for DiscImageDevice { + fn name(&self) -> &str { + &self.name + } + + fn list_root(&self) -> Result, VfsError> { + Ok(self.read_entries()) + } + + fn read_file(&self, path: &str) -> Result, VfsError> { + let entries = self.read_entries(); + let entry = entries.iter() + .find(|e| e.name.eq_ignore_ascii_case(path) && !e.is_directory) + .ok_or_else(|| VfsError::NotFound(path.to_string()))?; + + let offset = entry.offset; + let size = entry.size as usize; + + // Read from file using seek + let mut file = std::fs::File::open(&self.path)?; + let file_len = file.seek(SeekFrom::End(0))?; + if offset + size as u64 > file_len { + return Err(VfsError::NotFound(format!( + "File data extends past end of image: {} (offset={:#x}, size={:#x}, image_len={:#x})", + path, offset, size, file_len + ))); + } + file.seek(SeekFrom::Start(offset))?; + let mut buf = vec![0u8; size]; + let bytes_read = file.read(&mut buf)?; + if bytes_read < size { + // Try reading the rest + let mut total = bytes_read; + while total < size { + let n = file.read(&mut buf[total..])?; + if n == 0 { + return Err(VfsError::NotFound(format!( + "Short read: got {} of {} bytes for {}", + total, size, path + ))); + } + total += n; + } + } + Ok(buf) + } + + fn stat(&self, path: &str) -> Result { + let entries = self.read_entries(); + entries.into_iter() + .find(|e| e.name.eq_ignore_ascii_case(path)) + .ok_or_else(|| VfsError::NotFound(path.to_string())) + } +} diff --git a/crates/xenia-vfs/src/lib.rs b/crates/xenia-vfs/src/lib.rs new file mode 100644 index 0000000..86cedc2 --- /dev/null +++ b/crates/xenia-vfs/src/lib.rs @@ -0,0 +1,33 @@ +pub mod device; +pub mod disc_image; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum VfsError { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("Invalid format: {0}")] + InvalidFormat(String), + + #[error("File not found: {0}")] + NotFound(String), +} + +/// A virtual filesystem entry (file or directory). +#[derive(Debug)] +pub struct VfsEntry { + pub name: String, + pub is_directory: bool, + pub size: u64, + pub offset: u64, +} + +/// Trait for VFS device implementations (XISO, STFS, host path, etc.) +pub trait VfsDevice: Send + Sync { + fn name(&self) -> &str; + fn list_root(&self) -> Result, VfsError>; + fn read_file(&self, path: &str) -> Result, VfsError>; + fn stat(&self, path: &str) -> Result; +} diff --git a/crates/xenia-xex/Cargo.toml b/crates/xenia-xex/Cargo.toml new file mode 100644 index 0000000..b3b6c5e --- /dev/null +++ b/crates/xenia-xex/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "xenia-xex" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +xenia-memory = { workspace = true } +tracing = { workspace = true } +byteorder = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +aes = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } + diff --git a/crates/xenia-xex/build.rs b/crates/xenia-xex/build.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/crates/xenia-xex/build.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/crates/xenia-xex/src/header.rs b/crates/xenia-xex/src/header.rs new file mode 100644 index 0000000..0da667c --- /dev/null +++ b/crates/xenia-xex/src/header.rs @@ -0,0 +1,128 @@ +use serde::Serialize; + +/// XEX2 file header. Parsed from the beginning of an Xbox 360 executable. +#[derive(Debug, Serialize)] +pub struct Xex2Header { + pub magic: u32, + pub module_flags: u32, + pub header_size: u32, + pub security_offset: u32, + pub header_count: u32, + pub optional_headers: Vec, + pub security_info: Option, + /// Parsed file format info (if present). + pub file_format_info: Option, + /// Parsed import libraries (addresses only until resolve_imports is called). + pub import_libraries: Vec, + /// Execution info (title ID, media ID, etc.). + pub execution_info: Option, + /// Original PE name from the XEX header. + pub original_pe_name: Option, +} + +#[derive(Debug, Serialize)] +pub struct Xex2OptionalHeader { + pub key: u32, + pub value: u32, +} + +#[derive(Debug, Serialize)] +pub struct Xex2SecurityInfo { + pub image_size: u32, + pub load_address: u32, + pub export_table_address: u32, + pub image_flags: u32, + /// Encrypted session key (decrypted with retail/devkit key to get actual session key). + pub aes_key: [u8; 16], + pub page_descriptors: Vec, +} + +#[derive(Debug, Clone, Copy, Serialize)] +pub struct Xex2PageDescriptor { + pub size_and_info: u32, +} + +impl Xex2PageDescriptor { + pub fn page_count(&self) -> u32 { + self.size_and_info >> 4 + } + + pub fn info(&self) -> u32 { + self.size_and_info & 0xF + } +} + +/// File format info (compression and encryption types). +#[derive(Debug, Clone, Serialize)] +pub struct FileFormatInfo { + pub info_size: u32, + pub encryption_type: u16, + pub compression_type: u16, + /// For basic compression: list of (data_size, zero_size) block pairs. + pub basic_blocks: Vec, + /// For normal (LZX) compression: window size. + pub normal_window_size: u32, + /// For normal (LZX) compression: first block size (from header). + pub normal_first_block_size: u32, + /// For normal (LZX) compression: first block hash (from header). + pub normal_first_block_hash: [u8; 20], +} + +#[derive(Debug, Clone, Copy, Serialize)] +pub struct BasicCompressionBlock { + pub data_size: u32, + pub zero_size: u32, +} + +/// An imported library with its resolved imports. +#[derive(Debug, Clone, Serialize)] +pub struct ImportLibrary { + pub name: String, + pub id: u32, + pub version_min: u32, + pub version_cur: u32, + /// Import entries. Before `resolve_imports`, these contain addresses but no ordinals. + /// After `resolve_imports`, ordinals and record types are filled in from the PE image. + pub imports: Vec, +} + +/// A single import entry within an import library. +#[derive(Debug, Clone, Serialize)] +pub struct ImportEntry { + pub ordinal: u16, + pub record_type: u8, // 0 = variable, 1 = thunk + pub address: u32, +} + +/// Execution info parsed from the XEX header. +#[derive(Debug, Clone, Serialize)] +pub struct ExecutionInfo { + pub media_id: u32, + pub title_id: u32, + pub disc_number: u8, + pub disc_count: u8, +} + +/// XEX2 magic: "XEX2" +pub const XEX2_MAGIC: u32 = 0x58455832; + +/// Compression types +pub const COMPRESSION_NONE: u16 = 0; +pub const COMPRESSION_BASIC: u16 = 1; +pub const COMPRESSION_NORMAL: u16 = 2; + +/// Encryption types +pub const ENCRYPTION_NONE: u16 = 0; +pub const ENCRYPTION_NORMAL: u16 = 1; + +/// Optional header keys +pub mod header_keys { + pub const ENTRY_POINT: u32 = 0x00010100; + pub const IMAGE_BASE_ADDRESS: u32 = 0x00010201; + pub const IMPORT_LIBRARIES: u32 = 0x000103FF; + pub const TLS_INFO: u32 = 0x00020200; + pub const EXECUTION_INFO: u32 = 0x00040006; + pub const DEFAULT_STACK_SIZE: u32 = 0x00020104; + pub const ORIGINAL_PE_NAME: u32 = 0x000183FF; + pub const FILE_FORMAT_INFO: u32 = 0x000003FF; +} diff --git a/crates/xenia-xex/src/lib.rs b/crates/xenia-xex/src/lib.rs new file mode 100644 index 0000000..755a618 --- /dev/null +++ b/crates/xenia-xex/src/lib.rs @@ -0,0 +1,6 @@ +pub mod header; +pub mod loader; +pub mod lzx; +pub mod pe; + +pub use header::Xex2Header; diff --git a/crates/xenia-xex/src/loader.rs b/crates/xenia-xex/src/loader.rs new file mode 100644 index 0000000..ba92c37 --- /dev/null +++ b/crates/xenia-xex/src/loader.rs @@ -0,0 +1,571 @@ +use crate::header::*; +use aes::cipher::{BlockDecrypt, KeyInit}; +use aes::Aes128; +use byteorder::{BigEndian, ReadBytesExt}; +use std::io::{self, Cursor, Read, Seek, SeekFrom}; + +/// Parse a XEX2 header from raw file data. +pub fn parse_xex2_header(data: &[u8]) -> io::Result { + let mut cursor = Cursor::new(data); + + let magic = cursor.read_u32::()?; + if magic != XEX2_MAGIC { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Invalid XEX2 magic: {:#010x} (expected {:#010x})", magic, XEX2_MAGIC), + )); + } + + let module_flags = cursor.read_u32::()?; + let header_size = cursor.read_u32::()?; + let _reserved = cursor.read_u32::()?; + let security_offset = cursor.read_u32::()?; + let header_count = cursor.read_u32::()?; + + let mut optional_headers = Vec::new(); + for _ in 0..header_count { + let key = cursor.read_u32::()?; + let value = cursor.read_u32::()?; + optional_headers.push(Xex2OptionalHeader { key, value }); + } + + // Parse security info + let security_info = if (security_offset as usize) < data.len() { + cursor.seek(SeekFrom::Start(security_offset as u64))?; + Some(parse_security_info(&mut cursor)?) + } else { + None + }; + + // Parse file format info + let file_format_info = parse_file_format_info(data, &optional_headers); + + // Parse import libraries (addresses only; call resolve_imports after decompression) + let import_libraries = parse_import_libraries(data, &optional_headers); + + // Parse execution info + let execution_info = parse_execution_info(data, &optional_headers); + + // Parse original PE name + let original_pe_name = parse_original_pe_name(data, &optional_headers); + + Ok(Xex2Header { + magic, + module_flags, + header_size, + security_offset, + header_count, + optional_headers, + security_info, + file_format_info, + import_libraries, + execution_info, + original_pe_name, + }) +} + +fn parse_security_info(cursor: &mut Cursor<&[u8]>) -> io::Result { + // xex2_security_info layout (from xex2_info.h): + // 0x000: header_size (u32) + // 0x004: image_size (u32) + // 0x008: rsa_signature (0x100 bytes) + // 0x108: unk_108 (u32) + // 0x10C: image_flags (u32) + // 0x110: load_address (u32) + // 0x114: section_digest (0x14 bytes) + // 0x128: import_table_count (u32) + // 0x12C: import_table_digest (0x14 bytes) + // 0x140: xgd2_media_id (0x10 bytes) + // 0x150: aes_key (0x10 bytes) + // 0x160: export_table (u32) + // 0x164: header_digest (0x14 bytes) + // 0x178: region (u32) + // 0x17C: allowed_media_types (u32) + // 0x180: page_descriptor_count (u32) + // 0x184: page_descriptors[] (each is 0x18 bytes: u32 value + 0x14 digest) + + let _header_size = cursor.read_u32::()?; // 0x000 + let image_size = cursor.read_u32::()?; // 0x004 + + // Skip RSA signature (0x100 bytes) + let mut rsa_sig = [0u8; 0x100]; + cursor.read_exact(&mut rsa_sig)?; // 0x008 + + let _unk_108 = cursor.read_u32::()?; // 0x108 + let image_flags = cursor.read_u32::()?; // 0x10C + let load_address = cursor.read_u32::()?; // 0x110 + + // Skip section_digest (0x14 bytes) + let mut digest = [0u8; 0x14]; + cursor.read_exact(&mut digest)?; // 0x114 + + let _import_table_count = cursor.read_u32::()?; // 0x128 + + // Skip import_table_digest (0x14 bytes) + cursor.read_exact(&mut digest)?; // 0x12C + + // Skip xgd2_media_id (0x10 bytes) + let mut media_id = [0u8; 0x10]; + cursor.read_exact(&mut media_id)?; // 0x140 + + // Read aes_key (0x10 bytes) + let mut aes_key = [0u8; 0x10]; + cursor.read_exact(&mut aes_key)?; // 0x150 + + let export_table_address = cursor.read_u32::()?; // 0x160 + + // Skip header_digest (0x14 bytes) + cursor.read_exact(&mut digest)?; // 0x164 + + let _region = cursor.read_u32::()?; // 0x178 + let _allowed_media = cursor.read_u32::()?; // 0x17C + + let page_descriptor_count = cursor.read_u32::()?; // 0x180 + + let mut page_descriptors = Vec::new(); + for _ in 0..page_descriptor_count { + let size_and_info = cursor.read_u32::()?; + // Skip data_digest (0x14 bytes per descriptor) + cursor.read_exact(&mut digest)?; + page_descriptors.push(Xex2PageDescriptor { size_and_info }); + } + + Ok(Xex2SecurityInfo { + image_size, + load_address, + export_table_address, + image_flags, + aes_key, + page_descriptors, + }) +} + +/// Parse file format info from the optional header data. +fn parse_file_format_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option { + // The key format: low 8 bits indicate the data size category + // 0xFF = data offset is a pointer to variable-size data in the header area + let header = headers.iter().find(|h| h.key == header_keys::FILE_FORMAT_INFO)?; + let offset = header.value as usize; + if offset + 8 > data.len() { + return None; + } + + let mut cursor = Cursor::new(data); + cursor.seek(SeekFrom::Start(offset as u64)).ok()?; + + let info_size = cursor.read_u32::().ok()?; + let encryption_type = cursor.read_u16::().ok()?; + let compression_type = cursor.read_u16::().ok()?; + + let mut basic_blocks = Vec::new(); + let mut normal_window_size = 0u32; + let mut normal_first_block_size = 0u32; + let mut normal_first_block_hash = [0u8; 20]; + + match compression_type { + COMPRESSION_BASIC => { + // Basic compression blocks: (data_size, zero_size) pairs + // Number of blocks = (info_size - 8) / 8 + let block_count = if info_size > 8 { (info_size - 8) / 8 } else { 0 }; + for _ in 0..block_count { + let data_size = cursor.read_u32::().ok()?; + let zero_size = cursor.read_u32::().ok()?; + basic_blocks.push(BasicCompressionBlock { data_size, zero_size }); + } + } + COMPRESSION_NORMAL => { + normal_window_size = cursor.read_u32::().ok()?; + // Read first_block: block_size (4) + block_hash (20) + normal_first_block_size = cursor.read_u32::().ok()?; + cursor.read_exact(&mut normal_first_block_hash).ok()?; + } + _ => {} + } + + Some(FileFormatInfo { + info_size, + encryption_type, + compression_type, + basic_blocks, + normal_window_size, + normal_first_block_size, + normal_first_block_hash, + }) +} + +/// Parse import libraries from the optional header data. +/// At this stage, only record addresses are read; ordinals and record types +/// are resolved later by `resolve_imports` once the PE image is decompressed. +fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec { + let header = match headers.iter().find(|h| h.key == header_keys::IMPORT_LIBRARIES) { + Some(h) => h, + None => return Vec::new(), + }; + + let offset = header.value as usize; + if offset + 12 > data.len() { + return Vec::new(); + } + + fn be_u32(data: &[u8], off: usize) -> u32 { + u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]) + } + fn be_u16(data: &[u8], off: usize) -> u16 { + u16::from_be_bytes([data[off], data[off+1]]) + } + + let total_size = be_u32(data, offset) as usize; + let string_table_size = be_u32(data, offset + 4) as usize; + let string_count = be_u32(data, offset + 8) as usize; + + // Parse string table (null-terminated, 4-byte aligned) + let string_data_start = offset + 12; + let mut strings = Vec::new(); + let mut spos = 0usize; + for _ in 0..string_count { + let start = string_data_start + spos; + let mut end = start; + while end < data.len() && data[end] != 0 { end += 1; } + let name = std::str::from_utf8(&data[start..end]).unwrap_or("???").to_string(); + spos += name.len() + 1; + // 4-byte alignment + if spos % 4 != 0 { spos += 4 - (spos % 4); } + strings.push(name); + } + + // Parse libraries + let mut libs = Vec::new(); + let mut lib_off = offset + 12 + string_table_size; + + while lib_off + 0x28 <= data.len() && lib_off < offset + total_size { + let lib_size = be_u32(data, lib_off) as usize; + if lib_size == 0 { break; } + + let id = be_u32(data, lib_off + 0x18); + let version_cur = be_u32(data, lib_off + 0x1C); + let version_min = be_u32(data, lib_off + 0x20); + let name_index = (be_u16(data, lib_off + 0x24) & 0xFF) as usize; + let count = be_u16(data, lib_off + 0x26) as usize; + + let lib_name = strings.get(name_index).cloned().unwrap_or_else(|| format!("lib_{name_index}")); + + let mut imports = Vec::new(); + for i in 0..count { + let record_addr = be_u32(data, lib_off + 0x28 + i * 4); + imports.push(ImportEntry { + ordinal: 0, + record_type: 0xFF, + address: record_addr, + }); + } + + libs.push(ImportLibrary { + name: lib_name, + id, + version_min, + version_cur, + imports, + }); + lib_off += lib_size; + } + + libs +} + +/// Resolve import ordinals and record types from the decompressed PE image. +/// Must be called after `load_image` provides the PE data. +pub fn resolve_imports(header: &mut Xex2Header, pe_image: &[u8]) { + let image_base = get_image_base(header).unwrap_or(0); + + for lib in &mut header.import_libraries { + for imp in &mut lib.imports { + let pe_off = imp.address.wrapping_sub(image_base) as usize; + if pe_off + 4 <= pe_image.len() { + // PE image values are big-endian (Xbox 360 native) + let val = u32::from_be_bytes([ + pe_image[pe_off], pe_image[pe_off+1], + pe_image[pe_off+2], pe_image[pe_off+3], + ]); + imp.record_type = ((val >> 24) & 0xFF) as u8; + imp.ordinal = (val & 0xFFFF) as u16; + } + } + } +} + +/// Parse execution info from optional header data. +fn parse_execution_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option { + // EXECUTION_INFO key is 0x00040006 — the low byte 0x06 means the value + // is an inline struct of 6 u32 words (24 bytes total). + // Layout: media_id(4), version(4), base_version(4), title_id(4), + // platform(1), exec_type(1), disc_number(1), disc_count(1) + let header = headers.iter().find(|h| h.key == header_keys::EXECUTION_INFO)?; + let off = header.value as usize; + if off + 20 > data.len() { + return None; + } + + let media_id = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); + let title_id = u32::from_be_bytes([data[off+12], data[off+13], data[off+14], data[off+15]]); + let disc_number = data[off + 18]; + let disc_count = data[off + 19]; + + Some(ExecutionInfo { + media_id, + title_id, + disc_number, + disc_count, + }) +} + +/// Parse original PE name from optional header data. +fn parse_original_pe_name(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option { + let header = headers.iter().find(|h| h.key == header_keys::ORIGINAL_PE_NAME)?; + let off = header.value as usize; + if off + 4 > data.len() { + return None; + } + + let size = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]) as usize; + if off + size > data.len() || size <= 4 { + return None; + } + + let name_bytes = &data[off + 4..off + size]; + Some(String::from_utf8_lossy(name_bytes).trim_end_matches('\0').to_string()) +} + +/// Get an optional header value by key. +pub fn get_opt_header(header: &Xex2Header, key: u32) -> Option { + header.optional_headers.iter() + .find(|h| h.key == key) + .map(|h| h.value) +} + +/// Get the entry point address from the XEX2 header. +pub fn get_entry_point(header: &Xex2Header) -> Option { + get_opt_header(header, header_keys::ENTRY_POINT) +} + +/// Get the image base address. +pub fn get_image_base(header: &Xex2Header) -> Option { + get_opt_header(header, header_keys::IMAGE_BASE_ADDRESS) +} + +/// Get the default stack size. +pub fn get_stack_size(header: &Xex2Header) -> u32 { + get_opt_header(header, header_keys::DEFAULT_STACK_SIZE).unwrap_or(0x10_0000) // Default 1MB +} + +/// Load the XEX image data into a flat buffer (decompressing if needed). +/// Returns the decompressed image bytes ready to map into guest memory. +pub fn load_image(data: &[u8], header: &Xex2Header) -> io::Result> { + let source = &data[header.header_size as usize..]; + + match &header.file_format_info { + Some(info) if info.compression_type == COMPRESSION_BASIC => { + load_basic_compressed(source, info) + } + Some(info) if info.compression_type == COMPRESSION_NORMAL => { + load_normal_compressed(source, info, header) + } + _ => { + // Uncompressed (or no format info = treat as uncompressed) + Ok(source.to_vec()) + } + } +} + +/// Load basic compressed image data. +fn load_basic_compressed(source: &[u8], info: &FileFormatInfo) -> io::Result> { + // Calculate total uncompressed size + let total_size: u64 = info.basic_blocks.iter() + .map(|b| b.data_size as u64 + b.zero_size as u64) + .sum(); + + let mut output = vec![0u8; total_size as usize]; + let mut src_offset = 0usize; + let mut dst_offset = 0usize; + + for block in &info.basic_blocks { + let data_size = block.data_size as usize; + let zero_size = block.zero_size as usize; + + if src_offset + data_size > source.len() { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + format!("Basic compression block data extends past end of file (src_offset={:#x}, data_size={:#x}, source_len={:#x})", + src_offset, data_size, source.len()), + )); + } + + // Copy data block + if dst_offset + data_size <= output.len() { + output[dst_offset..dst_offset + data_size] + .copy_from_slice(&source[src_offset..src_offset + data_size]); + } + src_offset += data_size; + dst_offset += data_size; + + // Zero-filled gap (already zeroed from vec initialization) + dst_offset += zero_size; + } + + Ok(output) +} + +/// Xbox 360 retail AES key for XEX2 session key decryption. +const XEX2_RETAIL_KEY: [u8; 16] = [ + 0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3, + 0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91, +]; + +/// Xbox 360 devkit AES key (all zeros). +#[allow(dead_code)] +const XEX2_DEVKIT_KEY: [u8; 16] = [0u8; 16]; + +/// AES-128-CBC decryption with zero IV (matching Xbox 360 XEX decryption). +fn aes_decrypt_cbc(key: &[u8; 16], input: &[u8]) -> Vec { + let cipher = Aes128::new(key.into()); + let mut output = vec![0u8; input.len()]; + let mut iv = [0u8; 16]; + + for (i, chunk) in input.chunks(16).enumerate() { + if chunk.len() < 16 { + // Partial block at end - copy as-is + output[i * 16..i * 16 + chunk.len()].copy_from_slice(chunk); + break; + } + let mut block = aes::Block::clone_from_slice(chunk); + cipher.decrypt_block(&mut block); + // XOR with IV (previous ciphertext block) + for j in 0..16 { + block[j] ^= iv[j]; + } + iv.copy_from_slice(chunk); + output[i * 16..(i + 1) * 16].copy_from_slice(&block); + } + + output +} + +/// Derive the session key by decrypting the XEX's aes_key field with the retail key. +/// Falls back to devkit key if retail produces invalid results. +fn derive_session_key(header: &Xex2Header) -> [u8; 16] { + let sec = match &header.security_info { + Some(s) => s, + None => return [0u8; 16], + }; + + let decrypted = aes_decrypt_cbc(&XEX2_RETAIL_KEY, &sec.aes_key); + let mut session_key = [0u8; 16]; + session_key.copy_from_slice(&decrypted[..16]); + session_key +} + +/// De-block compressed data: strip block headers and extract chunk payloads. +/// +/// The first block's size comes from the file format header (first_block_size). +/// Each block in the data starts with a block_info struct for the NEXT block: +/// - block_size: u32 BE (size of the next block) +/// - block_hash: [u8; 20] (SHA1 of the next block) +/// Followed by chunks: { chunk_size: u16 BE, data: [u8; chunk_size] }, terminated by chunk_size=0 +fn deblock(input: &[u8], first_block_size: u32) -> io::Result> { + let mut output = Vec::new(); + let mut pos = 0usize; + let mut cur_block_size = first_block_size as usize; + + while cur_block_size > 0 && pos < input.len() { + let next_block_pos = pos + cur_block_size; + + // Read next block's info from start of current block data + let next_block_size = if pos + 4 <= input.len() { + u32::from_be_bytes([ + input[pos], input[pos + 1], input[pos + 2], input[pos + 3], + ]) as usize + } else { + 0 + }; + + // Skip block_info header (4 bytes size + 20 bytes hash) + let mut p = pos + 4 + 20; + + // Read chunks within this block + loop { + if p + 2 > input.len() { + break; + } + let chunk_size = ((input[p] as usize) << 8) | (input[p + 1] as usize); + p += 2; + if chunk_size == 0 { + break; + } + if p + chunk_size > input.len() { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + format!("De-block chunk extends past input (pos={:#x}, chunk_size={:#x}, input_len={:#x})", + p, chunk_size, input.len()), + )); + } + output.extend_from_slice(&input[p..p + chunk_size]); + p += chunk_size; + } + + if next_block_pos <= pos { + break; // Prevent infinite loop + } + pos = next_block_pos; + cur_block_size = next_block_size; + } + + Ok(output) +} + +/// Load normal (LZX) compressed image data. +/// Pipeline: decrypt → de-block → LZX decompress (pure Rust) +fn load_normal_compressed(source: &[u8], info: &FileFormatInfo, header: &Xex2Header) -> io::Result> { + let uncompressed_size = header.security_info.as_ref() + .map(|s| s.image_size as usize) + .unwrap_or(0); + + if uncompressed_size == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Cannot decompress: image_size is 0", + )); + } + + // Step 1: Decrypt if needed + let decrypted; + let input = if info.encryption_type == ENCRYPTION_NORMAL { + let session_key = derive_session_key(header); + decrypted = aes_decrypt_cbc(&session_key, source); + &decrypted + } else { + source + }; + + // Step 2: De-block (strip block headers, extract chunk payloads) + let deblocked = deblock(input, info.normal_first_block_size)?; + + if deblocked.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "De-blocking produced no data", + )); + } + + // Step 3: LZX decompress using pure Rust decoder + let window_bits = match info.normal_window_size { + s if s == 0 => 15, // default + s => (s as f64).log2() as u32, + }; + + let mut decoder = crate::lzx::LzxDecoder::new(window_bits); + let output = decoder.decompress(&deblocked, uncompressed_size) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("LZX decompression failed: {e}")))?; + + tracing::info!("LZX decompressed: {} -> {} bytes", deblocked.len(), uncompressed_size); + + Ok(output) +} diff --git a/crates/xenia-xex/src/lzx.rs b/crates/xenia-xex/src/lzx.rs new file mode 100644 index 0000000..2a8bf2c --- /dev/null +++ b/crates/xenia-xex/src/lzx.rs @@ -0,0 +1,692 @@ +//! LZX decompressor for Xbox 360 XEX2 "normal compression". +//! Ported from libmspack lzxd.c (C) 2003-2013 Stuart Caie, LGPL 2.1. + +use std::fmt; + +// ── LZX constants ─────────────────────────────────────────────────────────── + +const LZX_MIN_MATCH: usize = 2; +const LZX_NUM_CHARS: usize = 256; +const LZX_BLOCKTYPE_VERBATIM: u8 = 1; +const LZX_BLOCKTYPE_ALIGNED: u8 = 2; +const LZX_BLOCKTYPE_UNCOMPRESSED: u8 = 3; +const LZX_NUM_PRIMARY_LENGTHS: usize = 7; +const LZX_NUM_SECONDARY_LENGTHS: usize = 249; +const LZX_FRAME_SIZE: usize = 32768; +const HUFF_MAXBITS: usize = 16; + +const PRETREE_MAXSYMS: usize = 20; +const PRETREE_TABLEBITS: usize = 6; +const MAINTREE_MAXSYMS: usize = LZX_NUM_CHARS + 290 * 8; // 2576 +const MAINTREE_TABLEBITS: usize = 12; +const LENGTH_MAXSYMS: usize = LZX_NUM_SECONDARY_LENGTHS + 1; // 250 +const LENGTH_TABLEBITS: usize = 12; +const ALIGNED_MAXSYMS: usize = 8; +const ALIGNED_TABLEBITS: usize = 7; +const LENTABLE_SAFETY: usize = 64; + +const BITBUF_WIDTH: u32 = 32; + +// ── Static tables ─────────────────────────────────────────────────────────── + +static POSITION_SLOTS: [u32; 11] = [30, 32, 34, 36, 38, 42, 50, 66, 98, 162, 290]; + +static EXTRA_BITS: [u8; 36] = [ + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, + 15, 15, 16, 16, +]; + +#[rustfmt::skip] +static POSITION_BASE: [u32; 290] = [ + 0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, + 768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384, 24576, 32768, + 49152, 65536, 98304, 131072, 196608, 262144, 393216, 524288, 655360, + 786432, 917504, 1048576, 1179648, 1310720, 1441792, 1572864, 1703936, + 1835008, 1966080, 2097152, 2228224, 2359296, 2490368, 2621440, 2752512, + 2883584, 3014656, 3145728, 3276800, 3407872, 3538944, 3670016, 3801088, + 3932160, 4063232, 4194304, 4325376, 4456448, 4587520, 4718592, 4849664, + 4980736, 5111808, 5242880, 5373952, 5505024, 5636096, 5767168, 5898240, + 6029312, 6160384, 6291456, 6422528, 6553600, 6684672, 6815744, 6946816, + 7077888, 7208960, 7340032, 7471104, 7602176, 7733248, 7864320, 7995392, + 8126464, 8257536, 8388608, 8519680, 8650752, 8781824, 8912896, 9043968, + 9175040, 9306112, 9437184, 9568256, 9699328, 9830400, 9961472, 10092544, + 10223616, 10354688, 10485760, 10616832, 10747904, 10878976, 11010048, + 11141120, 11272192, 11403264, 11534336, 11665408, 11796480, 11927552, + 12058624, 12189696, 12320768, 12451840, 12582912, 12713984, 12845056, + 12976128, 13107200, 13238272, 13369344, 13500416, 13631488, 13762560, + 13893632, 14024704, 14155776, 14286848, 14417920, 14548992, 14680064, + 14811136, 14942208, 15073280, 15204352, 15335424, 15466496, 15597568, + 15728640, 15859712, 15990784, 16121856, 16252928, 16384000, 16515072, + 16646144, 16777216, 16908288, 17039360, 17170432, 17301504, 17432576, + 17563648, 17694720, 17825792, 17956864, 18087936, 18219008, 18350080, + 18481152, 18612224, 18743296, 18874368, 19005440, 19136512, 19267584, + 19398656, 19529728, 19660800, 19791872, 19922944, 20054016, 20185088, + 20316160, 20447232, 20578304, 20709376, 20840448, 20971520, 21102592, + 21233664, 21364736, 21495808, 21626880, 21757952, 21889024, 22020096, + 22151168, 22282240, 22413312, 22544384, 22675456, 22806528, 22937600, + 23068672, 23199744, 23330816, 23461888, 23592960, 23724032, 23855104, + 23986176, 24117248, 24248320, 24379392, 24510464, 24641536, 24772608, + 24903680, 25034752, 25165824, 25296896, 25427968, 25559040, 25690112, + 25821184, 25952256, 26083328, 26214400, 26345472, 26476544, 26607616, + 26738688, 26869760, 27000832, 27131904, 27262976, 27394048, 27525120, + 27656192, 27787264, 27918336, 28049408, 28180480, 28311552, 28442624, + 28573696, 28704768, 28835840, 28966912, 29097984, 29229056, 29360128, + 29491200, 29622272, 29753344, 29884416, 30015488, 30146560, 30277632, + 30408704, 30539776, 30670848, 30801920, 30932992, 31064064, 31195136, + 31326208, 31457280, 31588352, 31719424, 31850496, 31981568, 32112640, + 32243712, 32374784, 32505856, 32636928, 32768000, 32899072, 33030144, + 33161216, 33292288, 33423360, +]; + +// ── Error type ────────────────────────────────────────────────────────────── + +#[derive(Debug)] +pub enum LzxError { + BadHuffmanTable, + Decrunch(String), +} + +impl fmt::Display for LzxError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BadHuffmanTable => write!(f, "failed to build Huffman table"), + Self::Decrunch(msg) => write!(f, "LZX decrunch error: {msg}"), + } + } +} + +impl std::error::Error for LzxError {} + +// ── Bit reader (MSB order, 16-bit LE pairs) ──────────────────────────────── + +struct BitReader<'a> { + data: &'a [u8], + pos: usize, + buf: u32, + left: i32, +} + +impl<'a> BitReader<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0, buf: 0, left: 0 } + } + + /// Inject one 16-bit little-endian pair into MSB bit buffer. + fn fill(&mut self) { + let b0 = if self.pos < self.data.len() { + let b = self.data[self.pos]; self.pos += 1; b as u32 + } else { 0 }; + let b1 = if self.pos < self.data.len() { + let b = self.data[self.pos]; self.pos += 1; b as u32 + } else { 0 }; + let word = (b1 << 8) | b0; + self.buf |= word << (16 - self.left as u32); + self.left += 16; + } + + #[inline] + fn ensure(&mut self, n: i32) { + while self.left < n { self.fill(); } + } + + #[inline] + fn peek(&self, n: u32) -> u32 { + self.buf >> (BITBUF_WIDTH - n) + } + + #[inline] + fn remove(&mut self, n: u32) { + self.buf <<= n; + self.left -= n as i32; + } + + #[inline] + fn read(&mut self, n: u32) -> u32 { + self.ensure(n as i32); + let v = self.peek(n); + self.remove(n); + v + } + + /// Read a raw byte directly (for UNCOMPRESSED blocks). + fn raw_byte(&mut self) -> u8 { + if self.pos < self.data.len() { + let b = self.data[self.pos]; self.pos += 1; b + } else { 0 } + } + + /// Re-align the bitstream at a frame boundary. + fn align_frame(&mut self) { + if self.left > 0 { self.ensure(16); } + let r = self.left & 15; + if r != 0 { self.remove(r as u32); } + } +} + +// ── Huffman table builder (MSB order) ─────────────────────────────────────── + +fn make_decode_table( + nsyms: usize, + nbits: usize, + length: &[u8], + table: &mut [u16], +) -> bool { + let mut pos: usize = 0; + let table_mask = 1usize << nbits; + let mut bit_mask = table_mask >> 1; + + // Short codes: direct mapping + for bit_num in 1..=nbits { + for sym in 0..nsyms { + if length[sym] as usize != bit_num { continue; } + let leaf = pos; + pos += bit_mask; + if pos > table_mask { return true; } + for i in leaf..leaf + bit_mask { + table[i] = sym as u16; + } + } + bit_mask >>= 1; + } + + if pos == table_mask { return false; } + + // Mark remaining entries as unused + for i in pos..table_mask { + table[i] = 0xFFFF; + } + + let mut next_symbol = if (table_mask >> 1) < nsyms { nsyms } else { table_mask >> 1 }; + + let mut pos32 = (pos as u32) << 16; + let table_mask32 = (table_mask as u32) << 16; + let mut bit_mask32: u32 = 1 << 15; + + // Long codes: tree traversal + for bit_num in (nbits + 1)..=HUFF_MAXBITS { + for sym in 0..nsyms { + if length[sym] as usize != bit_num { continue; } + if pos32 >= table_mask32 { return true; } + + let mut leaf = (pos32 >> 16) as usize; + + for fill in 0..(bit_num - nbits) { + if table[leaf] == 0xFFFF { + table[next_symbol << 1] = 0xFFFF; + table[(next_symbol << 1) + 1] = 0xFFFF; + table[leaf] = next_symbol as u16; + next_symbol += 1; + } + leaf = (table[leaf] as usize) << 1; + if (pos32 >> (15 - fill as u32)) & 1 != 0 { + leaf += 1; + } + } + table[leaf] = sym as u16; + pos32 += bit_mask32; + } + bit_mask32 >>= 1; + } + + pos32 != table_mask32 +} + +// ── Huffman symbol decoder ────────────────────────────────────────────────── + +fn read_huffsym( + br: &mut BitReader, + table: &[u16], + lens: &[u8], + tablebits: usize, + maxsyms: usize, +) -> Result { + br.ensure(HUFF_MAXBITS as i32); + let mut sym = table[br.peek(tablebits as u32) as usize] as usize; + if sym >= maxsyms { + let mut i: u32 = 1 << (BITBUF_WIDTH - tablebits as u32); + loop { + i >>= 1; + if i == 0 { return Err(LzxError::BadHuffmanTable); } + sym = table[(sym << 1) | if br.buf & i != 0 { 1 } else { 0 }] as usize; + if sym < maxsyms { break; } + } + } + br.remove(lens[sym] as u32); + Ok(sym) +} + +// ── LZX decoder state ─────────────────────────────────────────────────────── + +pub struct LzxDecoder { + window: Vec, + window_size: usize, + window_posn: usize, + frame_posn: usize, + frame: usize, + num_offsets: usize, + + r0: u32, + r1: u32, + r2: u32, + + block_type: u8, + block_length: usize, + block_remaining: usize, + + header_read: bool, + intel_filesize: i32, + intel_curpos: i32, + intel_started: bool, + + // Huffman code lengths + pretree_len: Vec, + maintree_len: Vec, + length_len: Vec, + aligned_len: Vec, + + // Huffman decode tables + pretree_table: Vec, + maintree_table: Vec, + length_table: Vec, + aligned_table: Vec, + + length_empty: bool, +} + +impl LzxDecoder { + pub fn new(window_bits: u32) -> Self { + assert!((15..=21).contains(&window_bits)); + let window_size = 1usize << window_bits; + let num_offsets = (POSITION_SLOTS[(window_bits - 15) as usize] as usize) << 3; + + Self { + window: vec![0u8; window_size], + window_size, + window_posn: 0, + frame_posn: 0, + frame: 0, + num_offsets, + r0: 1, r1: 1, r2: 1, + block_type: 0, + block_length: 0, + block_remaining: 0, + header_read: false, + intel_filesize: 0, + intel_curpos: 0, + intel_started: false, + pretree_len: vec![0u8; PRETREE_MAXSYMS + LENTABLE_SAFETY], + maintree_len: vec![0u8; MAINTREE_MAXSYMS + LENTABLE_SAFETY], + length_len: vec![0u8; LENGTH_MAXSYMS + LENTABLE_SAFETY], + aligned_len: vec![0u8; ALIGNED_MAXSYMS + LENTABLE_SAFETY], + pretree_table: vec![0u16; (1 << PRETREE_TABLEBITS) + PRETREE_MAXSYMS * 2], + maintree_table: vec![0u16; (1 << MAINTREE_TABLEBITS) + MAINTREE_MAXSYMS * 2], + length_table: vec![0u16; (1 << LENGTH_TABLEBITS) + LENGTH_MAXSYMS * 2], + aligned_table: vec![0u16; (1 << ALIGNED_TABLEBITS) + ALIGNED_MAXSYMS * 2], + length_empty: false, + } + } + + fn build_table( + lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize, + ) -> Result<(), LzxError> { + if make_decode_table(maxsyms, tablebits, lens, table) { + Err(LzxError::BadHuffmanTable) + } else { + Ok(()) + } + } + + fn build_table_maybe_empty( + lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize, + ) -> Result { + if make_decode_table(maxsyms, tablebits, lens, table) { + // Check if table is simply empty (all lengths zero) + for i in 0..maxsyms { + if lens[i] > 0 { + return Err(LzxError::BadHuffmanTable); + } + } + Ok(true) // empty + } else { + Ok(false) // not empty + } + } + + /// Read Huffman code lengths using the pretree (lzxd_read_lens). + fn read_lens( + br: &mut BitReader, + lens: &mut [u8], + pretree_len: &mut [u8], + pretree_table: &mut [u16], + first: usize, + last: usize, + ) -> Result<(), LzxError> { + // Build pretree: 20 symbols, 4 bits each + for i in 0..20 { + pretree_len[i] = br.read(4) as u8; + } + Self::build_table(pretree_len, pretree_table, PRETREE_MAXSYMS, PRETREE_TABLEBITS)?; + + let mut x = first; + while x < last { + let z = read_huffsym(br, pretree_table, pretree_len, PRETREE_TABLEBITS, PRETREE_MAXSYMS)?; + if z == 17 { + // Run of zeros: [read 4 bits] + 4 + let mut y = br.read(4) as usize + 4; + while y > 0 && x < last { lens[x] = 0; x += 1; y -= 1; } + } else if z == 18 { + // Run of zeros: [read 5 bits] + 20 + let mut y = br.read(5) as usize + 20; + while y > 0 && x < last { lens[x] = 0; x += 1; y -= 1; } + } else if z == 19 { + // Run of same: [read 1 bit] + 4, then read symbol + let mut y = br.read(1) as usize + 4; + let z2 = read_huffsym(br, pretree_table, pretree_len, PRETREE_TABLEBITS, PRETREE_MAXSYMS)?; + let mut val = lens[x] as i32 - z2 as i32; + if val < 0 { val += 17; } + while y > 0 && x < last { lens[x] = val as u8; x += 1; y -= 1; } + } else { + // Delta: code 0..16 + let mut val = lens[x] as i32 - z as i32; + if val < 0 { val += 17; } + lens[x] = val as u8; + x += 1; + } + } + Ok(()) + } + + /// Decompress the full LZX stream into the output buffer. + pub fn decompress(&mut self, input: &[u8], output_len: usize) -> Result, LzxError> { + let mut br = BitReader::new(input); + let mut output = Vec::with_capacity(output_len); + let mut offset: usize = 0; + + let end_frame = (output_len / LZX_FRAME_SIZE) + 1; + + while self.frame < end_frame { + // Read header once + if !self.header_read { + let i_bit = br.read(1); + let (hi, lo) = if i_bit != 0 { + (br.read(16), br.read(16)) + } else { + (0, 0) + }; + self.intel_filesize = ((hi << 16) | lo) as i32; + self.header_read = true; + } + + // Frame size + let frame_size = if output_len > 0 && (output_len - offset) < LZX_FRAME_SIZE { + output_len - offset + } else { + LZX_FRAME_SIZE + }; + + let mut bytes_todo = (self.frame_posn + frame_size).wrapping_sub(self.window_posn) as i32; + + while bytes_todo > 0 { + // New block? + if self.block_remaining == 0 { + // Realign after odd UNCOMPRESSED block + if self.block_type == LZX_BLOCKTYPE_UNCOMPRESSED && (self.block_length & 1) != 0 { + br.raw_byte(); + } + // Read block type (3 bits) and length (24 bits) + self.block_type = br.read(3) as u8; + let hi = br.read(16) as usize; + let lo = br.read(8) as usize; + self.block_length = (hi << 8) | lo; + self.block_remaining = self.block_length; + + match self.block_type { + LZX_BLOCKTYPE_ALIGNED => { + for i in 0..8 { self.aligned_len[i] = br.read(3) as u8; } + Self::build_table(&self.aligned_len, &mut self.aligned_table, ALIGNED_MAXSYMS, ALIGNED_TABLEBITS)?; + // Fall through to verbatim tree reading + Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 0, 256)?; + Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?; + Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?; + if self.maintree_len[0xE8] != 0 { self.intel_started = true; } + Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?; + self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?; + } + LZX_BLOCKTYPE_VERBATIM => { + Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 0, 256)?; + Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?; + Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?; + if self.maintree_len[0xE8] != 0 { self.intel_started = true; } + Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?; + self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?; + } + LZX_BLOCKTYPE_UNCOMPRESSED => { + self.intel_started = true; + // Align to byte boundary + if br.left == 0 { br.ensure(16); } + br.left = 0; + br.buf = 0; + // Read R0, R1, R2 (12 bytes, little-endian u32s) + let mut buf = [0u8; 12]; + for b in &mut buf { *b = br.raw_byte(); } + self.r0 = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]); + self.r1 = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]); + self.r2 = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]); + } + _ => return Err(LzxError::Decrunch("bad block type".into())), + } + } + + let mut this_run = self.block_remaining as i32; + if this_run > bytes_todo { this_run = bytes_todo; } + bytes_todo -= this_run; + self.block_remaining -= this_run as usize; + + let window_size = self.window_size; + + match self.block_type { + LZX_BLOCKTYPE_VERBATIM => { + while this_run > 0 { + let main_element = read_huffsym(&mut br, &self.maintree_table, &self.maintree_len, MAINTREE_TABLEBITS, MAINTREE_MAXSYMS)?; + if main_element < LZX_NUM_CHARS { + self.window[self.window_posn] = main_element as u8; + self.window_posn += 1; + this_run -= 1; + } else { + let me = main_element - LZX_NUM_CHARS; + let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS; + if match_length == LZX_NUM_PRIMARY_LENGTHS { + if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); } + let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?; + match_length += footer; + } + match_length += LZX_MIN_MATCH; + + let mut match_offset = (me >> 3) as u32; + match match_offset { + 0 => match_offset = self.r0, + 1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; } + 2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; } + 3 => { match_offset = 1; self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset; } + _ => { + let extra = if match_offset >= 36 { 17 } else { EXTRA_BITS[match_offset as usize] as u32 }; + let verbatim_bits = br.read(extra); + match_offset = POSITION_BASE[match_offset as usize] - 2 + verbatim_bits; + self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset; + } + } + + if self.window_posn + match_length > window_size { + return Err(LzxError::Decrunch("match overrun".into())); + } + self.copy_match(match_offset as usize, match_length); + this_run -= match_length as i32; + } + } + } + LZX_BLOCKTYPE_ALIGNED => { + while this_run > 0 { + let main_element = read_huffsym(&mut br, &self.maintree_table, &self.maintree_len, MAINTREE_TABLEBITS, MAINTREE_MAXSYMS)?; + if main_element < LZX_NUM_CHARS { + self.window[self.window_posn] = main_element as u8; + self.window_posn += 1; + this_run -= 1; + } else { + let me = main_element - LZX_NUM_CHARS; + let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS; + if match_length == LZX_NUM_PRIMARY_LENGTHS { + if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); } + let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?; + match_length += footer; + } + match_length += LZX_MIN_MATCH; + + let mut match_offset = (me >> 3) as u32; + match match_offset { + 0 => match_offset = self.r0, + 1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; } + 2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; } + _ => { + let extra = if match_offset >= 36 { 17 } else { EXTRA_BITS[match_offset as usize] as u32 }; + match_offset = POSITION_BASE[match_offset as usize] - 2; + if extra > 3 { + let verbatim_bits = br.read(extra - 3); + match_offset += verbatim_bits << 3; + let aligned = read_huffsym(&mut br, &self.aligned_table, &self.aligned_len, ALIGNED_TABLEBITS, ALIGNED_MAXSYMS)?; + match_offset += aligned as u32; + } else if extra == 3 { + let aligned = read_huffsym(&mut br, &self.aligned_table, &self.aligned_len, ALIGNED_TABLEBITS, ALIGNED_MAXSYMS)?; + match_offset += aligned as u32; + } else if extra > 0 { + let verbatim_bits = br.read(extra); + match_offset += verbatim_bits; + } else { + match_offset = 1; + } + self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset; + } + } + + if self.window_posn + match_length > window_size { + return Err(LzxError::Decrunch("match overrun".into())); + } + self.copy_match(match_offset as usize, match_length); + this_run -= match_length as i32; + } + } + } + LZX_BLOCKTYPE_UNCOMPRESSED => { + let run = this_run as usize; + for _ in 0..run { + self.window[self.window_posn] = br.raw_byte(); + self.window_posn += 1; + } + } + _ => return Err(LzxError::Decrunch("bad block type in decode".into())), + } + + // Overrun accounting + if this_run < 0 { + let overrun = (-this_run) as usize; + if overrun > self.block_remaining { + return Err(LzxError::Decrunch("overrun past block end".into())); + } + self.block_remaining -= overrun; + } + } + + // Frame boundary check + if (self.window_posn.wrapping_sub(self.frame_posn)) != frame_size { + return Err(LzxError::Decrunch(format!( + "decode beyond frame: {} != {}", self.window_posn - self.frame_posn, frame_size + ))); + } + + // Re-align bitstream + br.align_frame(); + + // Intel E8 postprocessing + if self.intel_started && self.intel_filesize != 0 + && self.frame <= 32768 && frame_size > 10 + { + let mut e8_buf = vec![0u8; frame_size]; + e8_buf.copy_from_slice(&self.window[self.frame_posn..self.frame_posn + frame_size]); + + let mut i = 0usize; + let limit = frame_size - 10; + let mut curpos = self.intel_curpos; + let filesize = self.intel_filesize; + + while i < limit { + if e8_buf[i] != 0xE8 { i += 1; curpos += 1; continue; } + let abs_off = e8_buf[i+1] as i32 + | (e8_buf[i+2] as i32) << 8 + | (e8_buf[i+3] as i32) << 16 + | (e8_buf[i+4] as i32) << 24; + + if abs_off >= -curpos && abs_off < filesize { + let rel_off = if abs_off >= 0 { abs_off - curpos } else { abs_off + filesize }; + e8_buf[i+1] = rel_off as u8; + e8_buf[i+2] = (rel_off >> 8) as u8; + e8_buf[i+3] = (rel_off >> 16) as u8; + e8_buf[i+4] = (rel_off >> 24) as u8; + } + i += 5; + curpos += 5; + } + self.intel_curpos += frame_size as i32; + + let to_write = frame_size.min(output_len - offset); + output.extend_from_slice(&e8_buf[..to_write]); + offset += to_write; + } else { + if self.intel_filesize != 0 { self.intel_curpos += frame_size as i32; } + let to_write = frame_size.min(output_len - offset); + output.extend_from_slice(&self.window[self.frame_posn..self.frame_posn + to_write]); + offset += to_write; + } + + // Advance frame + self.frame_posn += frame_size; + self.frame += 1; + if self.window_posn == self.window_size { self.window_posn = 0; } + if self.frame_posn == self.window_size { self.frame_posn = 0; } + } + + Ok(output) + } + + /// Copy a match from the window (handles wrap-around). + fn copy_match(&mut self, match_offset: usize, match_length: usize) { + let window_size = self.window_size; + let mut remaining = match_length; + + if match_offset > self.window_posn { + // Source wraps around window end + let j = match_offset - self.window_posn; + let mut src = window_size - j; + if j < remaining { + remaining -= j; + for _ in 0..j { + self.window[self.window_posn] = self.window[src]; + self.window_posn += 1; + src += 1; + } + src = 0; // wrap to start + } + for _ in 0..remaining { + self.window[self.window_posn] = self.window[src]; + self.window_posn += 1; + src += 1; + } + } else { + let mut src = self.window_posn - match_offset; + for _ in 0..remaining { + self.window[self.window_posn] = self.window[src]; + self.window_posn += 1; + src += 1; + } + } + } +} diff --git a/crates/xenia-xex/src/pe.rs b/crates/xenia-xex/src/pe.rs new file mode 100644 index 0000000..e7ec272 --- /dev/null +++ b/crates/xenia-xex/src/pe.rs @@ -0,0 +1,68 @@ +//! Minimal PE parser for Xbox 360 executables. +//! PE headers are little-endian even on the big-endian Xbox 360. + +use serde::Serialize; + +#[derive(Serialize, Debug, Clone)] +pub struct PeSection { + pub name: String, + pub virtual_address: u32, + pub virtual_size: u32, + pub raw_offset: u32, + pub raw_size: u32, + pub flags: u32, +} + +impl PeSection { + pub fn is_code(&self) -> bool { + self.flags & 0x20000000 != 0 // IMAGE_SCN_MEM_EXECUTE + } +} + +fn le_u16(data: &[u8], off: usize) -> u16 { + u16::from_le_bytes([data[off], data[off + 1]]) +} + +fn le_u32(data: &[u8], off: usize) -> u32 { + u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) +} + +pub fn parse_sections(pe: &[u8]) -> anyhow::Result> { + anyhow::ensure!(pe.len() >= 64, "PE too small"); + anyhow::ensure!(pe[0] == b'M' && pe[1] == b'Z', "not a PE (bad MZ)"); + + let e_lfanew = le_u32(pe, 0x3C) as usize; + anyhow::ensure!(e_lfanew + 4 <= pe.len(), "e_lfanew out of bounds"); + + let nt_sig = le_u32(pe, e_lfanew); + anyhow::ensure!(nt_sig == 0x00004550, "bad PE signature: 0x{nt_sig:08X}"); + + let file_header_off = e_lfanew + 4; + let num_sections = le_u16(pe, file_header_off + 2) as usize; + let opt_header_size = le_u16(pe, file_header_off + 16) as usize; + + let section_table_off = file_header_off + 20 + opt_header_size; + + let mut sections = Vec::new(); + for i in 0..num_sections { + let s = section_table_off + i * 40; + if s + 40 > pe.len() { break; } + + let name_bytes = &pe[s..s + 8]; + let name = std::str::from_utf8(name_bytes) + .unwrap_or("???") + .trim_end_matches('\0') + .to_string(); + + sections.push(PeSection { + name, + virtual_size: le_u32(pe, s + 8), + virtual_address: le_u32(pe, s + 12), + raw_size: le_u32(pe, s + 16), + raw_offset: le_u32(pe, s + 20), + flags: le_u32(pe, s + 36), + }); + } + + Ok(sections) +}