Merge remote-tracking branch 'origin/master' into feat/reachable-main-discovery-douglas

# Conflicts:
#	docs/config_schema.md
#	recompiler/CMakeLists.txt
#	recompiler/src/main_psx.cpp
This commit is contained in:
Matthew Stanley
2026-07-14 00:09:56 -07:00
33 changed files with 1673 additions and 47 deletions
+32
View File
@@ -87,6 +87,13 @@ out_dir = "generated" # both
strict = true # both — currently always true
discovery = "whole-image" # game-only: "whole-image" or "reachable"
out_stem = "SCPH1001" # optional; overrides the auto-derived stem
[[recompiler.patch]]
id = "descriptive-policy-name"
address = "0x80012340"
expected = "0x24020002"
replacement = "0x24020001"
note = "Why this game-owned instruction change is required" # optional
```
Output filenames: `<out_dir>/<out_stem>_full.c` and
@@ -108,6 +115,31 @@ uses it as a static-analysis bound. The value must be nonzero, instruction- and
is still loaded from the user's disc. A generated config's canonical final-page
reservation may be slightly larger than the header; it does not widen analysis.
Each `[[recompiler.patch]]` replaces one exact 32-bit MIPS word before function
discovery, control-flow analysis, and normal translation. It is intended for
small, understood game-code changes whose addresses, opcodes, and policy remain
in the game repository. The framework does not contain title IDs or
title-specific addresses.
- `id`, `address`, `expected`, and `replacement` are required hex/string
fields; `note` is optional.
- IDs are case-sensitive and unique within one config.
- Addresses must be four-byte aligned and are unique by the PSX 29-bit physical
address. Thus `0x00012340`, `0x80012340`, and `0xA0012340` are aliases of one
site and cannot define separate patches.
- Main-EXE generation fails if the word at the target site is not `expected`.
This catches a wrong disc revision or stale patch instead of guessing.
- Captured overlays may place unrelated variants at one virtual address. In
overlay mode, a patch is applied only to a variant whose word is `expected`;
a nonmatching variant is translated unchanged.
- When `--config` and `--ws-config` supply the same byte-identical patch it is
deduplicated. Reusing an ID or physical address for different patch data is
an error.
Patches are build-time inputs, not runtime memory writes or live toggles.
Regenerate the affected main executable or captured overlays after changing
them.
## Runtime block
Consumed by the cmake macro `psxrecomp_v4_add_runtime_target` (eventually)
@@ -0,0 +1,33 @@
# Guarded MIPS patch upstream provenance
This focused implementation was rebuilt for current `mstan/psxrecomp` from
Douglas's declarative-patch work in
[`mstan/psxrecomp` PR #15](https://github.com/mstan/psxrecomp/pull/15), exact
source commit
[`19001c0a383edde439988b93eb2385e2d789d350`](https://github.com/douglasjv/psxrecomp-tweaks/commit/19001c0a383edde439988b93eb2385e2d789d350).
The implementation keeps the original design: game-owned instruction data,
an exact opcode guard, replacement before ordinary MIPS-to-C translation, and
guarded overlay variants. This rebuild applies replacements before discovery
and control-flow analysis as well, so patched branches cannot leave the
generated graph based on stale bytes.
The upstreaming scope intentionally includes only:
- the typed `[[recompiler.patch]]` schema and loader;
- physical-alias-aware uniqueness and lookup;
- main-EXE failure on an opcode mismatch;
- overlay nonmatch pass-through;
- focused parser/code-generation tests and schema documentation.
It intentionally excludes all other work from PR #15 and subsequent Douglas
branches: signed widescreen bounds, textured-edge expansion, full-mirror
rendering, OpenGL context changes, CPU-copied-overlay fallback, debug/input
controls, and every game repository or title-specific address. Those concerns
require independent review and validation before any separate upstreaming.
Authorship from the source implementation is retained in the integrating
commit with:
```text
Co-authored-by: douglasjv <vanderveerdouglas@gmail.com>
```
@@ -0,0 +1,29 @@
# kem0x PR 14: encoded CD-ROM interrupt matching
Evaluated and adapted on 2026-07-13.
## Provenance
The CD-ROM interrupt mask fix was rebuilt from Kareem Olim's
[PSXrecomp PR 14](https://github.com/mstan/psxrecomp/pull/14), specifically
commit
[`dd9e5c65073dd41fbac2d965d2f945a866ec59e2`](https://github.com/mstan/psxrecomp/commit/dd9e5c65073dd41fbac2d965d2f945a866ec59e2)
(`Deliver encoded CD interrupt reasons correctly`). The implementation here
centralizes the mask/status comparison in a tested internal helper rather than
copying the source patch verbatim.
The behavior follows the
[PSX-SPX CD-ROM register documentation](https://psx-spx.consoledev.net/cdromdrive/#0x1f801803-read-banks-1-and-3-hintsts):
HINTSTS bits 0..2 are an encoded HC05 interrupt reason, but the interrupt line
is asserted whenever `HINTMSK & HINTSTS` is nonzero. Converting the reason to a
one-hot bit before applying the mask is therefore incorrect. A common `0x07`
mask must deliver encoded INT4 (DataEnd) and INT5 (DiskError) as well as
INT1--INT3.
## Deliberately excluded
This branch does not import PR 14's WebAssembly/web frontend, Pepsiman-specific
hooks or patches, CD-DA playback changes, SPU changes, raster/rendering changes,
or any other runtime and build-system material. Those changes have different
scope and review requirements and are not needed for encoded interrupt mask
matching.
@@ -0,0 +1,38 @@
# kem0x PR #14: SPU END-without-REPEAT behavior
Evaluated on 2026-07-13 against psxrecomp master `dde268dc0fb9daf8fe6529f4aebfe80995350334`.
## Source and credit
- Pull request: [mstan/psxrecomp#14](https://github.com/mstan/psxrecomp/pull/14)
- Evaluated commit: [`b2597df6d5905a0e183b5309a9b84720cd417e19`](https://github.com/mstan/psxrecomp/commit/b2597df6d5905a0e183b5309a9b84720cd417e19)
- Original author: Kareem Olim (`Kareem Olim <kareemolim@gmail.com>`)
The generic behavior identified in that commit is that an ADPCM block carrying
END without REPEAT must redirect decoding to the voice's repeat address, enter
Release, and immediately clear the envelope. This prevents a one-shot voice
from leaking or looping unrelated sample RAM after its terminator.
Current master already implements the same behavior independently in commit
`462e2bf5fdb4d46b935322bf7f782fd424aea584`. Therefore this branch does not
replace that runtime code. It adds a focused regression test that pins the
shared behavior: END-only becomes silent and inactive at the boundary, resumes
decoding at the repeat address, and emits one stop event; END+REPEAT remains
audible and active.
## Explicit exclusions
The following material from the source commit was reviewed and intentionally
not imported:
- `PEPSIMAN_SFX_RETRIGGER_GUARD` and its address-based burst/cooldown policy:
game-specific compensation for Pepsiman timing, not generic SPU behavior.
- The `PSX_WEB` reduction of `SPU_EVENT_CAP`: WebAssembly memory sizing is
unrelated to ADPCM END semantics and belongs in a separate change.
- `SpuVoice.start_addr`: introduced for the excluded retrigger policy and not
needed for the generic END transition.
- All other PR #14 changes: CD-ROM interrupts, CD-DA, raster timing, build
changes, and game-specific hooks require independent review and branches.
No Pepsiman compile-time hook or game-specific playback heuristic is present in
this branch.
@@ -0,0 +1,41 @@
# MSVC portability from Shaneomac PR #16
## Source and credit
- Source PR: [mstan/psxrecomp#16](https://github.com/mstan/psxrecomp/pull/16)
- Source commit: [`df4cc5c70ab1e5d0fd2a8f817dd346905d4d0eeb`](https://github.com/mstan/psxrecomp/commit/df4cc5c70ab1e5d0fd2a8f817dd346905d4d0eeb)
- Author preserved by cherry-pick: Martin Penkava
`<mpenkava1337@gmail.com>`.
- The source commit's Claude Fable 5 co-author trailer is also preserved.
## Adapted scope
The source commit was applied as a focused portability change:
- MSVC bit-scan intrinsics replace unavailable GCC/Clang builtins;
- generated CPS startup markers use an MSVC `.CRT$XCU` initializer and retain
the constructor path for GCC/Clang; and
- runtime globals defined by C translation units receive explicit C linkage in
the C++ entry point.
`recompiler/tests/test_msvc_cps_codegen.py` generates a minimal game dispatch
and checks that both compiler startup paths are emitted.
## Validation in this branch
- Visual Studio 2022/MSVC 19.41 built `psxrecomp-game`, `psxrecomp-bios`, and
`l2_structural_test`; the structural suite passed 44/44.
- The MSVC-built game recompiler passed
`test_msvc_cps_codegen.py` on generated output.
- A freshly generated BIOS runtime compiled all 63 C/C++ object steps under
MSVC, including the new intrinsic and C-linkage paths. The final BIOS-only
link remains blocked by the baseline's unrelated missing
`psx_game_address_in_text` and `psx_game_text_native_ok` definitions. No
change for that pre-existing game-symbol contract is included here.
## Explicitly excluded
No other PR #16 material was imported. In particular, this branch excludes the
SmackDown-specific batch files, dynamic-code discovery fallback, SPU changes,
primitive rejection, Vulkan diagnostics/optimization, and CRT shaders. Those
items require independent review and provenance records.
@@ -0,0 +1,37 @@
# POSIX overlay discovery provenance
This change ports one focused capability from Douglas Vanderveer's PSXRecomp
work: Linux/macOS discovery of cached overlay libraries and manifest-driven
resolution of their `func_XXXXXXXX` exports.
The original implementation was developed while validating the
[R4 recompilation](https://github.com/douglasjv/r4) and appeared in Douglas's
framework commits
[`df7d1faa5f27be0ba357463a763c77efc43e1f91`](https://github.com/douglasjv/psxrecomp-tweaks/commit/df7d1faa5f27be0ba357463a763c77efc43e1f91)
and
[`7abbfdf38b488d3764b96c155549bc930e0521b6`](https://github.com/douglasjv/psxrecomp-tweaks/commit/7abbfdf38b488d3764b96c155549bc930e0521b6).
It is rebuilt here against the newer cache index, ABI tag, codegen namespace,
lazy manifest index, exact native dispatch, and rescan behavior on current
master. The earlier upstream context is
[mstan/psxrecomp PR #15](https://github.com/mstan/psxrecomp/pull/15).
Included scope:
- strict `<addr8>_<crc8>.dll` validation for both cache-key fields;
- POSIX directory enumeration with GCC-before-TCC deduplication;
- POSIX codegen-tag mismatch diagnostics and ABI preflight cleanup;
- manifest-authoritative `dlsym` registration, with failed/empty libraries
closed and successful libraries retained while their function pointers live;
- additive, idempotent rescan behavior inherited from the current loader.
Explicitly excluded are frame splitting, SPU changes, forced-interpreter pages,
macOS GL context changes, CPU-copied overlay fallback, game configuration,
R4 addresses or timing policy, and all other R4-specific work.
The focused Linux test builds a real shared-library fixture, exercises strict
name parsing and directory filtering, detects a sibling codegen tag, resolves a
manifest-style `func_80012345` symbol, and verifies close/reopen behavior:
```sh
bash runtime/tests/run_overlay_posix_test.sh
```
@@ -0,0 +1,33 @@
# Shaneomac PR #16: overlay-loader initialization guard
| Field | Value |
|---|---|
| Upstream PR | <https://github.com/mstan/psxrecomp/pull/16> |
| Source commit | [`e6809ccf7d778a4c2f32d9e27c0ec31a44cbd2ba`](https://github.com/mstan/psxrecomp/commit/e6809ccf7d778a4c2f32d9e27c0ec31a44cbd2ba) |
| Source author | Martin Penkava (`shaneomac1337`) `<mpenkava1337@gmail.com>` |
| Source co-author | Claude Fable 5 `<noreply@anthropic.com>` |
| Port base | `dde268dc0fb9daf8fe6529f4aebfe80995350334` |
| Evaluated/ported | 2026-07-13 (America/Los_Angeles) |
## Included
Only the overlay-loader lifecycle guards from the source commit were adapted.
`overlay_loader_dispatch()`, `overlay_loader_is_candidate()`, and
`overlay_loader_call_native()` now return without consulting overlay indexes
until `overlay_loader_init()` finishes constructing them and sets `s_active`.
The focused regression test is
`runtime/tests/test_overlay_init_guard.py`.
## Explicitly excluded
- The source commit's decodable-word/high-RAM dirty-interpreter fallback in
`runtime/src/dirty_ram_interp.c`.
- All SmackDown-specific behavior and data.
- Every other PR #16 change, including Vulkan, audio/SPU, GPU primitive-size,
MSVC portability, and helper-script work.
The source commit combined the lifecycle guard with a separate discovery
fallback. This port intentionally does not cherry-pick that commit; it adapts
only the three fail-closed `s_active` checks so the framework change remains
game-agnostic and reviewable in isolation.
+52 -13
View File
@@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.20)
project(PSXRecomp C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(CTest)
add_subdirectory(lib/fmt)
@@ -72,6 +73,7 @@ set(PSXRECOMP_GAME_SOURCES
src/code_generator.cpp
src/annotations.cpp
src/config_loader.cpp
src/recompiler_patch.cpp
src/main_psx.cpp
)
@@ -160,6 +162,7 @@ add_executable(psxrecomp-bios
src/function_discovery.cpp
src/full_function_emitter.cpp
src/config_loader.cpp
src/recompiler_patch.cpp
src/main_bios.cpp
)
target_include_directories(psxrecomp-bios PRIVATE
@@ -206,16 +209,52 @@ target_include_directories(l2_structural_test PRIVATE
lib/fmt/include
)
target_link_libraries(l2_structural_test PRIVATE fmt rabbitizer)
# Reachable main-EXE discovery test: parser-bound validation plus exact-entry
# analysis behavior for direct, indirect, seeded, and out-of-bound targets.
add_executable(reachable_discovery_test
tests/reachable_discovery_test.cpp
src/function_analysis.cpp
src/ps1_exe_parser.cpp
)
target_include_directories(reachable_discovery_test PRIVATE
include
lib/fmt/include
)
target_link_libraries(reachable_discovery_test PRIVATE fmt)
if(BUILD_TESTING)
add_executable(recompiler_patch_test
tests/recompiler_patch_test.cpp
src/ps1_exe_parser.cpp
src/mips_decoder.cpp
src/control_flow.cpp
src/function_analysis.cpp
src/code_generator.cpp
src/annotations.cpp
src/config_loader.cpp
src/recompiler_patch.cpp
)
target_include_directories(recompiler_patch_test PRIVATE
include
src
lib/rabbitizer/include
lib/rabbitizer/cplusplus/include
lib/fmt/include
lib/toml11
lib/ELFIO
)
target_link_libraries(recompiler_patch_test PRIVATE fmt rabbitizer)
add_test(NAME recompiler_patch_test COMMAND recompiler_patch_test)
add_executable(recompiler_patch_cxx17_header_test
tests/recompiler_patch_cxx17_header_test.cpp
)
target_include_directories(recompiler_patch_cxx17_header_test PRIVATE src)
set_target_properties(recompiler_patch_cxx17_header_test PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
)
add_test(NAME recompiler_patch_cxx17_header_test
COMMAND recompiler_patch_cxx17_header_test)
endif()
# Reachable main-EXE discovery test: parser-bound validation plus exact-entry
# analysis behavior for direct, indirect, seeded, and out-of-bound targets.
add_executable(reachable_discovery_test
tests/reachable_discovery_test.cpp
src/function_analysis.cpp
src/ps1_exe_parser.cpp
)
target_include_directories(reachable_discovery_test PRIVATE
include
lib/fmt/include
)
target_link_libraries(reachable_discovery_test PRIVATE fmt)
+48
View File
@@ -4,6 +4,7 @@
#include <cctype>
#include <fstream>
#include <set>
#include <stdexcept>
#include <string>
@@ -719,6 +720,52 @@ GameConfig load_game_config(const fs::path& config_path_in) {
out_stem = derive_out_stem(fs::path(exe_field).filename().string());
}
std::vector<RecompilerPatch> recompiler_patches;
if (recomp.contains("patch")) {
const auto& patches = toml::find<toml::array>(recomp, "patch");
std::set<std::string> seen_ids;
std::set<uint32_t> seen_addresses;
for (const auto& item : patches) {
RecompilerPatch patch;
patch.id = toml::find<std::string>(item, "id");
patch.address = parse_hex(toml::find<std::string>(item, "address"),
"recompiler.patch.address");
patch.expected = parse_hex(toml::find<std::string>(item, "expected"),
"recompiler.patch.expected");
patch.replacement = parse_hex(
toml::find<std::string>(item, "replacement"),
"recompiler.patch.replacement");
if (item.contains("note")) {
patch.note = toml::find<std::string>(item, "note");
}
if (patch.id.empty()) {
throw std::runtime_error(
"recompiler.patch.id must not be empty");
}
if ((patch.address & 3u) != 0u) {
throw std::runtime_error(fmt::format(
"{}: recompiler patch '{}' address 0x{:08X} is not "
"instruction-aligned",
config_path.string(), patch.id, patch.address));
}
if (!seen_ids.insert(patch.id).second) {
throw std::runtime_error(fmt::format(
"{}: duplicate [[recompiler.patch]] id '{}'",
config_path.string(), patch.id));
}
const uint32_t address_key =
recompiler_patch_address_key(patch.address);
if (!seen_addresses.insert(address_key).second) {
throw std::runtime_error(fmt::format(
"{}: duplicate [[recompiler.patch]] physical address "
"0x{:08X} (KUSEG/KSEG aliases are one site)",
config_path.string(), address_key));
}
recompiler_patches.push_back(std::move(patch));
}
}
// Optional [widescreen] block — per-game hooks for the widescreen hack.
std::vector<uint32_t> ws_sprite_tag_funcs;
uint32_t ws_sprite_anchor_addr = 0;
@@ -951,6 +998,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
/*strict*/ strict,
/*discovery*/ discovery,
/*out_stem*/ out_stem,
/*recompiler_patches*/ recompiler_patches,
/*runtime*/ parse_runtime_block(cfg, root),
/*ws_sprite_tag_funcs*/ ws_sprite_tag_funcs,
/*ws_sprite_anchor_addr*/ ws_sprite_anchor_addr,
+5
View File
@@ -23,6 +23,8 @@
#include <string>
#include <vector>
#include "recompiler_patch.h"
namespace PSXRecompV4 {
// Pad input mode (per player). Replaces the old analog on/off boolean.
@@ -388,6 +390,9 @@ struct GameConfig {
bool strict;
std::string discovery; // "whole-image" (default) or "reachable"
std::string out_stem; // derived if not explicit
// Game-owned, exact MIPS word replacements. IDs and physical instruction
// addresses are unique within a config; see docs/config_schema.md.
std::vector<RecompilerPatch> recompiler_patches;
// [runtime] block (optional)
RuntimeConfig runtime;
+9 -1
View File
@@ -1656,9 +1656,17 @@ void FullFunctionEmitter::emit_dispatch(
// RECURSION_BUG.md §25 — mark CPS mode at startup for runtime code that
// must emit the CPS contract (the overlay sljit JIT, overlay_sljit.c).
out += "\n/* CPS runtime-mode marker (overlay sljit JIT reads g_psx_cps_mode). */\n";
out += "__attribute__((constructor)) static void psx_cps_mark_bios(void) {\n";
out += "static void psx_cps_mark_bios(void) {\n";
out += " extern int g_psx_cps_mode; g_psx_cps_mode = 1;\n";
out += "}\n";
// Run psx_cps_mark_bios before main(). __attribute__((constructor)) is
// GCC/Clang-only; MSVC uses a static initializer pointer in .CRT$XCU.
out += "#if defined(_MSC_VER)\n";
out += "#pragma section(\".CRT$XCU\", read)\n";
out += "__declspec(allocate(\".CRT$XCU\")) static void (*psx_cps_mark_bios_ctor)(void) = psx_cps_mark_bios;\n";
out += "#else\n";
out += "__attribute__((constructor)) static void psx_cps_mark_bios_ctor(void) { psx_cps_mark_bios(); }\n";
out += "#endif\n";
}
}
+18 -1
View File
@@ -130,6 +130,7 @@ int main(int argc, char** argv) {
std::set<uint32_t> ws_backdrop_unsquash; // [widescreen.backdrop] unsquash_funcs
bool ws_auto_screen_x_cull = false; // [widescreen.cull] auto_screen_x
std::set<uint32_t> persist_init_sites; // [persist_options] init-store hooks (game_options.toml)
std::vector<PSXRecompV4::RecompilerPatch> instruction_patches;
bool ws_auto_backdrop_preload = false; // [widescreen.cull] auto_backdrop
uint32_t ws_bg2d_count_site = 0, ws_bg2d_startcol_site = 0,
ws_bg2d_startx_site = 0,
@@ -149,6 +150,7 @@ int main(int argc, char** argv) {
extra_funcs_storage = cfg.seeds_path.string();
extra_funcs_path = extra_funcs_storage.c_str();
out_dir = cfg.out_dir;
instruction_patches = cfg.recompiler_patches;
ws_tag_funcs.insert(cfg.ws_sprite_tag_funcs.begin(),
cfg.ws_sprite_tag_funcs.end());
ws_cull_bias.insert(cfg.ws_cull_bias_sites.begin(), cfg.ws_cull_bias_sites.end());
@@ -241,6 +243,8 @@ int main(int argc, char** argv) {
ws_backdrop_unsquash.insert(wscfg.ws_backdrop_unsquash_funcs.begin(), wscfg.ws_backdrop_unsquash_funcs.end());
ws_auto_screen_x_cull = ws_auto_screen_x_cull || wscfg.ws_auto_screen_x_cull;
ws_auto_backdrop_preload = ws_auto_backdrop_preload || wscfg.ws_auto_backdrop_preload;
PSXRecompV4::merge_recompiler_patches(
instruction_patches, wscfg.recompiler_patches);
if (wscfg.ws_bg2d_init_func) ws_bg2d_init_func = wscfg.ws_bg2d_init_func;
fmt::print("ws-config: {} (backdrop_x sites={}, unsquash funcs={})\n",
ws_config_path.string(), ws_backdrop_x.size(), ws_backdrop_unsquash.size());
@@ -275,6 +279,11 @@ int main(int argc, char** argv) {
}
}
// Patch the bounded image before discovery/CFG analysis so replacements of
// control-flow instructions cannot disagree with the generated graph.
PSXRecompV4::apply_recompiler_patches_to_executable(
*exe, instruction_patches, overlay_mode);
fmt::print("✓ Successfully parsed PS1-EXE!\n\n");
// Print header information
@@ -1112,9 +1121,17 @@ int main(int argc, char** argv) {
// JIT (overlay_sljit.c) emits the CPS contract. Static ctor: no clash
// with the BIOS dispatch's marker.
ds << "\n/* CPS runtime-mode marker (overlay sljit JIT reads g_psx_cps_mode). */\n";
ds << "__attribute__((constructor)) static void psx_cps_mark_game(void) {\n";
ds << "static void psx_cps_mark_game(void) {\n";
ds << " extern int g_psx_cps_mode; g_psx_cps_mode = 1;\n";
ds << "}\n";
// Run psx_cps_mark_game before main(). __attribute__((constructor)) is
// GCC/Clang-only; MSVC uses a static initializer pointer in .CRT$XCU.
ds << "#if defined(_MSC_VER)\n";
ds << "#pragma section(\".CRT$XCU\", read)\n";
ds << "__declspec(allocate(\".CRT$XCU\")) static void (*psx_cps_mark_game_ctor)(void) = psx_cps_mark_game;\n";
ds << "#else\n";
ds << "__attribute__((constructor)) static void psx_cps_mark_game_ctor(void) { psx_cps_mark_game(); }\n";
ds << "#endif\n";
}
std::ofstream dispatch_file(dispatch_filename);
+99
View File
@@ -0,0 +1,99 @@
#include "recompiler_patch.h"
#include <stdexcept>
#include "fmt/format.h"
#include "ps1_exe_parser.h"
namespace PSXRecompV4 {
namespace {
uint32_t apply_one_recompiler_patch(const RecompilerPatch& patch,
uint32_t address,
uint32_t observed,
bool overlay_mode) {
if (observed == patch.expected) return patch.replacement;
if (overlay_mode) return observed;
throw std::runtime_error(fmt::format(
"recompiler patch '{}' expected 0x{:08X} at 0x{:08X}, found "
"0x{:08X}; wrong game revision or stale patch",
patch.id, patch.expected, address, observed));
}
} // namespace
uint32_t apply_recompiler_patch(const std::vector<RecompilerPatch>& patches,
uint32_t address,
uint32_t observed,
bool overlay_mode) {
const uint32_t address_key = recompiler_patch_address_key(address);
for (const auto& patch : patches) {
if (recompiler_patch_address_key(patch.address) != address_key) continue;
return apply_one_recompiler_patch(patch, address, observed, overlay_mode);
}
return observed;
}
void apply_recompiler_patches_to_executable(
PSXRecomp::PS1Executable& executable,
const std::vector<RecompilerPatch>& patches,
bool overlay_mode) {
const uint32_t load_key =
recompiler_patch_address_key(executable.load_address());
const uint64_t image_end =
static_cast<uint64_t>(load_key) + executable.code_data.size();
for (const auto& patch : patches) {
const uint32_t patch_key = recompiler_patch_address_key(patch.address);
if (patch_key < load_key || static_cast<uint64_t>(patch_key) + 4u > image_end) {
continue; // The patch may target a different captured overlay image.
}
const size_t offset = static_cast<size_t>(patch_key - load_key);
const uint32_t observed =
static_cast<uint32_t>(executable.code_data[offset]) |
(static_cast<uint32_t>(executable.code_data[offset + 1]) << 8) |
(static_cast<uint32_t>(executable.code_data[offset + 2]) << 16) |
(static_cast<uint32_t>(executable.code_data[offset + 3]) << 24);
const uint32_t resolved = apply_one_recompiler_patch(
patch, patch.address, observed, overlay_mode);
if (resolved == observed) continue;
executable.code_data[offset] = static_cast<uint8_t>(resolved);
executable.code_data[offset + 1] = static_cast<uint8_t>(resolved >> 8);
executable.code_data[offset + 2] = static_cast<uint8_t>(resolved >> 16);
executable.code_data[offset + 3] = static_cast<uint8_t>(resolved >> 24);
}
}
void merge_recompiler_patches(std::vector<RecompilerPatch>& destination,
const std::vector<RecompilerPatch>& incoming) {
for (const auto& candidate : incoming) {
bool repeated = false;
for (const auto& existing : destination) {
const bool same_id = existing.id == candidate.id;
const bool same_address =
recompiler_patch_address_key(existing.address) ==
recompiler_patch_address_key(candidate.address);
if (!same_id && !same_address) continue;
const bool identical = same_id && same_address &&
existing.expected == candidate.expected &&
existing.replacement == candidate.replacement &&
existing.note == candidate.note;
if (!identical) {
throw std::runtime_error(fmt::format(
"conflicting recompiler patches '{}' at 0x{:08X} and '{}' "
"at 0x{:08X}",
existing.id, existing.address, candidate.id,
candidate.address));
}
repeated = true;
break;
}
if (!repeated) destination.push_back(candidate);
}
}
} // namespace PSXRecompV4
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace PSXRecomp {
class PS1Executable;
}
namespace PSXRecompV4 {
// One game-owned, opcode-verified instruction replacement. Addresses use PSX
// virtual-address spelling in TOML but compare by their 29-bit physical key, so
// KUSEG/KSEG0/KSEG1 aliases refer to the same instruction site.
struct RecompilerPatch {
std::string id;
uint32_t address = 0;
uint32_t expected = 0;
uint32_t replacement = 0;
std::string note;
};
inline uint32_t recompiler_patch_address_key(uint32_t address) {
return address & 0x1FFFFFFFu;
}
// Apply the patch targeting address, if any. A main-EXE guard mismatch throws;
// an overlay mismatch is an expected different variant and leaves the observed
// instruction unchanged.
uint32_t apply_recompiler_patch(const std::vector<RecompilerPatch>& patches,
uint32_t address,
uint32_t observed,
bool overlay_mode);
// Apply every in-range patch directly to the parsed image before discovery and
// CFG analysis. This keeps control-flow replacements and emitted code in sync.
void apply_recompiler_patches_to_executable(
PSXRecomp::PS1Executable& executable,
const std::vector<RecompilerPatch>& patches,
bool overlay_mode);
// Merge patches received through --config and --ws-config. Byte-identical
// repeats are deduplicated; conflicting IDs or aliased addresses throw.
void merge_recompiler_patches(std::vector<RecompilerPatch>& destination,
const std::vector<RecompilerPatch>& incoming);
} // namespace PSXRecompV4
@@ -0,0 +1,13 @@
#include "recompiler_patch.h"
#include <vector>
int main() {
std::vector<PSXRecompV4::RecompilerPatch> patches;
patches.push_back({"header-smoke", 0x80010000u, 0u, 0u, {}});
return patches.front().id == "header-smoke" &&
PSXRecompV4::recompiler_patch_address_key(
patches.front().address) == 0x00010000u
? 0
: 1;
}
+238
View File
@@ -0,0 +1,238 @@
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <functional>
#include <stdexcept>
#include <string>
#include <vector>
#include "code_generator.h"
#include "config_loader.h"
#include "control_flow.h"
#include "fmt/format.h"
#include "recompiler_patch.h"
namespace fs = std::filesystem;
using PSXRecompV4::RecompilerPatch;
namespace {
int failures = 0;
void check(bool condition, const std::string& name) {
if (condition) {
fmt::print("PASS {}\n", name);
} else {
fmt::print(stderr, "FAIL {}\n", name);
++failures;
}
}
template <typename Fn>
void check_throws(Fn&& fn, const std::string& needle,
const std::string& name) {
try {
fn();
check(false, name + " (did not throw)");
} catch (const std::exception& e) {
check(std::string(e.what()).find(needle) != std::string::npos,
name + " (" + e.what() + ")");
}
}
void append_word(std::vector<uint8_t>& bytes, uint32_t word) {
bytes.push_back(static_cast<uint8_t>(word));
bytes.push_back(static_cast<uint8_t>(word >> 8));
bytes.push_back(static_cast<uint8_t>(word >> 16));
bytes.push_back(static_cast<uint8_t>(word >> 24));
}
std::string generate_first_instruction(uint32_t first_word,
std::vector<RecompilerPatch> patches,
bool overlay_mode) {
constexpr uint32_t base = 0x80010000u;
PSXRecomp::PS1Executable exe{};
exe.header.load_address = base;
exe.header.initial_pc = base;
exe.header.file_size = 20;
append_word(exe.code_data, first_word);
append_word(exe.code_data, 0x00000000u); // branch/nonbranch delay-slot nop
append_word(exe.code_data, 0x24030001u); // addiu v1, zero, 1
append_word(exe.code_data, 0x03E00008u); // jr ra
append_word(exe.code_data, 0x00000000u); // return delay-slot nop
PSXRecompV4::apply_recompiler_patches_to_executable(
exe, patches, overlay_mode);
PSXRecomp::Function function{};
function.start_addr = base;
function.end_addr = base + 20;
function.size = 20;
function.name = "patch_test";
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
const auto cfg = analyzer.analyze_function(function);
PSXRecomp::CodeGenConfig config;
config.overlay_mode = overlay_mode;
PSXRecomp::CodeGenerator generator(exe, config);
return generator.generate_function(function, cfg).full_code;
}
std::string base_config() {
return R"toml([game]
name = "Patch Test"
id = "TEST-00000"
exe = "TEST.EXE"
load_address = "0x80010000"
entry_pc = "0x80010000"
text_size = "0x1000"
stack_base = "0x801FFFF0"
[recompiler]
seeds = "seeds.txt"
out_dir = "generated"
)toml";
}
fs::path write_config(const fs::path& root, const std::string& suffix,
const std::string& patch_tables) {
const fs::path path = root / ("game-" + suffix + ".toml");
std::ofstream file(path, std::ios::binary);
file << base_config() << patch_tables;
file.close();
return path;
}
void parser_tests(const fs::path& root) {
const auto valid = write_config(root, "valid", R"toml(
[[recompiler.patch]]
id = "gameplay-rate"
address = "0x80012340"
expected = "0x24020002"
replacement = "0x24020001"
note = "Test-only fixture"
)toml");
const auto config = PSXRecompV4::load_game_config(valid);
check(config.recompiler_patches.size() == 1,
"parser accepts one guarded patch");
check(config.recompiler_patches[0].id == "gameplay-rate" &&
config.recompiler_patches[0].address == 0x80012340u &&
config.recompiler_patches[0].expected == 0x24020002u &&
config.recompiler_patches[0].replacement == 0x24020001u,
"parser preserves patch fields");
const auto duplicate_address = write_config(root, "duplicate-address", R"toml(
[[recompiler.patch]]
id = "first"
address = "0x80012340"
expected = "0x24020002"
replacement = "0x24020001"
[[recompiler.patch]]
id = "alias"
address = "0xA0012340"
expected = "0x24020002"
replacement = "0x24020001"
)toml");
check_throws(
[&] { (void)PSXRecompV4::load_game_config(duplicate_address); },
"physical address", "parser rejects duplicate aliased addresses");
const auto duplicate_id = write_config(root, "duplicate-id", R"toml(
[[recompiler.patch]]
id = "same-id"
address = "0x80012340"
expected = "0x24020002"
replacement = "0x24020001"
[[recompiler.patch]]
id = "same-id"
address = "0x80012344"
expected = "0x24030002"
replacement = "0x24030001"
)toml");
check_throws([&] { (void)PSXRecompV4::load_game_config(duplicate_id); },
"duplicate [[recompiler.patch]] id",
"parser rejects duplicate IDs");
const auto unaligned = write_config(root, "unaligned", R"toml(
[[recompiler.patch]]
id = "unaligned"
address = "0x80012342"
expected = "0x24020002"
replacement = "0x24020001"
)toml");
check_throws([&] { (void)PSXRecompV4::load_game_config(unaligned); },
"instruction-aligned", "parser rejects unaligned sites");
}
void codegen_tests() {
constexpr uint32_t original = 0x24020002u; // addiu v0, zero, 2
constexpr uint32_t replacement = 0x24020001u; // addiu v0, zero, 1
const RecompilerPatch physical_alias{
"gameplay-rate", 0x00010000u, original, replacement, ""};
const std::string applied =
generate_first_instruction(original, {physical_alias}, false);
check(applied.find("0x24020001") != std::string::npos &&
applied.find("cpu->gpr[2] = 1;") != std::string::npos,
"codegen applies exact patch through physical alias");
check_throws(
[&] { (void)generate_first_instruction(0x24020003u,
{physical_alias}, false); },
"wrong game revision or stale patch",
"main codegen fails on stale opcode guard");
const std::string overlay =
generate_first_instruction(0x24020003u, {physical_alias}, true);
check(overlay.find("0x24020003") != std::string::npos &&
overlay.find("0x24020001") == std::string::npos,
"overlay nonmatching variant remains unchanged");
constexpr uint32_t beq_v0_zero = 0x10400002u;
constexpr uint32_t bne_v0_zero = 0x14400002u;
const RecompilerPatch branch_patch{
"feature-branch", 0x80010000u, beq_v0_zero, bne_v0_zero, ""};
const std::string branch =
generate_first_instruction(beq_v0_zero, {branch_patch}, false);
check(branch.find("cpu->gpr[2] != cpu->gpr[0]") != std::string::npos,
"patch is applied before control-flow analysis");
std::vector<RecompilerPatch> merged{physical_alias};
PSXRecompV4::merge_recompiler_patches(merged, {physical_alias});
check(merged.size() == 1,
"config merge deduplicates an identical patch");
const RecompilerPatch conflicting_id{
"gameplay-rate", 0x00010004u, original, replacement, ""};
check_throws(
[&] {
PSXRecompV4::merge_recompiler_patches(merged, {conflicting_id});
},
"conflicting recompiler patches",
"config merge rejects cross-config ID conflicts");
}
} // namespace
int main() {
const fs::path root = fs::temp_directory_path() /
fmt::format("psxrecomp-patch-test-{}", reinterpret_cast<uintptr_t>(&failures));
fs::remove_all(root);
fs::create_directories(root);
try {
parser_tests(root);
codegen_tests();
} catch (const std::exception& e) {
fmt::print(stderr, "FAIL unexpected exception: {}\n", e.what());
++failures;
}
std::error_code ec;
fs::remove_all(root, ec);
fmt::print("\nGuarded recompiler patches: {}\n",
failures == 0 ? "all tests passed" : "failures detected");
return failures == 0 ? 0 : 1;
}
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Verify that CPS game output carries both MSVC and GCC/Clang startup hooks.
Usage: python test_msvc_cps_codegen.py [--recompiler <psxrecomp-game.exe>]
"""
import argparse
import os
import struct
import subprocess
import sys
import tempfile
LOAD = 0x80010000
def make_exe():
header = bytearray(2048)
header[0:8] = b"PS-X EXE"
struct.pack_into("<I", header, 0x10, LOAD)
struct.pack_into("<I", header, 0x18, LOAD)
body = struct.pack("<II", 0x03E00008, 0x00000000) # jr ra; nop
struct.pack_into("<I", header, 0x1C, len(body))
return bytes(header) + body
def generate_dispatch(recompiler):
with tempfile.TemporaryDirectory() as tmp:
exe = os.path.join(tmp, "tiny.psx")
seeds = os.path.join(tmp, "seeds.txt")
out = os.path.join(tmp, "out")
with open(exe, "wb") as stream:
stream.write(make_exe())
with open(seeds, "w", encoding="utf-8") as stream:
stream.write(f"0x{LOAD:08X}\n")
result = subprocess.run(
[recompiler, exe, "--seeds", seeds, "--out-dir", out],
capture_output=True,
text=True,
)
if result.returncode:
raise RuntimeError(result.stderr or result.stdout)
dispatches = [
os.path.join(out, name)
for name in os.listdir(out)
if name.endswith("_dispatch.c")
]
if len(dispatches) != 1:
raise RuntimeError(
"expected one generated dispatch, found "
f"{[os.path.basename(path) for path in dispatches]}; "
f"generator output:\n{result.stdout}"
)
with open(dispatches[0], encoding="utf-8") as stream:
return stream.read()
def main():
here = os.path.dirname(os.path.abspath(__file__))
default = os.path.normpath(
os.path.join(here, "..", "build", "psxrecomp-game.exe")
)
parser = argparse.ArgumentParser()
parser.add_argument("--recompiler", default=default)
args = parser.parse_args()
if not os.path.isfile(args.recompiler):
print(f"FAIL: recompiler not found: {args.recompiler}", file=sys.stderr)
return 2
try:
generated = generate_dispatch(args.recompiler)
except (OSError, RuntimeError) as exc:
print(f"FAIL: generation failed: {exc}", file=sys.stderr)
return 1
required = (
"static void psx_cps_mark_game(void)",
"#if defined(_MSC_VER)",
'#pragma section(".CRT$XCU", read)',
'__declspec(allocate(".CRT$XCU")) static void '
"(*psx_cps_mark_game_ctor)(void) = psx_cps_mark_game;",
"__attribute__((constructor)) static void psx_cps_mark_game_ctor(void) "
"{ psx_cps_mark_game(); }",
)
missing = [text for text in required if text not in generated]
if missing:
for text in missing:
print(f"FAIL: generated dispatch is missing: {text}", file=sys.stderr)
return 1
print("PASS: generated CPS startup hooks cover MSVC and GCC/Clang.")
return 0
if __name__ == "__main__":
sys.exit(main())
+2
View File
@@ -22,6 +22,8 @@ set(PSXRECOMP_CODEGEN_HASH_SRCS
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/control_flow.cpp
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/basic_block.cpp
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/function_analysis.cpp
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/recompiler_patch.cpp
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/recompiler_patch.h
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/full_function_emitter.cpp
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/full_function_emitter.h
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/strict_translator.cpp
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Parsed <addr8>_<crc8>.dll cache filename. The parser is shared by the
* Windows and POSIX directory walkers so both platforms reject partial hex
* fields and lookalike suffixes identically. */
typedef struct PsxOverlayCacheFile {
char name[22];
char path[768];
uint32_t region_start;
uint32_t content_crc;
uint64_t mtime;
} PsxOverlayCacheFile;
typedef int (*PsxOverlayCacheVisitor)(const PsxOverlayCacheFile *file,
void *opaque);
int psx_overlay_cache_name_parse(const char *name, uint32_t *region_start,
uint32_t *content_crc);
/* POSIX helpers used by overlay_loader.c and its focused regression test.
* The Windows build provides inert stubs; it keeps using Win32 enumeration and
* LoadLibrary while still sharing the strict filename parser above. */
int psx_overlay_posix_scan_cache_dir(const char *dir,
PsxOverlayCacheVisitor visitor,
void *opaque);
int psx_overlay_posix_find_other_cache_tag(const char *base_dir,
const char *expected_tag,
char *found_tag,
size_t found_tag_size);
void *psx_overlay_posix_library_open(const char *path, char *error,
size_t error_size);
void *psx_overlay_posix_library_symbol(void *handle, const char *name);
void *psx_overlay_posix_library_entry(void *handle, uint32_t entry);
void psx_overlay_posix_library_close(void *handle);
#ifdef __cplusplus
}
#endif
+9
View File
@@ -28,6 +28,9 @@
#define PSX_CYC_H
#include <stdint.h>
#if defined(_MSC_VER)
#include <intrin.h> /* MSVC intrinsics: _BitScanForward (no __builtin_ctz) */
#endif
#include "cpu_state.h" /* CPUState (guard-safe: cpu_state.h includes us last) */
#ifdef __cplusplus
@@ -49,7 +52,13 @@ static inline void psx_cyc_base(CPUState* cpu) {
static inline void psx_cyc_deps(CPUState* cpu, uint32_t reg_mask) {
reg_mask &= 0xFFFFFFFEu; /* never touch ReadAbsorb[0] */
while (reg_mask) {
#if defined(_MSC_VER)
unsigned long _psx_ctz_idx;
_BitScanForward(&_psx_ctz_idx, reg_mask);
unsigned n = (unsigned)_psx_ctz_idx;
#else
unsigned n = (unsigned)__builtin_ctz(reg_mask);
#endif
cpu->read_absorb[n] = 0u;
reg_mask &= reg_mask - 1u;
}
+4
View File
@@ -194,6 +194,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/card_data_writes.c
${PSXRECOMP_ROOT}/runtime/src/overlay_capture.c
${PSXRECOMP_ROOT}/runtime/src/overlay_loader.c
${PSXRECOMP_ROOT}/runtime/src/overlay_posix.c
${PSXRECOMP_ROOT}/runtime/src/overlay_compile_worker.c
${PSXRECOMP_ROOT}/runtime/src/overlay_sljit.c
${PSXRECOMP_ROOT}/runtime/src/overlay_backend.c
@@ -506,6 +507,9 @@ function(psxrecomp_add_runtime_target target)
# by opengl32; Phase 2b will load modern GL via SDL_GL_GetProcAddress.
target_link_libraries(${target} PRIVATE ws2_32 dbghelp comdlg32 opengl32)
else()
if(CMAKE_DL_LIBS)
target_link_libraries(${target} PRIVATE ${CMAKE_DL_LIBS})
endif()
find_package(OpenGL)
if(OpenGL_FOUND)
target_link_libraries(${target} PRIVATE OpenGL::GL)
+5 -3
View File
@@ -11,6 +11,7 @@
*/
#include "cdrom.h"
#include "cdrom_irq.h"
#include "dma.h"
#include "spu.h"
#include "event_ring.h"
@@ -578,7 +579,8 @@ static void present_cdrom_irq(void) {
* the guest's command/ack write — and thus from being lost to that ISR's
* own trailing INTC ack. */
if (cdrom_irq_present_delay > 0) return;
if (irq_flag && (irq_enable & (1 << (irq_flag - 1))) && !cdrom_intc_request_latched) {
if (cdrom_irq_mask_matches_reason(irq_enable, irq_flag) &&
!cdrom_intc_request_latched) {
psx_irq_raise(2, irq_flag); /* IRQ_CDROM; detail = CD response/IRQ type */
cdrom_intc_request_latched = 1;
cdrom_intc_latched_generation = cdrom_irq_generation;
@@ -590,7 +592,7 @@ static void present_cdrom_irq(void) {
/* Fire CDROM IRQ into the interrupt controller (explicit, per command/response).
* Trace the masked case here only (low-frequency); refresh stays silent. */
static void fire_cdrom_irq(void) {
if (irq_flag && !(irq_enable & (1 << (irq_flag - 1)))) {
if (irq_flag && !cdrom_irq_mask_matches_reason(irq_enable, irq_flag)) {
trace_cdrom('f', 0, irq_flag, 0);
}
present_cdrom_irq();
@@ -1841,7 +1843,7 @@ uint32_t cdrom_cycles_to_irq(uint32_t i_mask) {
if (!(i_mask & (1u << 2))) return 0xFFFFFFFFu; /* IRQ_CDROM masked */
uint32_t best = 0xFFFFFFFFu;
/* Armed response awaiting presentation (will raise bit2 when delay hits 0). */
if (irq_flag && (irq_enable & (1u << (irq_flag - 1)))) {
if (cdrom_irq_mask_matches_reason(irq_enable, irq_flag)) {
uint32_t d = cdrom_irq_present_delay > 0 ? (uint32_t)cdrom_irq_present_delay : 0u;
if (d < best) best = d;
}
+15
View File
@@ -0,0 +1,15 @@
#ifndef PSXRECOMP_CDROM_IRQ_H
#define PSXRECOMP_CDROM_IRQ_H
#include <stdint.h>
/* HINTSTS bits 0..2 contain an encoded interrupt reason (INT1..INT5), not
* independent one-hot flags. The controller nevertheless gates its IRQ line
* by directly ANDing HINTSTS with HINTMSK. In particular, mask 0x07 enables
* all five encoded reasons, including INT4 and INT5. */
static inline int cdrom_irq_mask_matches_reason(uint8_t irq_enable,
uint8_t irq_reason) {
return (irq_enable & irq_reason & 0x07u) != 0;
}
#endif /* PSXRECOMP_CDROM_IRQ_H */
+15
View File
@@ -93,6 +93,21 @@
extern "C" uint64_t gte_get_exec_count(void);
/* Cross-language globals defined in C translation units. Declared extern "C" at
* file scope so MSVC gives them C linkage (matching the C definitions); without
* this MSVC name-mangles the C++ references and they fail to link. GCC/Clang do
* not mangle namespace-scope variables, so this is a no-op there. The existing
* block-scope `extern` redeclarations inside functions inherit this C linkage. */
extern "C" {
extern uint64_t psx_cycle_count;
extern uint64_t s_frame_count;
extern uint32_t g_overlay_region_floor;
extern int g_psx_cps_mode;
extern uint64_t g_slice_fired, g_slice_irq_taken, g_dirty_ram_insns_run;
extern uint32_t g_slice_exit_pc, g_slice_exit_reason, g_slice_exit_iter;
extern uint32_t g_slice_exit_dispatchable, g_slice_exit_dirty, g_slice_exit_in_text, g_slice_exit_want;
}
/* memory.c */
extern "C" void memory_init(const char* bios_path);
extern "C" void memory_set_sr_ptr(const uint32_t *p);
+153 -29
View File
@@ -8,6 +8,7 @@
#include "interrupts.h"
#include "debug_server.h"
#include "psx_cycles.h"
#include "overlay_posix.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
@@ -18,7 +19,7 @@
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#else
# include <dlfcn.h>
# include <unistd.h>
#endif
/* ============================================================================
@@ -1059,6 +1060,26 @@ static int cache_idx_has_basename(const char *fname) {
const char *overlay_loader_arch_abi(void) { return PSX_OVERLAY_ARCH_ABI; }
#ifndef _WIN32
static int add_posix_cache_file(const PsxOverlayCacheFile *file, void *opaque) {
(void)opaque;
if (s_cache_idx_count >= CACHE_IDX_CAP) {
loader_log("*** CACHE INDEX FULL (%d): further DLLs near %s are being "
"IGNORED — their regions will run interpreted. Raise "
"CACHE_IDX_CAP.", CACHE_IDX_CAP, file->path);
return 1;
}
if (cache_idx_has_basename(file->name)) return 0;
CacheEntry *e = &s_cache_idx[s_cache_idx_count++];
e->region_start = file->region_start;
e->mtime = file->mtime;
e->func_count = 0;
e->indexed_func_count = 0;
snprintf(e->path, sizeof(e->path), "%s", file->path);
return 0;
}
#endif
/* Scan one directory for <addr8>_<crc8>.dll cache entries into the index.
* `dir` is a full directory path. Idempotent (skips already-indexed paths). */
static void scan_one_cache_dir(const char *dir) {
@@ -1069,18 +1090,9 @@ static void scan_one_cache_dir(const char *dir) {
HANDLE h = FindFirstFileA(pattern, &fd);
if (h == INVALID_HANDLE_VALUE) return;
do {
if (strlen(fd.cFileName) != 21) continue; /* 8+1+8+4 = 21 */
/* Validate the <addr8>_<crc8>.dll shape explicitly: region_start 0 is
* LEGAL (the kernel-RAM window starts at phys 0), so a zero parse
* result can't be used as the invalid sentinel. */
int valid = (fd.cFileName[8] == '_');
for (int ci = 0; valid && ci < 8; ci++) {
char c = fd.cFileName[ci];
valid = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') ||
(c >= 'a' && c <= 'f');
}
if (!valid) continue;
uint32_t addr = (uint32_t)strtoul(fd.cFileName, NULL, 16);
uint32_t addr = 0, crc = 0;
if (!psx_overlay_cache_name_parse(fd.cFileName, &addr, &crc)) continue;
(void)crc;
if (s_cache_idx_count >= CACHE_IDX_CAP) {
/* Never-again (the silent-256 truncation): overflowing the index
* means real native coverage is being IGNORED. Shout once. */
@@ -1104,7 +1116,7 @@ static void scan_one_cache_dir(const char *dir) {
} while (FindNextFileA(h, &fd));
FindClose(h);
#else
(void)dir;
psx_overlay_posix_scan_cache_dir(dir, add_posix_cache_file, NULL);
#endif
}
@@ -1152,10 +1164,82 @@ static void warn_on_cgtag_mismatch(const char *tier) {
} while (FindNextFileA(h, &fd));
FindClose(h);
#else
(void)tier;
char base[768], expect[64], found[256];
snprintf(base, sizeof base, "%s/%s/%s/%s",
s_cache_dir, s_game_id, tier, PSX_OVERLAY_ARCH_ABI);
snprintf(expect, sizeof expect, "cg%d_%08x",
PSX_OVERLAY_CODEGEN_VER, (unsigned)PSX_OVERLAY_CODEGEN_HASH);
if (psx_overlay_posix_find_other_cache_tag(base, expect, found,
sizeof(found))) {
loader_log("*** OVERLAY CACHE HASH MISMATCH: this build reads %s/%s but "
"shards exist under %s/%s. The autocompile is writing to a "
"DIFFERENT codegen hash than this runtime reads -> ALL overlays "
"run INTERPRETED (slow). Fix overlay_autocompile_cmd's "
"--recompiler/--runtime-include to match THIS build's framework.",
tier, expect, tier, found);
}
#endif
}
static void cache_idx_remove_path(const char *path) {
for (int i = 0; i < s_cache_idx_count; i++) {
if (strcmp(s_cache_idx[i].path, path) == 0) {
s_cache_idx[i] = s_cache_idx[--s_cache_idx_count];
return;
}
}
}
static void remove_posix_dll_and_manifest(const char *dll_path) {
#ifndef _WIN32
remove(dll_path);
char ranges[800];
int n = snprintf(ranges, sizeof(ranges), "%s", dll_path);
if (n >= 4 && (size_t)n + 4 < sizeof(ranges) &&
strcmp(ranges + n - 4, ".dll") == 0) {
memcpy(ranges + n - 4, ".ranges", 8);
remove(ranges);
}
#else
(void)dll_path;
#endif
}
#ifndef _WIN32
typedef struct PosixAbiSweep {
int purged;
int kept;
} PosixAbiSweep;
static int posix_abi_sweep_file(const PsxOverlayCacheFile *file, void *opaque) {
PosixAbiSweep *sweep = (PosixAbiSweep *)opaque;
char error[256] = {0};
void *handle = psx_overlay_posix_library_open(file->path, error, sizeof(error));
int abi = 0;
if (handle) {
typedef int (*AbiFn)(void);
AbiFn abi_fn = (AbiFn)psx_overlay_posix_library_symbol(handle, "overlay_abi");
abi = abi_fn ? abi_fn() : 0;
psx_overlay_posix_library_close(handle);
}
if (handle && abi == PSX_OVERLAY_ABI_TAG) {
sweep->kept++;
return 0;
}
if (handle)
loader_log("ABI preflight rejecting %s: dll=0x%X runtime=0x%X",
file->path, abi, PSX_OVERLAY_ABI_TAG);
else
loader_log("ABI preflight rejecting unloadable %s: %s",
file->path, error);
remove_posix_dll_and_manifest(file->path);
cache_idx_remove_path(file->path);
sweep->purged++;
return 0;
}
#endif
/* ---- ABI pre-flight sweep (batch purge) ----------------------------------
* A contract-ABI bump (e.g. v9 -> v10) invalidates EVERY cached DLL at once.
* The lazy path handled that per-dispatch: try_load_region -> LoadLibrary ->
@@ -1225,7 +1309,18 @@ static void abi_preflight_sweep(const char *dir) {
FILE_ATTRIBUTE_NORMAL, NULL);
if (m != INVALID_HANDLE_VALUE) CloseHandle(m);
#else
(void)dir;
char marker[900];
snprintf(marker, sizeof marker, "%s/.abi_%08x.ok", dir,
(unsigned)PSX_OVERLAY_ABI_TAG);
if (access(marker, F_OK) == 0) return;
PosixAbiSweep sweep = {0};
psx_overlay_posix_scan_cache_dir(dir, posix_abi_sweep_file, &sweep);
if (sweep.purged)
loader_log("abi preflight: purged %d stale DLL(s), kept %d in %s",
sweep.purged, sweep.kept, dir);
FILE *m = fopen(marker, "wb");
if (m) fclose(m);
#endif
}
@@ -1833,34 +1928,60 @@ static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll
}
#else
static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll) {
(void)man; (void)man_n; (void)dll;
void *h = dlopen(dll_path, RTLD_NOW | RTLD_LOCAL);
if (!h) { loader_log("dlopen(%s) failed: %s", dll_path, dlerror()); return 0; }
char error[256] = {0};
void *h = psx_overlay_posix_library_open(dll_path, error, sizeof(error));
if (!h) { loader_log("dlopen(%s) failed: %s", dll_path, error); return 0; }
/* ABI gate (see the _WIN32 branch). */
typedef int (*AbiFn)(void);
AbiFn abi_fn = (AbiFn)dlsym(h, "overlay_abi");
AbiFn abi_fn = (AbiFn)psx_overlay_posix_library_symbol(h, "overlay_abi");
int abi = abi_fn ? abi_fn() : 0;
if (abi != PSX_OVERLAY_ABI_TAG) {
loader_log("ABI/flavor mismatch in %s: dll=0x%X runtime=0x%X — rejecting "
"and deleting stale cache entry", dll_path, abi,
PSX_OVERLAY_ABI_TAG);
dlclose(h);
remove(dll_path);
psx_overlay_posix_library_close(h);
remove_posix_dll_and_manifest(dll_path);
return 0;
}
typedef void (*InitFn)(const OverlayCallbacks *);
InitFn init_fn = (InitFn)dlsym(h, "overlay_init");
if (!init_fn) { loader_log("no overlay_init in %s", dll_path); dlclose(h); return 0; }
InitFn init_fn = (InitFn)psx_overlay_posix_library_symbol(h, "overlay_init");
if (!init_fn) {
loader_log("no overlay_init in %s", dll_path);
psx_overlay_posix_library_close(h);
return 0;
}
init_fn(&s_callbacks);
OverlayFlushFn flush_fn = (OverlayFlushFn)dlsym(h, "overlay_flush_cycles");
OverlayFlushFn flush_fn = (OverlayFlushFn)psx_overlay_posix_library_symbol(
h, "overlay_flush_cycles");
if (!flush_fn || dll < 0 || dll >= (int)(sizeof(s_dll_flush) / sizeof(s_dll_flush[0]))) {
loader_log("no ABI-v11 cycle flush export in %s", dll_path);
dlclose(h);
psx_overlay_posix_library_close(h);
return 0;
}
s_dll_flush[dll] = flush_fn;
loader_log("%s loaded (posix export scan TODO)", dll_path);
return 0;
/* ELF/Mach-O do not expose a portable export-table walker. The range
* manifest is already the loader's authority, so resolve only its exact
* callable entries. Missing symbols stay interpreted rather than acquiring
* a guessed extent. */
int registered = 0;
for (int i = 0; i < man_n; i++) {
ManFn *m = &man[i];
if (m->entry == 0 || m->n == 0) continue;
OverlayFn fn = (OverlayFn)psx_overlay_posix_library_entry(h, m->entry);
if (!fn) continue;
cand_register(m->entry & 0x1FFFFFFFu, fn, m, dll);
registered++;
}
loader_log("loaded %s -> %d manifest candidates", dll_path, registered);
if (registered == 0) {
s_dll_flush[dll] = NULL;
psx_overlay_posix_library_close(h);
}
/* A successful handle intentionally stays open for process lifetime: the
* registered function and flush pointers refer into it. Rescans are
* idempotent through s_loaded_paths and never acquire a second reference. */
return registered;
}
#endif
@@ -2390,6 +2511,7 @@ static int overlay_find_by_range(uint32_t phys) {
int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) {
uint32_t phys = addr & 0x1FFFFFFFu;
if (!s_active) return 0;
int lazy_loaded = 0;
retry_candidates:
int head = idx_head(phys);
@@ -2883,6 +3005,7 @@ static FpEnt s_fp[FP_CAP];
static uint64_t s_fp_seq = 0;
int overlay_loader_is_candidate(uint32_t phys) {
if (!s_active) return 0;
phys &= 0x1FFFFFFFu;
return idx_head(phys) >= 0 || lazy_has_exact_entry(phys);
}
@@ -3229,7 +3352,8 @@ void overlay_fp_log(uint32_t addr, const uint32_t *in_regs,
* leaks (root cause of the dwarf->overworld native blue screen).
* Returns 1 iff a native candidate ran. */
int overlay_loader_call_native(CPUState *cpu, uint32_t addr) {
if (!s_native_exec) return 0; /* interp mode: keep the legacy inline path */
if (!s_active || !s_native_exec)
return 0; /* inactive/interp mode: keep the legacy inline path */
uint32_t phys = addr & 0x1FFFFFFFu;
if (idx_head(phys) < 0 && !lazy_has_exact_entry(phys))
return 0; /* neither a registered nor an exact cached entry */
+161
View File
@@ -0,0 +1,161 @@
#include "overlay_posix.h"
#include <stdio.h>
#include <string.h>
static int hex_nibble(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
static int parse_hex8(const char *text, uint32_t *value) {
uint32_t parsed = 0;
for (int i = 0; i < 8; i++) {
int nibble = hex_nibble(text[i]);
if (nibble < 0) return 0;
parsed = (parsed << 4) | (uint32_t)nibble;
}
if (value) *value = parsed;
return 1;
}
int psx_overlay_cache_name_parse(const char *name, uint32_t *region_start,
uint32_t *content_crc) {
uint32_t addr = 0, crc = 0;
if (!name || strlen(name) != 21 || name[8] != '_' ||
strcmp(name + 17, ".dll") != 0 ||
!parse_hex8(name, &addr) || !parse_hex8(name + 9, &crc))
return 0;
if (region_start) *region_start = addr;
if (content_crc) *content_crc = crc;
return 1;
}
#ifndef _WIN32
#include <dirent.h>
#include <dlfcn.h>
#include <sys/stat.h>
int psx_overlay_posix_scan_cache_dir(const char *dir,
PsxOverlayCacheVisitor visitor,
void *opaque) {
DIR *dp = opendir(dir);
if (!dp) return 0;
int visited = 0;
struct dirent *de;
while ((de = readdir(dp)) != NULL) {
PsxOverlayCacheFile file;
memset(&file, 0, sizeof(file));
if (!psx_overlay_cache_name_parse(de->d_name, &file.region_start,
&file.content_crc))
continue;
int n = snprintf(file.path, sizeof(file.path), "%s/%s", dir, de->d_name);
if (n < 0 || (size_t)n >= sizeof(file.path)) continue;
memcpy(file.name, de->d_name, sizeof(file.name) - 1);
file.name[sizeof(file.name) - 1] = '\0';
struct stat st;
if (stat(file.path, &st) != 0 || !S_ISREG(st.st_mode)) continue;
file.mtime = (uint64_t)st.st_mtime;
visited++;
if (visitor && visitor(&file, opaque)) break;
}
closedir(dp);
return visited;
}
static int note_first_cache_file(const PsxOverlayCacheFile *file, void *opaque) {
(void)file;
*(int *)opaque = 1;
return 1;
}
int psx_overlay_posix_find_other_cache_tag(const char *base_dir,
const char *expected_tag,
char *found_tag,
size_t found_tag_size) {
DIR *dp = opendir(base_dir);
if (!dp) return 0;
int found = 0;
struct dirent *de;
while (!found && (de = readdir(dp)) != NULL) {
if (strncmp(de->d_name, "cg", 2) != 0 ||
strcmp(de->d_name, expected_tag) == 0)
continue;
char candidate[768];
int n = snprintf(candidate, sizeof(candidate), "%s/%s", base_dir,
de->d_name);
if (n < 0 || (size_t)n >= sizeof(candidate)) continue;
int has_cache_file = 0;
psx_overlay_posix_scan_cache_dir(candidate, note_first_cache_file,
&has_cache_file);
if (!has_cache_file) continue;
if (found_tag && found_tag_size)
snprintf(found_tag, found_tag_size, "%s", de->d_name);
found = 1;
}
closedir(dp);
return found;
}
void *psx_overlay_posix_library_open(const char *path, char *error,
size_t error_size) {
dlerror();
void *handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
if (!handle && error && error_size) {
const char *message = dlerror();
snprintf(error, error_size, "%s", message ? message : "unknown dlopen error");
}
return handle;
}
void *psx_overlay_posix_library_symbol(void *handle, const char *name) {
return handle ? dlsym(handle, name) : NULL;
}
void *psx_overlay_posix_library_entry(void *handle, uint32_t entry) {
char name[32];
snprintf(name, sizeof(name), "func_%08X", entry);
return psx_overlay_posix_library_symbol(handle, name);
}
void psx_overlay_posix_library_close(void *handle) {
if (handle) dlclose(handle);
}
#else
int psx_overlay_posix_scan_cache_dir(const char *dir,
PsxOverlayCacheVisitor visitor,
void *opaque) {
(void)dir; (void)visitor; (void)opaque;
return 0;
}
int psx_overlay_posix_find_other_cache_tag(const char *base_dir,
const char *expected_tag,
char *found_tag,
size_t found_tag_size) {
(void)base_dir; (void)expected_tag; (void)found_tag; (void)found_tag_size;
return 0;
}
void *psx_overlay_posix_library_open(const char *path, char *error,
size_t error_size) {
(void)path; (void)error; (void)error_size;
return NULL;
}
void *psx_overlay_posix_library_symbol(void *handle, const char *name) {
(void)handle; (void)name;
return NULL;
}
void *psx_overlay_posix_library_entry(void *handle, uint32_t entry) {
(void)handle; (void)entry;
return NULL;
}
void psx_overlay_posix_library_close(void *handle) { (void)handle; }
#endif
+9
View File
@@ -3,6 +3,9 @@
#include "psx_cycles.h"
#include "cpu_state.h"
#include <stdlib.h>
#if defined(_MSC_VER)
#include <intrin.h> /* MSVC intrinsics: _BitScanReverse (no __builtin_clz) */
#endif
#include "cdrom.h"
#include "dma.h"
#include "interrupts.h"
@@ -451,7 +454,13 @@ static const uint8_t PSX_MULT_TAB24[24] = {
static inline uint32_t psx_clz32(uint32_t v) {
/* v is never 0 here (callers OR in 0x400). */
#if defined(_MSC_VER)
unsigned long _idx;
_BitScanReverse(&_idx, v); /* index of highest set bit */
return (uint32_t)(31u - _idx);
#else
return (uint32_t)__builtin_clz(v);
#endif
}
uint32_t psx_mult_latency_s(uint32_t rs) { /* MULT (signed): sign-fold magnitude */
+12
View File
@@ -0,0 +1,12 @@
#include <stdint.h>
#if defined(__GNUC__)
#define EXPORT __attribute__((visibility("default")))
#else
#define EXPORT
#endif
EXPORT int overlay_abi(void) { return 0x12345678; }
EXPORT void overlay_init(const void *callbacks) { (void)callbacks; }
EXPORT void overlay_flush_cycles(void) {}
EXPORT uint32_t func_80012345(void) { return 0xC0DEF00Du; }
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd "$(dirname "$0")/../.." && pwd)"
tmp="$(mktemp -d "${TMPDIR:-/tmp}/psxrecomp-posix-overlay.XXXXXX")"
trap 'rm -rf "$tmp"' EXIT
cache="$tmp/cache"
base="$tmp/tags"
expected="cg12_11111111"
other="cg12_22222222"
mkdir -p "$cache/00000000_11111111.dll" "$base/$expected" "$base/$other"
cc -std=c99 -Wall -Wextra -Werror -fPIC -shared \
"$root/runtime/tests/overlay_posix_fixture.c" \
-o "$cache/80010000_DEADBEEF.dll"
: > "$cache/00000000_00000000.dll"
: > "$cache/80010000_DEADBEG0.dll"
: > "$base/$other/80020000_12345678.dll"
cc -std=c99 -Wall -Wextra -Werror \
-I"$root/runtime/include" \
"$root/runtime/tests/test_overlay_posix.c" \
"$root/runtime/src/overlay_posix.c" \
-ldl -o "$tmp/test_overlay_posix"
"$tmp/test_overlay_posix" "$cache" "$base" "$expected" "$other"
+50
View File
@@ -0,0 +1,50 @@
/*
* Validate PS1 CD-ROM interrupt mask matching.
*
* Build:
* cc -std=c99 -Wall -Wextra -Werror -I../src \
* -o test_cdrom_irq_mask test_cdrom_irq_mask.c
*/
#include "cdrom_irq.h"
#include <stdint.h>
#include <stdio.h>
static int failures;
#define CHECK(expected, mask, reason, label) do { \
int actual = cdrom_irq_mask_matches_reason((uint8_t)(mask), \
(uint8_t)(reason)); \
if (actual != (expected)) { \
fprintf(stderr, "FAIL: %s (mask=0x%02X reason=%u expected=%d got=%d)\n",\
(label), (unsigned)(mask), (unsigned)(reason), \
(expected), actual); \
failures++; \
} \
} while (0)
int main(void) {
/* The conventional 0x07 mask must deliver every encoded HC05 reason. */
for (uint8_t reason = 1; reason <= 5; reason++) {
CHECK(1, 0x07, reason, "mask 0x07 enables INT1 through INT5");
}
/* The mask/status comparison is bitwise, not a reason-to-one-hot lookup. */
CHECK(0, 0x01, 2, "bit 0 does not match INT2");
CHECK(1, 0x01, 3, "bit 0 matches encoded INT3");
CHECK(0, 0x02, 4, "bit 1 does not match INT4");
CHECK(1, 0x04, 4, "bit 2 matches encoded INT4");
CHECK(1, 0x01, 5, "bit 0 matches encoded INT5");
CHECK(1, 0x04, 5, "bit 2 matches encoded INT5");
CHECK(0, 0x00, 1, "zero mask suppresses an interrupt");
CHECK(0, 0x07, 0, "reason zero means no interrupt");
CHECK(0, 0x18, 5, "audio-buffer mask bits do not match an HC05 reason");
if (failures) {
fprintf(stderr, "FAILED (%d)\n", failures);
return 1;
}
puts("ALL PASS");
return 0;
}
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Structural regression test for pre-init overlay-loader entry points.
The public dispatch/candidate/native-call entry points can be reached during
early BIOS initialization, before overlay_loader_init() constructs their index
tables. They must fail closed on s_active before consulting those tables.
Usage: python runtime/tests/test_overlay_init_guard.py
Exit 0 = PASS.
"""
from pathlib import Path
import re
import sys
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "runtime" / "src" / "overlay_loader.c"
def function_body(source: str, name: str) -> str:
match = re.search(rf"\b(?:int|void)\s+{re.escape(name)}\s*\([^;]*?\)\s*\{{", source, re.S)
if not match:
raise AssertionError(f"missing function definition: {name}")
start = match.end()
depth = 1
for pos in range(start, len(source)):
if source[pos] == "{":
depth += 1
elif source[pos] == "}":
depth -= 1
if depth == 0:
return source[start:pos]
raise AssertionError(f"unterminated function definition: {name}")
def require_before(body: str, guard_pattern: str, state_pattern: str, name: str) -> None:
guard = re.search(guard_pattern, body)
state = re.search(state_pattern, body)
if not guard:
raise AssertionError(f"{name}: missing pre-init s_active guard")
if not state:
raise AssertionError(f"{name}: test could not find protected index access")
if guard.start() > state.start():
raise AssertionError(f"{name}: s_active guard occurs after index access")
def main() -> int:
source = SOURCE.read_text(encoding="utf-8")
dispatch = function_body(source, "overlay_loader_dispatch")
require_before(
dispatch,
r"if\s*\(\s*!s_active\s*\)\s*return\s+0\s*;",
r"idx_head\s*\(",
"overlay_loader_dispatch",
)
candidate = function_body(source, "overlay_loader_is_candidate")
require_before(
candidate,
r"if\s*\(\s*!s_active\s*\)\s*return\s+0\s*;",
r"idx_head\s*\(",
"overlay_loader_is_candidate",
)
call_native = function_body(source, "overlay_loader_call_native")
require_before(
call_native,
r"if\s*\(\s*!s_active\s*\|\|\s*!s_native_exec\s*\)\s*return\s+0\s*;",
r"idx_head\s*\(",
"overlay_loader_call_native",
)
print("PASS: overlay loader entry points fail closed before initialization")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except AssertionError as exc:
print(f"FAIL: {exc}")
sys.exit(1)
+79
View File
@@ -0,0 +1,79 @@
#include "overlay_posix.h"
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
typedef struct ScanResult {
int count;
uint32_t addr;
uint32_t crc;
} ScanResult;
static int record_cache_file(const PsxOverlayCacheFile *file, void *opaque) {
ScanResult *result = (ScanResult *)opaque;
result->count++;
if (strcmp(file->name, "80010000_DEADBEEF.dll") == 0) {
result->addr = file->region_start;
result->crc = file->content_crc;
}
return 0;
}
static void check_name_parser(void) {
uint32_t addr = 1, crc = 1;
assert(psx_overlay_cache_name_parse("00000000_00000000.dll", &addr, &crc));
assert(addr == 0 && crc == 0);
assert(psx_overlay_cache_name_parse("89abcdef_ABCDEF01.dll", &addr, &crc));
assert(addr == 0x89ABCDEFu && crc == 0xABCDEF01u);
assert(!psx_overlay_cache_name_parse("G0010000_DEADBEEF.dll", &addr, &crc));
assert(!psx_overlay_cache_name_parse("80010000_DEADBEG0.dll", &addr, &crc));
assert(!psx_overlay_cache_name_parse("80010000_DEADBEEF.so", &addr, &crc));
assert(!psx_overlay_cache_name_parse("80010000-DEADBEEF.dll", &addr, &crc));
assert(!psx_overlay_cache_name_parse("80010000_DEADBEEF.dll.extra", &addr, &crc));
}
int main(int argc, char **argv) {
assert(argc == 5);
check_name_parser();
ScanResult result = {0};
assert(psx_overlay_posix_scan_cache_dir(argv[1], record_cache_file, &result) == 2);
assert(result.count == 2);
assert(result.addr == 0x80010000u);
assert(result.crc == 0xDEADBEEFu);
char found[64] = {0};
assert(psx_overlay_posix_find_other_cache_tag(argv[2], argv[3], found,
sizeof(found)) == 1);
assert(strcmp(found, argv[4]) == 0);
char error[256] = {0};
void *handle = psx_overlay_posix_library_open(argv[1], error, sizeof(error));
assert(handle == NULL); /* A directory is not a loadable overlay library. */
char fixture[1024];
snprintf(fixture, sizeof(fixture), "%s/80010000_DEADBEEF.dll", argv[1]);
handle = psx_overlay_posix_library_open(fixture, error, sizeof(error));
assert(handle != NULL);
typedef int (*AbiFn)(void);
typedef uint32_t (*EntryFn)(void);
AbiFn abi = (AbiFn)psx_overlay_posix_library_symbol(handle, "overlay_abi");
EntryFn entry = (EntryFn)psx_overlay_posix_library_entry(handle, 0x80012345u);
assert(abi && abi() == 0x12345678);
assert(entry && entry() == 0xC0DEF00Du);
assert(psx_overlay_posix_library_entry(handle, 0x80099999u) == NULL);
assert(psx_overlay_posix_library_symbol(handle, "func_80099999") == NULL);
psx_overlay_posix_library_close(handle);
/* A close followed by a fresh open must remain usable; the production
* loader closes rejected/empty libraries and retains successful handles. */
handle = psx_overlay_posix_library_open(fixture, error, sizeof(error));
assert(handle != NULL);
psx_overlay_posix_library_close(handle);
puts("overlay POSIX discovery tests passed");
return 0;
}
+131
View File
@@ -0,0 +1,131 @@
/*
* Regression test for an ADPCM END block without REPEAT.
*
* The test includes spu.c so it can stage a voice exactly at the block
* boundary, without exposing emulator internals in the runtime API.
*
* Build from runtime/tests:
* gcc -std=c99 -I../include -o test_spu_end_without_repeat.exe \
* test_spu_end_without_repeat.c
*/
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "../src/spu.c"
/* Dependencies owned by the surrounding runtime are inert in this unit test. */
uint64_t s_frame_count;
uint64_t psx_get_cycle_count(void) { return 0; }
void audio_trace_pcm(int tap, const int16_t *stereo, int frames) {
(void)tap;
(void)stereo;
(void)frames;
}
void audio_trace_event(uint16_t kind, uint32_t a, uint32_t b) {
(void)kind;
(void)a;
(void)b;
}
bool spu_shadow_enabled(void) { return false; }
void spu_shadow_reset(void) {}
void spu_shadow_process(int16_t *canon, int frames) {
(void)canon;
(void)frames;
}
static int failures;
#define CHECK(condition, message) \
do { \
if (!(condition)) { \
fprintf(stderr, "FAIL: %s\n", message); \
failures++; \
} else { \
printf("ok: %s\n", message); \
} \
} while (0)
static void fill_block(uint32_t addr, uint8_t flags, uint8_t packed) {
spu_ram[addr] = 0x00; /* filter 0, shift 0 */
spu_ram[addr + 1u] = flags;
memset(spu_ram + addr + 2u, packed, 14u);
}
static SpuVoice *stage_end_boundary(uint8_t flags, uint32_t repeat_addr) {
SpuVoice *v = &voices[0];
memset(v, 0, sizeof(*v));
v->active = 1;
v->cur_addr = 0x310u; /* the block after the terminator */
v->repeat_addr = repeat_addr;
v->sample_idx = SPU_BLOCK_SAMPLES;
v->env_level = 0x6000u;
v->adsr_phase = ADSR_SUSTAIN;
v->flags = flags;
spu_regs[2] = 0x1000u; /* one decoded sample per output sample */
return v;
}
static void test_end_without_repeat_is_silent(void) {
const uint32_t repeat_addr = 0x120u;
spu_init();
/* Distinguish the required repeat-address decode from an incorrect
* forward decode. Both blocks are deliberately non-silent: the envelope,
* rather than convenient zero sample data, must make the result silent. */
fill_block(repeat_addr, 0x04u, 0x11u);
fill_block(0x310u, 0x00u, 0x77u);
SpuVoice *v = stage_end_boundary(0x01u, repeat_addr);
int16_t sample = voice_next_sample(0);
CHECK(sample == 0, "END without REPEAT emits silence at the boundary");
CHECK(v->env_level == 0, "END without REPEAT forces the envelope to zero");
CHECK(v->adsr_phase == ADSR_RELEASE,
"END without REPEAT enters Release");
CHECK(!v->active, "zero-level Release deactivates the voice");
CHECK(v->samples[0] == 4096,
"decoder resumes at the latched repeat address, not forward RAM");
CHECK(spu_event_total() == 1, "END stop records exactly one event");
SpuEvent event;
CHECK(spu_event_get(&event, 1) == 1 && event.kind == SPU_EV_END_STOP,
"END stop event has the stop kind");
CHECK(event.addr == repeat_addr,
"END stop event reports the redirected repeat address");
(void)voice_next_sample(0);
CHECK(spu_event_total() == 1,
"an inactive one-shot does not retrigger its END transition");
}
static void test_end_with_repeat_keeps_playing(void) {
const uint32_t repeat_addr = 0x180u;
spu_init();
fill_block(repeat_addr, 0x00u, 0x11u);
SpuVoice *v = stage_end_boundary(0x03u, repeat_addr);
int16_t sample = voice_next_sample(0);
CHECK(sample != 0, "END with REPEAT remains audible");
CHECK(v->env_level != 0, "END with REPEAT preserves the envelope");
CHECK(v->adsr_phase == ADSR_SUSTAIN,
"END with REPEAT preserves the ADSR phase");
CHECK(v->active, "END with REPEAT keeps the voice active");
SpuEvent event;
CHECK(spu_event_get(&event, 1) == 1 && event.kind == SPU_EV_END_LOOP,
"END with REPEAT records a loop event");
}
int main(void) {
test_end_without_repeat_is_silent();
test_end_with_repeat_keeps_playing();
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}