mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
@@ -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
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
|
||||
@@ -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; }
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user