Merge pull request #366 from Alexbeav/split/01-standalone-fixes

Standalone fixes from the TAS integration: JALR handoff, conformance harness, UNC paths, disc region, GTE deadline
This commit is contained in:
Matt Stanley
2026-09-14 12:43:11 -07:00
committed by GitHub
32 changed files with 1382 additions and 240 deletions
+51
View File
@@ -0,0 +1,51 @@
# External test-ROM conformance status
Date: 2026-08-21
This lane turns small public PS1 hardware tests into deterministic PSXRecomp
regression fixtures. It boots the exact pinned PS-X EXE from a locally built
disc, captures guest TTY bytes through a bounded debug endpoint, and compares
the assertion log with the upstream hardware-derived result.
## Current result
| Fixture | Assertions | PSXRecomp result | Meaning |
|---|---:|---|---|
| `gte-test-all` | 1,150 | **1,150 passed, 0 failed** | All valid GTE opcodes and tested register semantics match the pinned oracle. |
| `cpu-cop` | 17 | **Blocked after 2 passes** | The strict AOT compiler replaces functions containing intentional invalid COP instructions with `psx_unknown_dispatch` stubs. |
The GTE fixture initially passed zero of its 50 register tests. The oracle then
identified and verified title-neutral corrections for register sign extension,
RES1, FLAG writes, 44-bit MAC overflow, projection overflow flags, MVMVA edge
cases, lighting/color staging, and depth-cue pipeline ordering. The completed
fixture passes from a clean `prepare` -> recompile -> build -> headless run.
The CPU result is not a harness failure. Its first two tests execute and pass;
the next intentionally invalid COP instruction reaches generated function
`0x80010144`, which is a fatal unknown-dispatch stub. A faithful interpreter or
exception-generating fallback for compiler-rejected instructions is required
before this fixture can test the remaining CPU behavior.
## Provenance and boundaries
- Fixture source: `JaCzekanski/ps1-tests`, MIT, release `build-158`, commit
`5f73d61b27a8d51269b54dde1c456d2d2f7dc865`.
- Exact binary/log/archive hashes are in
`tools/conformance_testrom/fixtures.json` and each private run's
`prepare-receipt.json`.
- Fixture binaries and locally owned PlayStation license-sector data are not
committed.
- The upstream expected logs are the automated oracle. A matched physical-PS1
recapture remains the promotion gate for a hardware-qualified receipt.
## Next factory steps
1. Add a nonfatal compiler fallback for deliberately invalid instructions and
finish the 17-test CPU/COP fixture.
2. Have Eagle recapture the hashed synthetic GTE disc on hardware.
3. Add focused CPU, cache, DMA, timer, CD, GPU, SPU, and SIO fixtures one at a
time; each must retain exact input hashes and a machine-readable result.
4. Run this lane before whole-game validation so architectural regressions are
found below the game layer.
Reproduction instructions are in `tools/conformance_testrom/README.md`.
+39
View File
@@ -0,0 +1,39 @@
# GTE deadlines and deferred CPU work
Pepsiman qualification10 first differs at return 1,514: source cycle
856709465, native 856709463, one changed RAM page (1FF000). Both repeats
produce identical RAM/CPU/log captures and later stop after 2,288 returns at
a separate GPU scope guard. The original inputs are unchanged.
Passive pre-fetch tracing is qualified against each untraced control. The
first 96,369 PC/clock entries agree. The following entry is PC 8001E4B8:
source 856413994, native 856413995. The preceding SWC2 reads the result of a
DPCS command issued while prior load-wait credits still overlap CPU steps.
Pending GPR-load values and kernel scratch registers are not assumed equal
at this internal representation boundary.
The runtime calculated its GTE stall from psx_cycle_count before publishing
the instruction's pending base cycle. psx_advance_cycles then published that
pending cycle and added the stale stall, counting the cycle twice. The same
omission could arm a GTE command deadline too early or give an MFC2/CFC2 read
the wrong load-overlap credit. The command latency itself is unchanged:
original Octoshock 2.3 GTE_Instruction returns DPCS cost 8 minus 1.
psx_gte_set, psx_gte_stall and psx_gte_read now publish pending batch and local
charges before reading the clock. This is the same ownership rule already
used by the multiply/divide deadline helpers. It applies to callers using
the shared runtime, including interpreted and generated CPU paths.
The BIOS/disc-free test_gte_deferred.c links the actual psx_cycles.c. Its
short DPCS/ADDIU/SWC2 timing case fails before the fix. It then exercises all
64 latency-table entries, prior command overlap, batch/local/both pending
charges, elapsed/deadline boundaries, read credit and unchanged store credit.
75,661 checks pass at O0 and O2. This fixture verifies shared timing helpers;
full game replays and generated/interpretation coverage remain separate gates.
Diagnosis receipts: pepsiman-boundary1514-cpu-comparison.json,
pepsiman-source-boundary1514-11/passivity.json,
pepsiman-native-boundary1514-12/passivity.json and
gte-deferred-regression-01/{before,O0,O2}.log in the private accuracy campaign.
No frame-specific clock offset, changed input, GTE arithmetic change, observer
reduction or performance tuning is part of this correction.
+32
View File
@@ -0,0 +1,32 @@
# Windows network asset paths
Pepsiman's first native attempt exited before guest execution because BIOS
selection could not open the path returned by its resolver. An independent
fixture using the same WinLibs GCC 16.1 UCRT library reproduced the boundary:
the original `\\server\share\file` was readable, `path.is_absolute()` returned
false, and `std::filesystem::absolute()` returned `D:\server\share\file` without
an error. Python's argument list had preserved the original argument.
The handover corpus already records a similar network-path symptom under
PSX-WIN-003. This reproduction isolates a filesystem resolution defect in
addition to that finding's separate PowerShell argument-boundary problem.
Microsoft documents UNC paths as fully qualified in
[Windows file path formats](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats).
`host_path.h` preserves fully qualified Windows network and device paths
before invoking the standard library's relative-path resolution. Runtime
BIOS, disc and config resolution, CUE payload resolution, and compiler config
and BIOS CLI paths use that shared rule. Relative config values still resolve
against their config root; ordinary relative CLI paths still resolve against
the current directory. No emulated device or timing behavior changes.
`host_path_test` is registered with the recompiler CTest suite. It checks both
UNC separator forms, device namespace paths, mapped drives, relative paths,
config-root isolation and error-code clearing. Its optional path argument
also checks that an owned asset remains readable, without changing it.
The campaign additionally compiled the production disc resolver and verified
that both network spellings preserve the Pepsiman CUE and all eight tracks.
The existing disc resolver, CD-DA and SBI fixtures pass with this correction.
This is a host admission correction, not a TAS pass or PS1 hardware claim.
Full native observations and registered title regressions remain required.
+37
View File
@@ -352,9 +352,30 @@ if(BUILD_TESTING)
target_include_directories(control_flow_metadata_test PRIVATE include)
target_link_libraries(control_flow_metadata_test PRIVATE fmt)
add_test(NAME control_flow_metadata_test COMMAND control_flow_metadata_test)
if(PSXRECOMP_ENABLE_CHD)
add_executable(disc_identity_test ../runtime/tests/test_disc_identity.cpp
../runtime/src/disc_identity.cpp ../runtime/src/disc_path.cpp
../runtime/src/iso_reader.cpp ../runtime/src/cue_sheet.cpp
../runtime/src/psx_sha256.c ../runtime/src/crc32.c)
target_include_directories(disc_identity_test PRIVATE ../runtime/include include)
target_compile_options(disc_identity_test PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/UNDEBUG>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-UNDEBUG>)
target_link_libraries(disc_identity_test PRIVATE chdr-static)
add_test(NAME disc_identity_test COMMAND disc_identity_test)
endif()
add_executable(host_path_test tests/host_path_test.cpp)
target_include_directories(host_path_test PRIVATE include)
add_test(NAME host_path_test COMMAND host_path_test)
add_test(NAME emitter_directive_line_test
COMMAND emitter_directive_line_test)
add_executable(guest_tty_test
../runtime/tests/test_guest_tty.c
../runtime/src/guest_tty.c)
target_include_directories(guest_tty_test PRIVATE ../runtime/include)
add_test(NAME guest_tty_test COMMAND guest_tty_test)
add_executable(debug_trace_ranges_test
../runtime/tests/test_debug_trace_ranges.c
../runtime/src/debug_trace_ranges.c
@@ -680,6 +701,7 @@ if(BUILD_TESTING)
frame_interpolation_display_rate
hybrid_dev_any_input
launcher_pad_mode_wiring
jalr_link_register_guards
launcher_vulkan_option
mod_controller_override
mod_load_acceleration
@@ -753,6 +775,21 @@ target_include_directories(reachable_discovery_test PRIVATE
)
target_link_libraries(reachable_discovery_test PRIVATE fmt)
# GTE deadline ownership includes pending CPU charges from both host sinks.
if(BUILD_TESTING AND CMAKE_C_COMPILER_ID STREQUAL "GNU")
foreach(_mode IN ITEMS O0 O2)
set(_target "tas_gte_deferred_${_mode}")
add_executable(${_target}
"${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_gte_deferred.c"
"${CMAKE_CURRENT_SOURCE_DIR}/../runtime/src/psx_cycles.c")
target_include_directories(${_target} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../runtime/include")
target_compile_definitions(${_target} PRIVATE PSX_ENABLE_BLOCK_CYCLES=1)
target_compile_options(${_target} PRIVATE -${_mode} -UNDEBUG -ffunction-sections -fdata-sections)
target_link_options(${_target} PRIVATE -Wl,--gc-sections)
add_test(NAME ${_target} COMMAND ${_target})
endforeach()
endif()
# ---- test-registration completeness guard -----------------------------------
if(BUILD_TESTING AND Python3_Interpreter_FOUND AND NOT MSVC)
add_test(NAME gpu_upload_history
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <filesystem>
#include <system_error>
namespace PSXRecompV4 {
// Some MinGW libstdc++ builds treat a readable UNC path as drive-relative:
// absolute("\\\\server\\share\\file") becomes "D:\\server\\share\\file".
// Windows already considers UNC and device namespace paths fully qualified.
// Preserve their spelling before asking std::filesystem to anchor a path.
inline bool host_path_is_absolute(const std::filesystem::path& path) {
#ifdef _WIN32
const auto& s = path.native();
const auto separator = [](wchar_t c) { return c == L'\\' || c == L'/'; };
if (s.size() > 2 && separator(s[0]) && separator(s[1]) && !separator(s[2]))
return true;
#endif
return path.is_absolute();
}
inline std::filesystem::path host_absolute(const std::filesystem::path& path,
std::error_code& ec) {
if (host_path_is_absolute(path)) {
ec.clear();
return path;
}
return std::filesystem::absolute(path, ec);
}
inline std::filesystem::path host_absolute(const std::filesystem::path& path) {
if (host_path_is_absolute(path)) return path;
return std::filesystem::absolute(path);
}
// A fully qualified config value must not be prefixed with its config root.
inline std::filesystem::path host_resolve(const std::filesystem::path& root,
const std::filesystem::path& path) {
return host_absolute(host_path_is_absolute(path) ? path : root / path);
}
} // namespace PSXRecompV4
+15 -14
View File
@@ -14,6 +14,7 @@
#include <vector>
#include "bios_rom_alias.h"
#include "host_path.h"
#include "fmt/format.h"
#include "ps1_exe_parser.h"
@@ -361,7 +362,7 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
}
if (runtime.contains("memcard_dir")) {
const auto rel = toml::find<std::string>(runtime, "memcard_dir");
rt.memcard_dir = fs::absolute(root / rel);
rt.memcard_dir = PSXRecompV4::host_resolve(root, rel);
rt.has_memcard_dir = true;
}
if (runtime.contains("disc_speed")) {
@@ -819,7 +820,7 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
}
fs::path find_project_root(const fs::path& config_path) {
fs::path cur = fs::absolute(config_path).parent_path();
fs::path cur = PSXRecompV4::host_absolute(config_path).parent_path();
const fs::path fallback = cur;
for (int i = 0; i < 8; ++i) {
for (const char* marker : { ".gitignore", ".git", "CMakeLists.txt" }) {
@@ -855,7 +856,7 @@ static std::string derive_out_stem(const std::string& rom_basename) {
}
BiosConfig load_bios_config(const fs::path& config_path_in) {
const fs::path config_path = fs::absolute(config_path_in);
const fs::path config_path = PSXRecompV4::host_absolute(config_path_in);
if (!fs::exists(config_path)) {
throw std::runtime_error(
fmt::format("config file not found: {}", config_path.string()));
@@ -920,7 +921,7 @@ BiosConfig load_bios_config(const fs::path& config_path_in) {
// Tolerate either BIOS filename convention (bare "SCPH1001.BIN" vs
// region-qualified "US-PSX-SCPH1001.BIN") — see bios_rom_alias.h. A no-op
// for game [program].exe fields, which never match a BIOS model token.
const fs::path rom_path = resolve_bios_rom(fs::absolute(root / rom_field));
const fs::path rom_path = resolve_bios_rom(PSXRecompV4::host_resolve(root, rom_field));
const uint32_t load_address =
parse_hex(toml::find<std::string>(prog, "load_address"), "program.load_address");
@@ -954,13 +955,13 @@ BiosConfig load_bios_config(const fs::path& config_path_in) {
fmt::format("{}: [recompiler] missing 'seeds' field", config_path.string()));
}
const std::string seeds_field = toml::find<std::string>(recomp, "seeds");
const fs::path seeds_path = fs::absolute(root / seeds_field);
const fs::path seeds_path = PSXRecompV4::host_resolve(root, seeds_field);
const std::string out_dir_field =
recomp.contains("out_dir")
? toml::find<std::string>(recomp, "out_dir")
: std::string{"generated"};
const fs::path out_dir = fs::absolute(root / out_dir_field);
const fs::path out_dir = PSXRecompV4::host_resolve(root, out_dir_field);
const bool strict = recomp.contains("strict")
? toml::find<bool>(recomp, "strict")
@@ -1128,7 +1129,7 @@ BiosConfig load_bios_config(const fs::path& config_path_in) {
}
GameConfig load_game_config(const fs::path& config_path_in) {
const fs::path config_path = fs::absolute(config_path_in);
const fs::path config_path = PSXRecompV4::host_absolute(config_path_in);
if (!fs::exists(config_path)) {
throw std::runtime_error(
fmt::format("game config not found: {}", config_path.string()));
@@ -1176,7 +1177,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
throw std::runtime_error(
fmt::format("{}: [game] missing 'exe' or 'rom' field", config_path.string()));
}
const fs::path exe_path = fs::absolute(root / exe_field);
const fs::path exe_path = PSXRecompV4::host_resolve(root, exe_field);
// Auto-detect EXE header values for any field not explicitly set in TOML.
// Parses the PS-X EXE header once and fills in load_address, entry_pc,
@@ -1244,10 +1245,10 @@ GameConfig load_game_config(const fs::path& config_path_in) {
std::vector<fs::path> discs;
if (game.contains("discs")) {
const auto& arr = toml::find<std::vector<std::string>>(game, "discs");
for (const auto& d : arr) discs.push_back(fs::absolute(root / d));
for (const auto& d : arr) discs.push_back(PSXRecompV4::host_resolve(root, d));
} else if (game.contains("disc")) {
const auto& d = toml::find<std::string>(game, "disc");
discs.push_back(fs::absolute(root / d));
discs.push_back(PSXRecompV4::host_resolve(root, d));
}
/* Per-disc serials, parallel to `discs`. Absent => no per-disc gate. */
std::vector<std::string> disc_serials;
@@ -1346,12 +1347,12 @@ GameConfig load_game_config(const fs::path& config_path_in) {
fmt::format("{}: [recompiler] missing 'seeds' field", config_path.string()));
}
const fs::path seeds_path =
fs::absolute(root / toml::find<std::string>(recomp, "seeds"));
PSXRecompV4::host_resolve(root, toml::find<std::string>(recomp, "seeds"));
fs::path bios_thunks_path;
if (recomp.contains("bios_thunks")) {
bios_thunks_path =
fs::absolute(root / toml::find<std::string>(recomp, "bios_thunks"));
PSXRecompV4::host_resolve(root, toml::find<std::string>(recomp, "bios_thunks"));
}
// [recompiler] bios_config — the BIOS profile this game is built
@@ -1360,14 +1361,14 @@ GameConfig load_game_config(const fs::path& config_path_in) {
fs::path bios_config_path;
if (recomp.contains("bios_config")) {
bios_config_path =
fs::absolute(root / toml::find<std::string>(recomp, "bios_config"));
PSXRecompV4::host_resolve(root, toml::find<std::string>(recomp, "bios_config"));
}
const std::string out_dir_field =
recomp.contains("out_dir")
? toml::find<std::string>(recomp, "out_dir")
: std::string{"generated"};
const fs::path out_dir = fs::absolute(root / out_dir_field);
const fs::path out_dir = PSXRecompV4::host_resolve(root, out_dir_field);
const bool strict = recomp.contains("strict")
? toml::find<bool>(recomp, "strict")
+3 -2
View File
@@ -41,6 +41,7 @@
#include "bios_slice_walker.h"
#include "bios_rom_alias.h"
#include "host_path.h"
#include "config_loader.h"
#include "full_function_emitter.h"
#include "function_discovery.h"
@@ -969,9 +970,9 @@ int main(int argc, char** argv) {
} else if (a.rfind("--config=", 0) == 0) {
config_path = a.substr(std::string("--config=").size());
} else if (a == "--rom" && i + 1 < argc) {
config_rom_override = fs::absolute(argv[++i]);
config_rom_override = PSXRecompV4::host_absolute(argv[++i]);
} else if (a == "--out-dir" && i + 1 < argc) {
config_out_override = fs::absolute(argv[++i]);
config_out_override = PSXRecompV4::host_absolute(argv[++i]);
}
}
if (config_path) {
+53
View File
@@ -0,0 +1,53 @@
#include "host_path.h"
#include <cstdio>
#include <cstdlib>
#include <fstream>
namespace fs = std::filesystem;
using namespace PSXRecompV4;
static int checks = 0;
static void check(bool value, const char* message) {
++checks;
if (!value) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); }
}
int main(int argc, char** argv) {
#ifdef _WIN32
for (const char* spelling : {
R"(\\server\share\BIOS files\SCPH5500.BIN)",
"//server/share/Game (Japan)/game.cue",
R"(\\?\UNC\server\share\game.cue)",
R"(\\?\C:\Game files\game.cue)",
R"(Z:\Game files\game.cue)"}) {
const fs::path path(spelling);
check(host_path_is_absolute(path), "fully qualified Windows path");
std::error_code ec = std::make_error_code(std::errc::invalid_argument);
check(host_absolute(path, ec).native() == path.native() && !ec,
"nonthrowing absolute preserves exact Windows path and clears error");
check(host_absolute(path).native() == path.native(),
"throwing absolute preserves exact Windows path");
check(host_resolve(R"(D:\unrelated\config)", path).native() == path.native(),
"config root never replaces an absolute Windows path");
}
check(!host_path_is_absolute(R"(\current-drive\file)"), "drive-rooted path needs current drive");
check(!host_path_is_absolute(R"(C:drive-relative.bin)"), "drive-relative path needs drive directory");
#else
check(host_absolute("/tmp/PSX files/game.cue") == fs::path("/tmp/PSX files/game.cue"),
"POSIX absolute path unchanged");
check(!host_path_is_absolute(R"(\\server\share\file)"), "backslashes remain ordinary on POSIX");
#endif
const fs::path relative("relative directory/game.cue");
check(!host_path_is_absolute(relative), "ordinary relative path");
check(host_absolute(relative) == fs::absolute(relative), "relative path anchors to cwd");
const auto root = fs::current_path() / "config directory";
check(host_resolve(root, relative) == root / relative, "relative config value anchors to root");
// Optional private integration input: inspect only, never change the asset.
if (argc == 2) {
const fs::path path(argv[1]);
check(bool(std::ifstream(path, std::ios::binary)), "original private path readable");
check(bool(std::ifstream(host_absolute(path), std::ios::binary)), "resolved private path readable");
check(host_absolute(path).native() == path.native(), "private path remains exact");
}
std::printf("host_path: %d checks passed\n", checks);
}
+4 -3
View File
@@ -368,7 +368,7 @@ if(BUILD_TESTING)
src/disc_path.cpp
src/cue_sheet.cpp
src/iso_reader.cpp)
target_include_directories(disc_path_resolve_test PRIVATE include)
target_include_directories(disc_path_resolve_test PRIVATE include ${PSXRECOMP_ROOT}/recompiler/include)
target_link_libraries(disc_path_resolve_test PRIVATE chdr-static)
add_test(NAME disc_path_resolve_test COMMAND disc_path_resolve_test)
@@ -384,7 +384,7 @@ if(BUILD_TESTING)
tests/test_iso_reader_cdda.cpp
src/iso_reader.cpp
src/cue_sheet.cpp)
target_include_directories(iso_reader_cdda_test PRIVATE include)
target_include_directories(iso_reader_cdda_test PRIVATE include ${PSXRECOMP_ROOT}/recompiler/include)
target_link_libraries(iso_reader_cdda_test PRIVATE chdr-static)
add_test(NAME iso_reader_cdda_test COMMAND iso_reader_cdda_test)
@@ -392,7 +392,7 @@ if(BUILD_TESTING)
tests/test_iso_reader_sbi.cpp
src/iso_reader.cpp
src/cue_sheet.cpp)
target_include_directories(iso_reader_sbi_test PRIVATE include)
target_include_directories(iso_reader_sbi_test PRIVATE include ${PSXRECOMP_ROOT}/recompiler/include)
target_link_libraries(iso_reader_sbi_test PRIVATE chdr-static)
add_test(NAME iso_reader_sbi_test COMMAND iso_reader_sbi_test)
@@ -425,6 +425,7 @@ if(BUILD_TESTING)
src/psx_sha256.c)
target_include_directories(mod_runtime_test PRIVATE
include
../recompiler/include
../recompiler/lib/toml11)
target_link_libraries(mod_runtime_test PRIVATE chdr-static)
# The real disc hasher uses a 1 MiB scratch buffer. Match the runtime's
+2 -1
View File
@@ -2,7 +2,8 @@
//
// Single source of truth for "is this the right disc?". Reads a .cue, raw
// .bin/.iso/.img/.car, or CHD, checks for the ISO9660 PVD, extracts the volume id and the
// PlayStation boot serial (from SYSTEM.CNF), derives the region from the
// PlayStation boot serial (from the actual SYSTEM.CNF directory entry, with
// an early-image scan as fallback), derives the region from the
// serial prefix, and optionally compares against an expected serial / CRC32.
//
// Also mounts the resolved path via ISOReader to capture TOC geometry
+3 -2
View File
@@ -37,6 +37,7 @@ struct GTEState {
uint16_t SZ[4]; // Screen Z FIFO (unsigned 16-bit)
uint32_t RGBC; // Current input color/code register
uint32_t RGB[3]; // RGB color FIFO (RGB0..RGB2)
uint32_t RES1; // Undocumented read/write data register 23
// === Control Registers (matrices, vectors, config) ===
int16_t RT[3][3]; // Rotation matrix (s3.12)
@@ -98,8 +99,8 @@ struct GTEState {
void check_mac_overflow(int64_t value, int mac_num) {
// mac_num: 1,2,3
if (value > 0x7FFFFFFFFLL) FLAG |= (1u << (31 - mac_num)); // MAC1=30, MAC2=29, MAC3=28
if (value < -0x800000000LL) FLAG |= (1u << (28 - mac_num)); // MAC1=27, MAC2=26, MAC3=25
if (value > 0x7FFFFFFFFFFLL) FLAG |= (1u << (31 - mac_num)); // MAC1=30, MAC2=29, MAC3=28
if (value < -0x80000000000LL) FLAG |= (1u << (28 - mac_num)); // MAC1=27, MAC2=26, MAC3=25
}
void check_mac0_overflow(int64_t value) {
+23
View File
@@ -0,0 +1,23 @@
#ifndef PSXRECOMP_GUEST_TTY_H
#define PSXRECOMP_GUEST_TTY_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Bounded observability sink for bytes emitted through the PS1 guest console.
* Writers never block. A snapshot returns the newest complete byte window and
* the monotonically increasing total number of bytes observed. */
void psx_guest_tty_putchar(uint8_t value);
size_t psx_guest_tty_snapshot(uint8_t *out, size_t capacity,
uint64_t *total_bytes);
void psx_guest_tty_reset(void);
#ifdef __cplusplus
}
#endif
#endif
+1
View File
@@ -331,6 +331,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/psx_stick.c
${PSXRECOMP_ROOT}/runtime/src/memory.c
${PSXRECOMP_ROOT}/runtime/src/kernel_patch_ranges.c
${PSXRECOMP_ROOT}/runtime/src/guest_tty.c
${PSXRECOMP_ROOT}/runtime/src/gpu.c
${PSXRECOMP_ROOT}/runtime/src/ws_ui_group.c
${PSXRECOMP_ROOT}/runtime/src/ws_aspect_cone_math.c
+2 -1
View File
@@ -1,6 +1,7 @@
// cue_sheet.cpp — see cue_sheet.h.
#include "cue_sheet.h"
#include "host_path.h"
#include <cctype>
#include <cstdio>
@@ -94,7 +95,7 @@ fs::path resolve_cue_relative_file(const fs::path& dir, const std::string& name)
named = fs::path(slashed);
}
const fs::path literal = named.is_absolute() ? named : (dir / named);
const fs::path literal = PSXRecompV4::host_path_is_absolute(named) ? named : (dir / named);
std::error_code ec;
if (fs::exists(literal, ec)) return literal;
+36
View File
@@ -46,6 +46,7 @@
#include "crash_trace.h"
#include "gpu_gl_renderer.h"
#include "lockstep.h"
#include "guest_tty.h"
#include <stdio.h>
#include <stdlib.h>
@@ -3570,6 +3571,40 @@ static void handle_bioscall_dump(int id, const char *json)
debug_server_send_line(out); free(out);
}
/* guest_tty_dump — bounded, structured capture of bytes emitted through the
* guest console. Hex avoids JSON escaping ambiguity and preserves arbitrary
* byte values. The command is observational and never consumes the ring. */
static void handle_guest_tty_dump(int id, const char *json)
{
int requested = json_get_int(json, "tail", 4096);
if (requested < 0) requested = 0;
if (requested > 65536) requested = 65536;
size_t cap = (size_t)requested;
uint8_t *bytes = cap ? (uint8_t *)malloc(cap) : NULL;
if (cap && !bytes) { send_err(id, "oom"); return; }
uint64_t total = 0;
size_t count = psx_guest_tty_snapshot(bytes, cap, &total);
size_t out_cap = 160u + count * 2u;
char *out = (char *)malloc(out_cap);
if (!out) { free(bytes); send_err(id, "oom"); return; }
size_t pos = (size_t)snprintf(
out, out_cap,
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"tail\":%llu,\"hex\":\"",
id, (unsigned long long)total, (unsigned long long)count);
static const char digits[] = "0123456789abcdef";
for (size_t i = 0; i < count; ++i) {
out[pos++] = digits[bytes[i] >> 4];
out[pos++] = digits[bytes[i] & 0x0Fu];
}
out[pos++] = '"'; out[pos++] = '}'; out[pos++] = '\n'; out[pos] = '\0';
debug_server_send_line(out);
free(out);
free(bytes);
}
/* bios_info — which recompiled BIOS this build links, and whether the
* loaded ROM matches it. Everything static comes from psx_bios_image (the
* generated dispatch's self-description, couriered from the BIOS profile);
@@ -13707,6 +13742,7 @@ static const CmdEntry s_commands[] = {
{ "fntrace_dump", handle_fntrace_dump },
{ "unknown_dispatch_log", handle_unknown_dispatch_log },
{ "bioscall_dump", handle_bioscall_dump },
{ "guest_tty_dump", handle_guest_tty_dump },
{ "bios_info", handle_bios_info },
{ "hle_dump", handle_hle_dump },
{ "card_trace_dump", handle_card_trace_dump },
+17 -1
View File
@@ -1648,7 +1648,16 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
uint32_t target = cpu->gpr[rs];
if (target & 3) return interp_exception(cpu, 4, target, pc); /* LoadAddressError */
uint32_t return_pc = pc + 8;
cpu->gpr[rd ? rd : 31] = return_pc;
/* JALR writes the encoded rd exactly. The one-operand assembler
* form encodes rd=$ra; rd=0 is a real discard, not an implicit
* $ra. More importantly, only rd=$ra gives the call-unit code a
* return contract it can validate. Games also use other link
* registers for data-bearing transfer stubs (for example
* `jalr $a1,$t0`, where the target consumes $a1 and later returns
* through the pre-existing $ra). Treating pc+8 as that transfer's
* mandatory return address fabricates a nested continuation and
* leaks psx_dispatch_call frames. */
if (rd != 0) cpu->gpr[rd] = return_pc;
cpu->gpr[0] = 0;
exec_delay_slot(cpu, pc + 4);
cosim_exec_one_transfer_hook(pc + 4);
@@ -1660,6 +1669,13 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
uint32_t _cr = callret_begin(cpu, pc, target); /* call-resolution ring */
#define CRET(code, rv) do { callret_end(_cr, cpu, (code)); return (rv); } while (0)
if (g_precise_mode || g_ls_replay_active) { cpu->pc = target; CRET(CRES_PLAIN, 1); } /* slice / lockstep-replay: plain transfer, never execute the callee */
if (rd != 31) {
/* No architectural $ra contract: preserve the transfer as a
* pc-chain and let the callee's eventual JR choose the real
* continuation. This is both faithful and host-stack-flat. */
cpu->pc = target;
CRET(CRES_PCCHAIN, 1);
}
#ifdef PSX_HAS_GAME_DISPATCH
cpu->pc = 0;
if (interp_enter_compiled(cpu, target)) {
+10 -2
View File
@@ -140,6 +140,14 @@ void fill_toc_from_mount(DiscIdentity& v, const DiscPathResolution& resolved) {
return;
}
v.toc_opened = true;
// SYSTEM.CNF can lie beyond the bounded early-image scan. Read the actual
// directory entry first; unrelated executable strings must not select a region.
uint8_t pvd[2048];
if (reader.ReadSector(16, pvd) && is_iso_pvd(pvd)) {
std::vector<uint8_t> cnf(4096);
cnf.resize(reader.ReadFile("SYSTEM.CNF", cnf.data(), cnf.size()));
v.detected_serial = scan_boot_serial(cnf);
}
v.track_count = reader.TrackCount();
if (v.track_count < 1) v.track_count = 1;
v.leadout_lba = reader.GetSectorCount();
@@ -276,7 +284,7 @@ DiscIdentity identify_disc(const fs::path& path,
}
}
if (scan_ok) {
v.detected_serial = scan_boot_serial(scan);
if (v.detected_serial.empty()) v.detected_serial = scan_boot_serial(scan);
if (v.expected_serial_given) {
const std::string serial_id = uppercase_ascii(expected_serial);
std::string exe_id = serial_id;
@@ -354,7 +362,7 @@ DiscIdentity identify_disc(const fs::path& path,
const size_t scan_len = (size_t)std::min<uint64_t>(size, 16ull * 1024ull * 1024ull);
std::vector<uint8_t> scan(scan_len);
if (read_at(f, 0, scan.data(), scan.size())) {
v.detected_serial = scan_boot_serial(scan);
if (v.detected_serial.empty()) v.detected_serial = scan_boot_serial(scan);
if (v.expected_serial_given) {
// Match either the game-id form (SCUS-94236) or the EXE form (SCUS_942.36).
+2 -1
View File
@@ -1,6 +1,7 @@
// disc_path.cpp — see disc_path.h.
#include "disc_path.h"
#include "host_path.h"
#include "cue_sheet.h"
@@ -39,7 +40,7 @@ bool same_file_path(const fs::path& a, const fs::path& b) {
fs::path absolute_or_self(const fs::path& p) {
std::error_code ec;
fs::path abs = fs::absolute(p, ec);
fs::path abs = PSXRecompV4::host_absolute(p, ec);
return ec ? p : abs;
}
+206 -194
View File
@@ -78,100 +78,101 @@ static int32_t gte_divide(uint16_t H, uint16_t SZ3, uint32_t& FLAG) {
static inline bool gte_instr_lm(uint32_t instr) { return (instr & (1u << 10)) != 0; }
static inline int gte_instr_sf(uint32_t instr) { return (instr & (1u << 19)) ? 12 : 0; }
static int64_t gte_mac44_stage(GTEState* gte, int64_t value, int mac_num) {
constexpr uint64_t mask = (1ull << 44) - 1ull;
constexpr uint64_t sign = 1ull << 43;
gte->check_mac_overflow(value, mac_num);
uint64_t wrapped = static_cast<uint64_t>(value) & mask;
if (wrapped & sign) wrapped |= ~mask;
return static_cast<int64_t>(wrapped);
}
static void gte_store_mac_ir(GTEState* gte, int mac_num, int64_t value,
int shift, bool lm) {
value = gte_mac44_stage(gte, value, mac_num) >> shift;
int32_t mac = static_cast<int32_t>(value);
if (mac_num == 1) { gte->MAC1 = mac; gte->IR1 = gte->saturate_ir(mac, 1, lm); }
if (mac_num == 2) { gte->MAC2 = mac; gte->IR2 = gte->saturate_ir(mac, 2, lm); }
if (mac_num == 3) { gte->MAC3 = mac; gte->IR3 = gte->saturate_ir(mac, 3, lm); }
}
// Transform a vertex by Light matrix → IR1/IR2/IR3 (step 1 of lighting)
static void light_transform(GTEState* gte, int16_t* V, uint32_t instr) {
const bool lm = gte_instr_lm(instr);
int64_t mac1 = ((int64_t)gte->L[0][0] * V[0] +
(int64_t)gte->L[0][1] * V[1] +
(int64_t)gte->L[0][2] * V[2]) >> 12;
int64_t mac2 = ((int64_t)gte->L[1][0] * V[0] +
(int64_t)gte->L[1][1] * V[1] +
(int64_t)gte->L[1][2] * V[2]) >> 12;
int64_t mac3 = ((int64_t)gte->L[2][0] * V[0] +
(int64_t)gte->L[2][1] * V[1] +
(int64_t)gte->L[2][2] * V[2]) >> 12;
gte->MAC1 = static_cast<int32_t>(mac1);
gte->MAC2 = static_cast<int32_t>(mac2);
gte->MAC3 = static_cast<int32_t>(mac3);
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm);
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm);
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm);
const int shift = gte_instr_sf(instr);
for (int row = 0; row < 3; ++row) {
int64_t acc = gte_mac44_stage(
gte, (int64_t)gte->L[row][0] * V[0] +
(int64_t)gte->L[row][1] * V[1], row + 1);
gte_store_mac_ir(gte, row + 1,
acc + (int64_t)gte->L[row][2] * V[2], shift, lm);
}
}
// Apply light color matrix + background color to IR → IR (step 2 of lighting)
static void light_color(GTEState* gte, uint32_t instr) {
const bool lm = gte_instr_lm(instr);
int64_t mac1 = (int64_t)gte->BK[0] * 4096 +
(int64_t)gte->LC[0][0] * gte->IR1 +
(int64_t)gte->LC[0][1] * gte->IR2 +
(int64_t)gte->LC[0][2] * gte->IR3;
int64_t mac2 = (int64_t)gte->BK[1] * 4096 +
(int64_t)gte->LC[1][0] * gte->IR1 +
(int64_t)gte->LC[1][1] * gte->IR2 +
(int64_t)gte->LC[1][2] * gte->IR3;
int64_t mac3 = (int64_t)gte->BK[2] * 4096 +
(int64_t)gte->LC[2][0] * gte->IR1 +
(int64_t)gte->LC[2][1] * gte->IR2 +
(int64_t)gte->LC[2][2] * gte->IR3;
gte->MAC1 = static_cast<int32_t>(mac1 >> 12);
gte->MAC2 = static_cast<int32_t>(mac2 >> 12);
gte->MAC3 = static_cast<int32_t>(mac3 >> 12);
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm);
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm);
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm);
const int shift = gte_instr_sf(instr);
const int16_t input[3] = {gte->IR1, gte->IR2, gte->IR3};
for (int row = 0; row < 3; ++row) {
int64_t acc = gte_mac44_stage(
gte, (int64_t)gte->BK[row] * 4096 +
(int64_t)gte->LC[row][0] * input[0], row + 1);
acc = gte_mac44_stage(
gte, acc + (int64_t)gte->LC[row][1] * input[1], row + 1);
gte_store_mac_ir(gte, row + 1,
acc + (int64_t)gte->LC[row][2] * input[2], shift, lm);
}
}
// Multiply IR by RGBC color and push to RGB FIFO (step 3 of color output)
static void color_output(GTEState* gte, uint32_t instr) {
const bool lm = gte_instr_lm(instr);
uint8_t r0 = (gte->RGBC >> 0) & 0xFF;
uint8_t g0 = (gte->RGBC >> 8) & 0xFF;
uint8_t b0 = (gte->RGBC >> 16) & 0xFF;
int64_t mac1 = (int64_t)r0 * gte->IR1 * 16;
int64_t mac2 = (int64_t)g0 * gte->IR2 * 16;
int64_t mac3 = (int64_t)b0 * gte->IR3 * 16;
gte->MAC1 = static_cast<int32_t>(mac1 >> 12);
gte->MAC2 = static_cast<int32_t>(mac2 >> 12);
gte->MAC3 = static_cast<int32_t>(mac3 >> 12);
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm);
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm);
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm);
static void push_rgb_from_mac(GTEState* gte) {
gte->push_rgb(
gte->saturate_color(gte->MAC1 >> 4, 0),
gte->saturate_color(gte->MAC2 >> 4, 1),
gte->saturate_color(gte->MAC3 >> 4, 2));
}
// Depth cue / interpolate current IR toward the far color using IR0
// (common tail of DPCS/DPCT/DPCL/INTPL/NCDS/NCDT/CDP). Hardware:
// base = IR << 12
// step = lim(((FC << 12) - base) >> (sf*12)) ; ±0x8000 clamp, lm FORCED off
// MAC = (base + IR0 * step) >> (sf*12)
// IR = lim(MAC, lm) ; lm from the instruction
static void depth_cue_from_ir(GTEState* gte, uint32_t instr) {
// Multiply IR by RGBC color (step 3 of NCCS/CC color output).
static void color_multiply(GTEState* gte, uint32_t instr) {
const bool lm = gte_instr_lm(instr);
const int shift = gte_instr_sf(instr);
uint8_t r0 = (gte->RGBC >> 0) & 0xFF;
uint8_t g0 = (gte->RGBC >> 8) & 0xFF;
uint8_t b0 = (gte->RGBC >> 16) & 0xFF;
const int16_t input[3] = {gte->IR1, gte->IR2, gte->IR3};
gte_store_mac_ir(gte, 1, (int64_t)r0 * input[0] * 16, shift, lm);
gte_store_mac_ir(gte, 2, (int64_t)g0 * input[1] * 16, shift, lm);
gte_store_mac_ir(gte, 3, (int64_t)b0 * input[2] * 16, shift, lm);
}
static void color_output(GTEState* gte, uint32_t instr) {
color_multiply(gte, instr);
push_rgb_from_mac(gte);
}
// Interpolate an unshifted MAC input toward far color using IR0. The first
// stage always uses lm=0; the command's lm applies only to the final result.
static void depth_cue_from_mac(GTEState* gte, int64_t base1, int64_t base2,
int64_t base3, uint32_t instr) {
const int shift = gte_instr_sf(instr);
const bool lm = gte_instr_lm(instr);
int64_t base1 = (int64_t)gte->IR1 * 4096;
int64_t base2 = (int64_t)gte->IR2 * 4096;
int64_t base3 = (int64_t)gte->IR3 * 4096;
int16_t step1 = gte->saturate_ir(
(int32_t)((((int64_t)gte->FC[0] * 4096 - base1) >> shift)), 1, false);
int16_t step2 = gte->saturate_ir(
(int32_t)((((int64_t)gte->FC[1] * 4096 - base2) >> shift)), 2, false);
int16_t step3 = gte->saturate_ir(
(int32_t)((((int64_t)gte->FC[2] * 4096 - base3) >> shift)), 3, false);
int64_t mac1 = (base1 + (int64_t)gte->IR0 * step1) >> shift;
int64_t mac2 = (base2 + (int64_t)gte->IR0 * step2) >> shift;
int64_t mac3 = (base3 + (int64_t)gte->IR0 * step3) >> shift;
gte->check_mac_overflow(mac1, 1);
gte->check_mac_overflow(mac2, 2);
gte->check_mac_overflow(mac3, 3);
gte->MAC1 = static_cast<int32_t>(mac1);
gte->MAC2 = static_cast<int32_t>(mac2);
gte->MAC3 = static_cast<int32_t>(mac3);
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm);
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm);
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm);
gte_store_mac_ir(gte, 1, (int64_t)gte->FC[0] * 4096 - base1, shift, false);
gte_store_mac_ir(gte, 2, (int64_t)gte->FC[1] * 4096 - base2, shift, false);
gte_store_mac_ir(gte, 3, (int64_t)gte->FC[2] * 4096 - base3, shift, false);
const int16_t step1 = gte->IR1;
const int16_t step2 = gte->IR2;
const int16_t step3 = gte->IR3;
gte_store_mac_ir(gte, 1, base1 + (int64_t)gte->IR0 * step1, shift, lm);
gte_store_mac_ir(gte, 2, base2 + (int64_t)gte->IR0 * step2, shift, lm);
gte_store_mac_ir(gte, 3, base3 + (int64_t)gte->IR0 * step3, shift, lm);
}
static void depth_cue_from_ir(GTEState* gte, uint32_t instr) {
depth_cue_from_mac(gte,
(int64_t)gte->IR1 * 4096,
(int64_t)gte->IR2 * 4096,
(int64_t)gte->IR3 * 4096,
instr);
}
// ---------------------------------------------------------------------------
@@ -805,23 +806,22 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
const bool lm = gte_instr_lm(instr);
// Step 1: Matrix multiplication + translation
int64_t mac1 = (int64_t)gte->TR[0] * 4096 +
(int64_t)gte->RT[0][0] * V[0] +
(int64_t)gte->RT[0][1] * V[1] +
(int64_t)gte->RT[0][2] * V[2];
int64_t mac2 = (int64_t)gte->TR[1] * 4096 +
(int64_t)gte->RT[1][0] * V[0] +
(int64_t)gte->RT[1][1] * V[1] +
(int64_t)gte->RT[1][2] * V[2];
int64_t mac3 = (int64_t)gte->TR[2] * 4096 +
(int64_t)gte->RT[2][0] * V[0] +
(int64_t)gte->RT[2][1] * V[1] +
(int64_t)gte->RT[2][2] * V[2];
auto dot = [&](int row, int mac_num) {
int64_t acc = (int64_t)gte->TR[row] * 4096 +
(int64_t)gte->RT[row][0] * V[0];
acc = gte_mac44_stage(gte, acc, mac_num);
acc += (int64_t)gte->RT[row][1] * V[1];
acc = gte_mac44_stage(gte, acc, mac_num);
return acc + (int64_t)gte->RT[row][2] * V[2];
};
int64_t mac1 = dot(0, 1);
int64_t mac2 = dot(1, 2);
int64_t mac3 = dot(2, 3);
// Overflow flags always check the >>12 view (hardware).
gte->check_mac_overflow(mac1 >> 12, 1);
gte->check_mac_overflow(mac2 >> 12, 2);
gte->check_mac_overflow(mac3 >> 12, 3);
// MAC1..3 overflow flags observe the unshifted 44-bit accumulator.
gte->check_mac_overflow(mac1, 1);
gte->check_mac_overflow(mac2, 2);
gte->check_mac_overflow(mac3, 3);
gte->MAC1 = static_cast<int32_t>(mac1 >> shift);
gte->MAC2 = static_cast<int32_t>(mac2 >> shift);
gte->MAC3 = static_cast<int32_t>(mac3 >> shift);
@@ -888,6 +888,10 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
dome_probe_note(gte->SZ[3]); /* locate the dome draw fn (far-vertex tally) */
int64_t sx16 = gte->OFX + xterm;
int64_t sy16 = gte->OFY + (int64_t)gte->IR2 * h_div_sz;
/* RTPS/RTPT reuse MAC0's overflow flags for each projected X/Y
* accumulator before MAC0 is replaced by the depth-cue result. */
gte->check_mac0_overflow(sx16);
gte->check_mac0_overflow(sy16);
int64_t sx = sx16 >> 16;
int64_t sy = sy16 >> 16;
gte->push_sxy(sx, sy);
@@ -1055,8 +1059,15 @@ void gte_ncct(GTEState* gte, uint32_t instr) {
void gte_ncds_internal(GTEState* gte, int16_t* V, uint32_t instr) {
light_transform(gte, V, instr);
light_color(gte, instr);
depth_cue_from_ir(gte, instr);
color_output(gte, instr);
const uint8_t r = (gte->RGBC >> 0) & 0xFF;
const uint8_t g = (gte->RGBC >> 8) & 0xFF;
const uint8_t b = (gte->RGBC >> 16) & 0xFF;
depth_cue_from_mac(gte,
(int64_t)r * gte->IR1 * 16,
(int64_t)g * gte->IR2 * 16,
(int64_t)b * gte->IR3 * 16,
instr);
push_rgb_from_mac(gte);
}
void gte_ncds(GTEState* gte, uint32_t instr) {
@@ -1114,17 +1125,9 @@ void gte_dpcs(GTEState* gte, uint32_t instr) {
uint8_t r = (gte->RGBC >> 0) & 0xFF;
uint8_t g = (gte->RGBC >> 8) & 0xFF;
uint8_t b = (gte->RGBC >> 16) & 0xFF;
// MAC = RGBC << 4
gte->IR1 = gte->saturate_ir(r << 4, 1, false);
gte->IR2 = gte->saturate_ir(g << 4, 2, false);
gte->IR3 = gte->saturate_ir(b << 4, 3, false);
// Interpolate toward far color using IR0
depth_cue_from_ir(gte, instr);
// Output
gte->push_rgb(
gte->saturate_color(gte->MAC1 >> 4, 0),
gte->saturate_color(gte->MAC2 >> 4, 1),
gte->saturate_color(gte->MAC3 >> 4, 2));
depth_cue_from_mac(gte, (int64_t)r << 16, (int64_t)g << 16,
(int64_t)b << 16, instr);
push_rgb_from_mac(gte);
gte->set_error_flag();
}
@@ -1137,14 +1140,9 @@ void gte_dpct(GTEState* gte, uint32_t instr) {
uint8_t r = (gte->RGB[0] >> 0) & 0xFF;
uint8_t g = (gte->RGB[0] >> 8) & 0xFF;
uint8_t b = (gte->RGB[0] >> 16) & 0xFF;
gte->IR1 = gte->saturate_ir(r << 4, 1, false);
gte->IR2 = gte->saturate_ir(g << 4, 2, false);
gte->IR3 = gte->saturate_ir(b << 4, 3, false);
depth_cue_from_ir(gte, instr);
gte->push_rgb(
gte->saturate_color(gte->MAC1 >> 4, 0),
gte->saturate_color(gte->MAC2 >> 4, 1),
gte->saturate_color(gte->MAC3 >> 4, 2));
depth_cue_from_mac(gte, (int64_t)r << 16, (int64_t)g << 16,
(int64_t)b << 16, instr);
push_rgb_from_mac(gte);
}
gte->set_error_flag();
}
@@ -1158,18 +1156,12 @@ void gte_dpcl(GTEState* gte, uint32_t instr) {
uint8_t r = (gte->RGBC >> 0) & 0xFF;
uint8_t g = (gte->RGBC >> 8) & 0xFF;
uint8_t b = (gte->RGBC >> 16) & 0xFF;
gte->MAC1 = (r * gte->IR1) >> 8;
gte->MAC2 = (g * gte->IR2) >> 8;
gte->MAC3 = (b * gte->IR3) >> 8;
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, gte_instr_lm(instr));
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, gte_instr_lm(instr));
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, gte_instr_lm(instr));
// Depth cue toward far color
depth_cue_from_ir(gte, instr);
gte->push_rgb(
gte->saturate_color(gte->MAC1 >> 4, 0),
gte->saturate_color(gte->MAC2 >> 4, 1),
gte->saturate_color(gte->MAC3 >> 4, 2));
depth_cue_from_mac(gte,
(int64_t)r * gte->IR1 * 16,
(int64_t)g * gte->IR2 * 16,
(int64_t)b * gte->IR3 * 16,
instr);
push_rgb_from_mac(gte);
gte->set_error_flag();
}
@@ -1179,10 +1171,7 @@ void gte_dpcl(GTEState* gte, uint32_t instr) {
void gte_intpl(GTEState* gte, uint32_t instr) {
gte->FLAG = 0;
depth_cue_from_ir(gte, instr);
gte->push_rgb(
gte->saturate_color(gte->MAC1 >> 4, 0),
gte->saturate_color(gte->MAC2 >> 4, 1),
gte->saturate_color(gte->MAC3 >> 4, 2));
push_rgb_from_mac(gte);
gte->set_error_flag();
}
@@ -1193,8 +1182,15 @@ void gte_cdp(GTEState* gte, uint32_t instr) {
gte->FLAG = 0;
// IR1/IR2/IR3 already set (from previous NCS or similar)
light_color(gte, instr);
depth_cue_from_ir(gte, instr);
color_output(gte, instr);
const uint8_t r = (gte->RGBC >> 0) & 0xFF;
const uint8_t g = (gte->RGBC >> 8) & 0xFF;
const uint8_t b = (gte->RGBC >> 16) & 0xFF;
depth_cue_from_mac(gte,
(int64_t)r * gte->IR1 * 16,
(int64_t)g * gte->IR2 * 16,
(int64_t)b * gte->IR3 * 16,
instr);
push_rgb_from_mac(gte);
gte->set_error_flag();
}
@@ -1217,7 +1213,8 @@ void gte_mvmva(GTEState* gte, uint32_t instr) {
uint32_t mx = (instr >> 17) & 3; // Matrix: 0=RT, 1=Light, 2=LightColor, 3=reserved
uint32_t vv = (instr >> 15) & 3; // Vector: 0=V0, 1=V1, 2=V2, 3=IR
uint32_t tv = (instr >> 13) & 3; // Translation: 0=TR, 1=BK, 2=FC/bugged, 3=none
int sf = (instr >> 19) & 1; // Shift: 0=no shift, 1=shift right 12
const int shift = gte_instr_sf(instr);
const bool lm = gte_instr_lm(instr);
// Select matrix
int16_t M[3][3];
@@ -1225,7 +1222,18 @@ void gte_mvmva(GTEState* gte, uint32_t instr) {
case 0: std::memcpy(M, gte->RT, sizeof(M)); break;
case 1: std::memcpy(M, gte->L, sizeof(M)); break;
case 2: std::memcpy(M, gte->LC, sizeof(M)); break;
default: std::memset(M, 0, sizeof(M)); break; // Garbage on real HW
default: {
/* Undocumented matrix selector wiring, verified by hardware
* register vectors (also used by Beetle/DuckStation). */
const int16_t color = static_cast<int16_t>(
static_cast<uint16_t>(gte->RGBC & 0xFFu) << 4);
M[0][0] = static_cast<int16_t>(-color);
M[0][1] = color;
M[0][2] = gte->IR0;
M[1][0] = M[1][1] = M[1][2] = gte->RT[0][2];
M[2][0] = M[2][1] = M[2][2] = gte->RT[1][1];
break;
}
}
// Select vector
@@ -1246,28 +1254,39 @@ void gte_mvmva(GTEState* gte, uint32_t instr) {
case 3: T[0] = 0; T[1] = 0; T[2] = 0; break;
}
// Multiply
int64_t mac1 = T[0] + (int64_t)M[0][0] * V[0] + (int64_t)M[0][1] * V[1] + (int64_t)M[0][2] * V[2];
int64_t mac2 = T[1] + (int64_t)M[1][0] * V[0] + (int64_t)M[1][1] * V[1] + (int64_t)M[1][2] * V[2];
int64_t mac3 = T[2] + (int64_t)M[2][0] * V[0] + (int64_t)M[2][1] * V[1] + (int64_t)M[2][2] * V[2];
auto set_mac_ir = [&](int row, int64_t value) {
const int mac_num = row + 1;
value = gte_mac44_stage(gte, value, mac_num) >> shift;
int32_t mac = static_cast<int32_t>(value);
if (row == 0) { gte->MAC1 = mac; gte->IR1 = gte->saturate_ir(mac, 1, lm); }
if (row == 1) { gte->MAC2 = mac; gte->IR2 = gte->saturate_ir(mac, 2, lm); }
if (row == 2) { gte->MAC3 = mac; gte->IR3 = gte->saturate_ir(mac, 3, lm); }
};
if (sf) {
mac1 >>= 12; mac2 >>= 12; mac3 >>= 12;
for (int row = 0; row < 3; ++row) {
const int mac_num = row + 1;
if (tv == 2) {
/* Hardware's FC translation selector is wired incorrectly: its
* translation+X term only feeds a temporary IR saturation, then
* MAC/IR are replaced by the Y/Z dot product. */
int64_t first = gte_mac44_stage(
gte, T[row] + (int64_t)M[row][0] * V[0], mac_num);
int32_t temporary = static_cast<int32_t>(first >> shift);
if (row == 0) gte->IR1 = gte->saturate_ir(temporary, 1, false);
if (row == 1) gte->IR2 = gte->saturate_ir(temporary, 2, false);
if (row == 2) gte->IR3 = gte->saturate_ir(temporary, 3, false);
int64_t tail = gte_mac44_stage(
gte, (int64_t)M[row][1] * V[1], mac_num);
set_mac_ir(row, tail + (int64_t)M[row][2] * V[2]);
} else {
int64_t acc = gte_mac44_stage(
gte, T[row] + (int64_t)M[row][0] * V[0], mac_num);
acc = gte_mac44_stage(
gte, acc + (int64_t)M[row][1] * V[1], mac_num);
set_mac_ir(row, acc + (int64_t)M[row][2] * V[2]);
}
}
gte->check_mac_overflow(mac1, 1);
gte->check_mac_overflow(mac2, 2);
gte->check_mac_overflow(mac3, 3);
gte->MAC1 = static_cast<int32_t>(mac1);
gte->MAC2 = static_cast<int32_t>(mac2);
gte->MAC3 = static_cast<int32_t>(mac3);
bool lm = (instr >> 10) & 1;
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm);
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm);
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm);
gte->set_error_flag();
}
@@ -1415,7 +1434,7 @@ void gte_mtc2(GTEState* gte, uint8_t reg, uint32_t value) {
case 20: gte->RGB[0] = value; break;
case 21: gte->RGB[1] = value; break;
case 22: gte->RGB[2] = value; break;
case 23: break; // RES1 reserved
case 23: gte->RES1 = value; break;
case 24: gte->MAC0 = value; break;
case 25: gte->MAC1 = value; break;
case 26: gte->MAC2 = value; break;
@@ -1448,11 +1467,11 @@ void gte_mtc2(GTEState* gte, uint8_t reg, uint32_t value) {
uint32_t gte_mfc2(GTEState* gte, uint8_t reg) {
switch (reg) {
case 0: return (static_cast<uint16_t>(gte->V0[1]) << 16) | static_cast<uint16_t>(gte->V0[0]);
case 1: return static_cast<uint16_t>(gte->V0[2]);
case 1: return static_cast<uint32_t>(static_cast<int32_t>(gte->V0[2]));
case 2: return (static_cast<uint16_t>(gte->V1[1]) << 16) | static_cast<uint16_t>(gte->V1[0]);
case 3: return static_cast<uint16_t>(gte->V1[2]);
case 3: return static_cast<uint32_t>(static_cast<int32_t>(gte->V1[2]));
case 4: return (static_cast<uint16_t>(gte->V2[1]) << 16) | static_cast<uint16_t>(gte->V2[0]);
case 5: return static_cast<uint16_t>(gte->V2[2]);
case 5: return static_cast<uint32_t>(static_cast<int32_t>(gte->V2[2]));
case 6: return gte->RGBC;
case 7: return gte->OTZ;
case 8: return static_cast<int32_t>(gte->IR0); // sign-extend
@@ -1470,7 +1489,7 @@ uint32_t gte_mfc2(GTEState* gte, uint8_t reg) {
case 20: return gte->RGB[0];
case 21: return gte->RGB[1];
case 22: return gte->RGB[2];
case 23: return 0; // RES1
case 23: return gte->RES1;
case 24: return gte->MAC0;
case 25: return gte->MAC1;
case 26: return gte->MAC2;
@@ -1523,7 +1542,10 @@ void gte_ctc2(GTEState* gte, uint8_t reg, uint32_t value) {
case 28: gte->DQB = value; break;
case 29: gte->ZSF3 = static_cast<int16_t>(value & 0xFFFF); break;
case 30: gte->ZSF4 = static_cast<int16_t>(value & 0xFFFF); break;
case 31: gte->FLAG = value & 0x7FFFF000u; break;
case 31:
gte->FLAG = value & 0x7FFFF000u;
gte->set_error_flag();
break;
default: break;
}
}
@@ -1537,7 +1559,7 @@ uint32_t gte_cfc2(GTEState* gte, uint8_t reg) {
case 1: return (static_cast<uint16_t>(gte->RT[1][0]) << 16) | static_cast<uint16_t>(gte->RT[0][2]);
case 2: return (static_cast<uint16_t>(gte->RT[1][2]) << 16) | static_cast<uint16_t>(gte->RT[1][1]);
case 3: return (static_cast<uint16_t>(gte->RT[2][1]) << 16) | static_cast<uint16_t>(gte->RT[2][0]);
case 4: return static_cast<uint16_t>(gte->RT[2][2]);
case 4: return static_cast<uint32_t>(static_cast<int32_t>(gte->RT[2][2]));
case 5: return gte->TR[0];
case 6: return gte->TR[1];
case 7: return gte->TR[2];
@@ -1545,7 +1567,7 @@ uint32_t gte_cfc2(GTEState* gte, uint8_t reg) {
case 9: return (static_cast<uint16_t>(gte->L[1][0]) << 16) | static_cast<uint16_t>(gte->L[0][2]);
case 10: return (static_cast<uint16_t>(gte->L[1][2]) << 16) | static_cast<uint16_t>(gte->L[1][1]);
case 11: return (static_cast<uint16_t>(gte->L[2][1]) << 16) | static_cast<uint16_t>(gte->L[2][0]);
case 12: return static_cast<uint16_t>(gte->L[2][2]);
case 12: return static_cast<uint32_t>(static_cast<int32_t>(gte->L[2][2]));
case 13: return gte->BK[0];
case 14: return gte->BK[1];
case 15: return gte->BK[2];
@@ -1553,13 +1575,14 @@ uint32_t gte_cfc2(GTEState* gte, uint8_t reg) {
case 17: return (static_cast<uint16_t>(gte->LC[1][0]) << 16) | static_cast<uint16_t>(gte->LC[0][2]);
case 18: return (static_cast<uint16_t>(gte->LC[1][2]) << 16) | static_cast<uint16_t>(gte->LC[1][1]);
case 19: return (static_cast<uint16_t>(gte->LC[2][1]) << 16) | static_cast<uint16_t>(gte->LC[2][0]);
case 20: return static_cast<uint16_t>(gte->LC[2][2]);
case 20: return static_cast<uint32_t>(static_cast<int32_t>(gte->LC[2][2]));
case 21: return gte->FC[0];
case 22: return gte->FC[1];
case 23: return gte->FC[2];
case 24: return gte->OFX;
case 25: return gte->OFY;
case 26: return gte->H;
case 26: return static_cast<uint32_t>(static_cast<int32_t>(
static_cast<int16_t>(gte->H)));
case 27: return static_cast<int32_t>(gte->DQA); // sign-extend
case 28: return gte->DQB;
case 29: return static_cast<int32_t>(gte->ZSF3);
@@ -1634,6 +1657,7 @@ static void gte_import_cpu_state(PSXRecomp::GTE::GTEState* gte,
gte->SXY[3] = static_cast<int32_t>(d[14]);
for (int i = 0; i < 4; ++i) gte->SZ[i] = static_cast<uint16_t>(d[16 + i]);
for (int i = 0; i < 3; ++i) gte->RGB[i] = d[20 + i];
gte->RES1 = d[23];
gte->MAC0 = static_cast<int32_t>(d[24]);
gte->MAC1 = static_cast<int32_t>(d[25]);
gte->MAC2 = static_cast<int32_t>(d[26]);
@@ -1700,11 +1724,11 @@ static void gte_export_cpu_state(CPUState* cpu,
uint32_t* c = cpu->gte_ctrl;
d[0] = gte_pack_s16_pair(gte->V0[0], gte->V0[1]);
d[1] = static_cast<uint16_t>(gte->V0[2]);
d[1] = static_cast<uint32_t>(static_cast<int32_t>(gte->V0[2]));
d[2] = gte_pack_s16_pair(gte->V1[0], gte->V1[1]);
d[3] = static_cast<uint16_t>(gte->V1[2]);
d[3] = static_cast<uint32_t>(static_cast<int32_t>(gte->V1[2]));
d[4] = gte_pack_s16_pair(gte->V2[0], gte->V2[1]);
d[5] = static_cast<uint16_t>(gte->V2[2]);
d[5] = static_cast<uint32_t>(static_cast<int32_t>(gte->V2[2]));
d[6] = gte->RGBC;
d[7] = gte->OTZ;
d[8] = static_cast<uint32_t>(static_cast<int32_t>(gte->IR0));
@@ -1717,7 +1741,7 @@ static void gte_export_cpu_state(CPUState* cpu,
d[15] = static_cast<uint32_t>(gte->SXY[3]);
for (int i = 0; i < 4; ++i) d[16 + i] = gte->SZ[i];
for (int i = 0; i < 3; ++i) d[20 + i] = gte->RGB[i];
d[23] = 0;
d[23] = gte->RES1;
d[24] = static_cast<uint32_t>(gte->MAC0);
d[25] = static_cast<uint32_t>(gte->MAC1);
d[26] = static_cast<uint32_t>(gte->MAC2);
@@ -1730,23 +1754,24 @@ static void gte_export_cpu_state(CPUState* cpu,
c[1] = gte_pack_s16_pair(gte->RT[0][2], gte->RT[1][0]);
c[2] = gte_pack_s16_pair(gte->RT[1][1], gte->RT[1][2]);
c[3] = gte_pack_s16_pair(gte->RT[2][0], gte->RT[2][1]);
c[4] = static_cast<uint16_t>(gte->RT[2][2]);
c[4] = static_cast<uint32_t>(static_cast<int32_t>(gte->RT[2][2]));
for (int i = 0; i < 3; ++i) c[5 + i] = static_cast<uint32_t>(gte->TR[i]);
c[8] = gte_pack_s16_pair(gte->L[0][0], gte->L[0][1]);
c[9] = gte_pack_s16_pair(gte->L[0][2], gte->L[1][0]);
c[10] = gte_pack_s16_pair(gte->L[1][1], gte->L[1][2]);
c[11] = gte_pack_s16_pair(gte->L[2][0], gte->L[2][1]);
c[12] = static_cast<uint16_t>(gte->L[2][2]);
c[12] = static_cast<uint32_t>(static_cast<int32_t>(gte->L[2][2]));
for (int i = 0; i < 3; ++i) c[13 + i] = static_cast<uint32_t>(gte->BK[i]);
c[16] = gte_pack_s16_pair(gte->LC[0][0], gte->LC[0][1]);
c[17] = gte_pack_s16_pair(gte->LC[0][2], gte->LC[1][0]);
c[18] = gte_pack_s16_pair(gte->LC[1][1], gte->LC[1][2]);
c[19] = gte_pack_s16_pair(gte->LC[2][0], gte->LC[2][1]);
c[20] = static_cast<uint16_t>(gte->LC[2][2]);
c[20] = static_cast<uint32_t>(static_cast<int32_t>(gte->LC[2][2]));
for (int i = 0; i < 3; ++i) c[21 + i] = static_cast<uint32_t>(gte->FC[i]);
c[24] = static_cast<uint32_t>(gte->OFX);
c[25] = static_cast<uint32_t>(gte->OFY);
c[26] = gte->H;
c[26] = static_cast<uint32_t>(static_cast<int32_t>(
static_cast<int16_t>(gte->H)));
c[27] = static_cast<uint32_t>(static_cast<int32_t>(gte->DQA));
c[28] = static_cast<uint32_t>(gte->DQB);
c[29] = static_cast<uint32_t>(static_cast<int32_t>(gte->ZSF3));
@@ -1982,20 +2007,17 @@ static uint32_t gte_sign_extend_16(uint32_t value) {
* invalidate host precision caches: it is also the compatibility boundary for
* committed AOT C emitted before all masked/aliased writes used helpers. */
static void gte_cpu_canonicalize_backing(CPUState* cpu) {
static const uint8_t data_u16[] = {1, 3, 5, 7, 16, 17, 18, 19};
static const uint8_t data_s16[] = {8, 9, 10, 11};
static const uint8_t ctrl_u16[] = {4, 12, 20, 26};
static const uint8_t ctrl_s16[] = {27, 29, 30};
static const uint8_t data_u16[] = {7, 16, 17, 18, 19};
static const uint8_t data_s16[] = {1, 3, 5, 8, 9, 10, 11};
static const uint8_t ctrl_s16[] = {4, 12, 20, 26, 27, 29, 30};
for (uint8_t reg : data_u16) cpu->gte_data[reg] &= 0xFFFFu;
for (uint8_t reg : data_s16)
cpu->gte_data[reg] = gte_sign_extend_16(cpu->gte_data[reg]);
cpu->gte_data[15] = cpu->gte_data[14];
cpu->gte_data[23] = 0;
cpu->gte_data[28] = cpu->gte_data[29] = gte_cpu_pack_irgb(cpu);
cpu->gte_data[31] = gte_cpu_lzcr(cpu->gte_data[30]);
for (uint8_t reg : ctrl_u16) cpu->gte_ctrl[reg] &= 0xFFFFu;
for (uint8_t reg : ctrl_s16)
cpu->gte_ctrl[reg] = gte_sign_extend_16(cpu->gte_ctrl[reg]);
}
@@ -2003,15 +2025,12 @@ static void gte_cpu_canonicalize_backing(CPUState* cpu) {
extern "C" uint32_t gte_read_data(CPUState* cpu, uint8_t reg) {
if (reg >= 32) return 0;
switch (reg) {
case 1: case 3: case 5: case 7:
case 16: case 17: case 18: case 19:
case 7: case 16: case 17: case 18: case 19:
return cpu->gte_data[reg] & 0xFFFFu;
case 8: case 9: case 10: case 11:
case 1: case 3: case 5: case 8: case 9: case 10: case 11:
return gte_sign_extend_16(cpu->gte_data[reg]);
case 15:
return cpu->gte_data[14];
case 23:
return 0;
case 28: case 29:
return gte_cpu_pack_irgb(cpu);
case 31:
@@ -2024,9 +2043,7 @@ extern "C" uint32_t gte_read_data(CPUState* cpu, uint8_t reg) {
extern "C" uint32_t gte_read_ctrl(CPUState* cpu, uint8_t reg) {
if (reg >= 32) return 0;
switch (reg) {
case 4: case 12: case 20: case 26:
return cpu->gte_ctrl[reg] & 0xFFFFu;
case 27: case 29: case 30:
case 4: case 12: case 20: case 26: case 27: case 29: case 30:
return gte_sign_extend_16(cpu->gte_ctrl[reg]);
default:
return cpu->gte_ctrl[reg];
@@ -2037,11 +2054,10 @@ extern "C" void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val) {
if (reg >= 32) return;
gte_cpu_canonicalize_backing(cpu);
switch (reg) {
case 1: case 3: case 5: case 7:
case 16: case 17: case 18: case 19:
case 7: case 16: case 17: case 18: case 19:
cpu->gte_data[reg] = val & 0xFFFFu;
break;
case 8: case 9: case 10: case 11:
case 1: case 3: case 5: case 8: case 9: case 10: case 11:
cpu->gte_data[reg] = gte_sign_extend_16(val);
if (reg >= 9) {
const uint32_t packed = gte_cpu_pack_irgb(cpu);
@@ -2076,9 +2092,6 @@ extern "C" void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val) {
pgxp_gte_reg_written(15, val);
}
break;
case 23:
cpu->gte_data[23] = 0;
break;
case 28: {
cpu->gte_data[9] = (val & 0x1Fu) << 7;
cpu->gte_data[10] = ((val >> 5) & 0x1Fu) << 7;
@@ -2111,14 +2124,13 @@ extern "C" void gte_write_ctrl(CPUState* cpu, uint8_t reg, uint32_t val) {
if (reg >= 32) return;
gte_cpu_canonicalize_backing(cpu);
switch (reg) {
case 4: case 12: case 20: case 26:
cpu->gte_ctrl[reg] = val & 0xFFFFu;
break;
case 27: case 29: case 30:
case 4: case 12: case 20: case 26: case 27: case 29: case 30:
cpu->gte_ctrl[reg] = gte_sign_extend_16(val);
break;
case 31:
cpu->gte_ctrl[31] = val & 0x7FFFF000u;
if (cpu->gte_ctrl[31] & PSXRecomp::GTE::FLAG_ERROR_MASK)
cpu->gte_ctrl[31] |= PSXRecomp::GTE::FLAG_ERROR_BIT;
break;
default:
cpu->gte_ctrl[reg] = val;
+60
View File
@@ -0,0 +1,60 @@
#include "guest_tty.h"
#include <stdatomic.h>
#include <stdio.h>
#define GUEST_TTY_RING_CAP 65536u
#define GUEST_TTY_RING_MASK (GUEST_TTY_RING_CAP - 1u)
static _Atomic unsigned char s_guest_tty_ring[GUEST_TTY_RING_CAP];
static _Atomic uint64_t s_guest_tty_total;
static char s_guest_tty_line[257];
static unsigned s_guest_tty_line_len;
void psx_guest_tty_putchar(uint8_t value)
{
uint64_t seq = atomic_load_explicit(&s_guest_tty_total,
memory_order_relaxed);
atomic_store_explicit(&s_guest_tty_ring[seq & GUEST_TTY_RING_MASK], value,
memory_order_relaxed);
atomic_store_explicit(&s_guest_tty_total, seq + 1u, memory_order_release);
/* Preserve the existing human-readable runtime log alongside the
* structured ring used by automation. */
if (value == '\n' || s_guest_tty_line_len >= 256u) {
s_guest_tty_line[s_guest_tty_line_len] = '\0';
fprintf(stderr, "tty: %s\n", s_guest_tty_line);
s_guest_tty_line_len = 0;
if (value != '\n' && value != '\r')
s_guest_tty_line[s_guest_tty_line_len++] = (char)value;
} else if (value != '\r') {
s_guest_tty_line[s_guest_tty_line_len++] = (char)value;
}
}
size_t psx_guest_tty_snapshot(uint8_t *out, size_t capacity,
uint64_t *total_bytes)
{
uint64_t total = atomic_load_explicit(&s_guest_tty_total,
memory_order_acquire);
size_t available = (total < GUEST_TTY_RING_CAP)
? (size_t)total : (size_t)GUEST_TTY_RING_CAP;
size_t count = (capacity < available) ? capacity : available;
uint64_t start = total - count;
if (total_bytes) *total_bytes = total;
if (!out) return count;
for (size_t i = 0; i < count; ++i) {
out[i] = atomic_load_explicit(
&s_guest_tty_ring[(start + i) & GUEST_TTY_RING_MASK],
memory_order_relaxed);
}
return count;
}
void psx_guest_tty_reset(void)
{
atomic_store_explicit(&s_guest_tty_total, 0u, memory_order_release);
s_guest_tty_line_len = 0;
}
+19 -18
View File
@@ -78,6 +78,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
#include "freeze_heartbeat.h"
#include "config_loader.h"
#include "bios_rom_alias.h"
#include "host_path.h"
#include "launcher_device.h"
#include "game_options.h"
#include "mod_plugins.h"
@@ -2128,7 +2129,7 @@ static void sdl_drc_callback(void* /*user*/, Uint8* stream, int len) {
static std::filesystem::path find_upward(std::filesystem::path start,
const std::filesystem::path& marker) {
std::error_code ec;
start = std::filesystem::absolute(start, ec);
start = PSXRecompV4::host_absolute(start, ec);
if (ec) start = std::filesystem::current_path();
if (!std::filesystem::is_directory(start, ec)) start = start.parent_path();
@@ -2167,12 +2168,12 @@ static std::filesystem::path exe_dir_from_argv(const char* argv0) {
// is the .AppImage's own path, so settings.toml anchors next to it.
if (exe_dir.empty()) {
if (const char* appimg = std::getenv("APPIMAGE"); appimg && appimg[0]) {
exe_dir = fs::absolute(appimg, ec).parent_path();
exe_dir = PSXRecompV4::host_absolute(appimg, ec).parent_path();
if (ec) exe_dir.clear();
}
}
if (exe_dir.empty() && argv0 && argv0[0]) {
exe_dir = fs::absolute(argv0, ec).parent_path();
exe_dir = PSXRecompV4::host_absolute(argv0, ec).parent_path();
if (ec) exe_dir.clear();
}
// Last-ditch only (should be unreachable on a normal launch); a bare "." so
@@ -2188,15 +2189,15 @@ static std::filesystem::path resolve_existing_runtime_path(const char* requested
std::error_code ec;
fs::path p(requested);
if (fs::exists(p, ec)) return fs::absolute(p, ec);
if (p.is_absolute()) return {};
if (fs::exists(p, ec)) return PSXRecompV4::host_absolute(p, ec);
if (PSXRecompV4::host_path_is_absolute(p)) return {};
// Anchor exclusively on the exe directory — never cwd (see exe_dir_from_argv).
const fs::path root = exe_dir_from_argv(argv0);
fs::path direct = root / p;
if (fs::exists(direct, ec)) return fs::absolute(direct, ec);
if (fs::exists(direct, ec)) return PSXRecompV4::host_absolute(direct, ec);
fs::path found = find_upward(root, p);
if (!found.empty()) return fs::absolute(found / p, ec);
if (!found.empty()) return PSXRecompV4::host_absolute(found / p, ec);
return {};
}
@@ -2685,24 +2686,24 @@ static std::filesystem::path resolve_bios_path(const char* requested, const char
if (!requested || !requested[0]) return {};
fs::path p(requested);
if (fs::exists(p, ec)) {
fs::path abs = fs::absolute(p, ec);
fs::path abs = PSXRecompV4::host_absolute(p, ec);
return ec ? p : abs;
}
// Either BIOS filename convention is acceptable: a dump folder holding
// "US-PSX-SCPH1001.BIN" satisfies a request for "SCPH1001.BIN" and vice
// versa (see recompiler/include/bios_rom_alias.h).
if (fs::path aliased = PSXRecompV4::resolve_bios_rom(p); aliased != p) {
fs::path abs = fs::absolute(aliased, ec);
fs::path abs = PSXRecompV4::host_absolute(aliased, ec);
return ec ? aliased : abs;
}
if (p.is_absolute()) return p;
if (PSXRecompV4::host_path_is_absolute(p)) return p;
// Anchor on the exe directory — never cwd (see exe_dir_from_argv).
fs::path found = find_upward(exe_dir_from_argv(argv0), p);
if (!found.empty()) return found / p;
// Same walk, accepting the other naming convention at each rung: the
// literal name is absent but a region-qualified sibling may be present.
for (fs::path dir = fs::absolute(exe_dir_from_argv(argv0), ec);
for (fs::path dir = PSXRecompV4::host_absolute(exe_dir_from_argv(argv0), ec);
!dir.empty(); dir = dir.parent_path()) {
const fs::path aliased = PSXRecompV4::resolve_bios_rom(dir / p);
if (aliased != dir / p && fs::exists(aliased, ec)) return aliased;
@@ -2764,7 +2765,7 @@ static std::filesystem::path discover_retail_bios_near(const char* argv0) {
const fs::path cand = dir / name_buf[ni];
if (retail_bios_file_ok(cand)) {
auto abs = fs::weakly_canonical(cand, ec);
if (ec) abs = fs::absolute(cand, ec);
if (ec) abs = PSXRecompV4::host_absolute(cand, ec);
return abs;
}
}
@@ -3929,7 +3930,7 @@ static std::filesystem::path resolve_overlay_capture_path(
auto root_relative = [&](const std::string& raw) {
std::filesystem::path p(raw);
return p.is_absolute() ? p : value_base / p;
return PSXRecompV4::host_path_is_absolute(p) ? p : value_base / p;
};
std::filesystem::path result;
if (!direct.empty()) result = root_relative(direct);
@@ -12530,7 +12531,7 @@ int main(int argc, char** argv) {
default_game_config_storage = default_game_config.string();
game_config_path = default_game_config_storage.c_str();
}
} else if (!std::filesystem::path(game_config_path).is_absolute()) {
} else if (!PSXRecompV4::host_path_is_absolute(std::filesystem::path(game_config_path))) {
// An explicit --game with a relative path must ALSO anchor on the exe
// dir, never cwd — otherwise the disc / memcard_dir / game_options.toml
// that resolve against this file's parent silently point at cwd. Resolve
@@ -13739,7 +13740,7 @@ int main(int argc, char** argv) {
std::error_code ec;
if (!resolved.empty() && std::filesystem::exists(resolved, ec)) {
seed.bios_path = std::filesystem::weakly_canonical(resolved, ec);
if (ec) seed.bios_path = std::filesystem::absolute(resolved, ec);
if (ec) seed.bios_path = PSXRecompV4::host_absolute(resolved, ec);
seed.has_bios_path = true;
} else {
seed.bios_path.clear();
@@ -14035,7 +14036,7 @@ int main(int argc, char** argv) {
if (!resolved.empty() &&
std::filesystem::exists(resolved, ec)) {
auto abs = std::filesystem::weakly_canonical(resolved, ec);
if (ec) abs = std::filesystem::absolute(resolved, ec);
if (ec) abs = PSXRecompV4::host_absolute(resolved, ec);
std::snprintf(ls.bios_path, sizeof(ls.bios_path), "%s",
abs.string().c_str());
}
@@ -14223,7 +14224,7 @@ int main(int argc, char** argv) {
seed.bios_path =
std::filesystem::weakly_canonical(resolved, ec);
if (ec)
seed.bios_path = std::filesystem::absolute(resolved, ec);
seed.bios_path = PSXRecompV4::host_absolute(resolved, ec);
} else {
seed.bios_path = ls.bios_path;
}
@@ -15961,7 +15962,7 @@ soft_return_lobby:
std::error_code ec;
if (!resolved.empty() && std::filesystem::exists(resolved, ec)) {
auto abs = std::filesystem::weakly_canonical(resolved, ec);
if (ec) abs = std::filesystem::absolute(resolved, ec);
if (ec) abs = PSXRecompV4::host_absolute(resolved, ec);
std::snprintf(ls.bios_path, sizeof(ls.bios_path), "%s",
abs.string().c_str());
} else {
+1
View File
@@ -23,6 +23,7 @@
#include "lockstep.h"
#include "data_shards.h"
#include "dirty_ram_interp.h"
#include "guest_tty.h"
#include "psx_cycles.h"
#include "starvation_ring.h"
#include <stdint.h>
+12
View File
@@ -703,6 +703,10 @@ void psx_muldiv_stall(CPUState* cpu) {
* §1+DO_LDS that bracket this ran in the instruction's psx_cyc_step (COP2 is non-load).
* MTC2/CTC2 (writes) use psx_gte_stall (stall only, no give-back). */
void psx_gte_read(CPUState* cpu, uint32_t rt) {
/* Include deferred CPU work before computing a deadline or stall. A
* later advance publishes it too, so using the old clock double-counts
* that work in the stall and can arm a command deadline too early. */
psx_cyc_batch_flush();
if (cpu->gte_ts_done > psx_cycle_count) {
uint32_t stall = (uint32_t)(cpu->gte_ts_done - psx_cycle_count);
cpu->ld_absorb = stall;
@@ -760,6 +764,10 @@ uint32_t psx_gte_cmd_latency(uint32_t cmd) {
}
void psx_gte_set(CPUState* cpu, uint32_t latency) {
/* Include deferred CPU work before computing a deadline or stall. A
* later advance publishes it too, so using the old clock double-counts
* that work in the stall and can arm a command deadline too early. */
psx_cyc_batch_flush();
/* Back-to-back GTE ops serialize: finish the prior op first. */
if (cpu->gte_ts_done > psx_cycle_count) {
psx_advance_cycles((uint32_t)(cpu->gte_ts_done - psx_cycle_count));
@@ -768,6 +776,10 @@ void psx_gte_set(CPUState* cpu, uint32_t latency) {
}
void psx_gte_stall(CPUState* cpu) {
/* Include deferred CPU work before computing a deadline or stall. A
* later advance publishes it too, so using the old clock double-counts
* that work in the stall and can arm a command deadline too early. */
psx_cyc_batch_flush();
if (cpu->gte_ts_done > psx_cycle_count) {
psx_advance_cycles((uint32_t)(cpu->gte_ts_done - psx_cycle_count));
}
+50
View File
@@ -0,0 +1,50 @@
// SYSTEM.CNF beyond 16 MiB wins over unrelated early cdrom: strings.
#include "disc_identity.h"
#include <cassert>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <vector>
namespace fs=std::filesystem;
static void both32(uint8_t* p,uint32_t value) {
for(unsigned i=0;i<4;i++) {p[i]=(uint8_t)(value>>(8*i));p[7-i]=p[i];}
}
static void fixture(const fs::path& path,bool raw) {
const unsigned unit=raw?2352:2048,offset=raw?24:0;
std::ofstream file(path,std::ios::binary);
file.seekp(10000ull*unit-1);file.put(0);
if(raw) {
uint8_t sync[16]={};std::memset(sync+1,255,10);sync[15]=2;
file.seekp(0);file.write((char*)sync,sizeof sync);
}
auto write=[&](unsigned lba,const std::vector<uint8_t>& data) {
file.seekp((uint64_t)lba*unit+offset);
file.write((const char*)data.data(),data.size());
};
std::vector<uint8_t> pvd(2048);
pvd[0]=1;std::memcpy(pvd.data()+1,"CD001",5);pvd[6]=1;
auto* root=pvd.data()+156;root[0]=34;both32(root+2,20);both32(root+10,2048);
root[25]=2;root[28]=root[31]=root[32]=1;
write(16,pvd);
const std::string cnf="BOOT = cdrom:SLPS_009.98;1\r\n";
std::vector<uint8_t> directory(2048);
directory[0]=46;both32(directory.data()+2,9000);both32(directory.data()+10,(uint32_t)cnf.size());
directory[28]=directory[31]=1;directory[32]=12;
std::memcpy(directory.data()+33,"SYSTEM.CNF;1",12);write(20,directory);
write(9000,std::vector<uint8_t>(cnf.begin(),cnf.end()));
const std::string decoy="BOOT = cdrom:SLUS_999.99;1\r\n";
write(40,std::vector<uint8_t>(decoy.begin(),decoy.end()));
}
int main() {
auto root=fs::temp_directory_path()/ ("psx-disc-identity-"+std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count()));
fs::create_directory(root);
for(bool raw:{false,true}) {
auto image=root/(raw?"disc.bin":"disc.iso");fixture(image,raw);
auto id=PSXRecompV4::identify_disc(image,"",0,false,false);
assert(id.opened && id.has_header && id.detected_serial=="SLPS-00998" && id.region=="NTSC-J");
}
fs::remove(root/"disc.iso");fs::remove(root/"disc.bin");fs::remove(root);
}
+112
View File
@@ -0,0 +1,112 @@
/* Production GTE deadline regressions. No BIOS, disc or copied guest code. */
#include "psx_cyc.h"
#include <assert.h>
#include <stdio.h>
int g_ls_replay_active = 0;
int g_ls_mode = 0;
int g_precise_mode = 0;
int g_psx_call_bail = 0;
uint32_t i_mask = 0;
uint64_t g_guest_store_count = 0;
uint64_t g_mmio_access_count = 0;
static uint32_t s_cd_cycles_remaining = 5;
static int s_cd_ready = 0;
static uint32_t s_dma_ready_cycles = 0;
void sio_advance(uint32_t cycles) { (void)cycles; }
void cdrom_advance(uint32_t cycles) {
if (s_cd_ready) return;
if (cycles >= s_cd_cycles_remaining) {
s_cd_cycles_remaining = 0;
s_cd_ready = 1;
} else {
s_cd_cycles_remaining -= cycles;
}
}
void dma_advance(uint32_t cycles) {
if (s_cd_ready) s_dma_ready_cycles += cycles;
}
void timers_advance(uint32_t cycles) { (void)cycles; }
void interrupts_advance_cycles(uint32_t cycles) { (void)cycles; }
void interrupts_service_scheduled_events(void) {}
uint32_t interrupts_cycles_to_vblank(void) { return UINT32_MAX; }
uint32_t timers_cycles_to_irq(uint32_t mask) { (void)mask; return UINT32_MAX; }
uint32_t cdrom_cycles_to_irq(uint32_t mask) {
(void)mask;
return s_cd_ready ? UINT32_MAX : s_cd_cycles_remaining;
}
uint32_t dma_cycles_to_internal_event(void) { return UINT32_MAX; }
void source_gpu_runtime_advance(void) {}
uint32_t source_gpu_runtime_cycles_to_event(void) { return UINT32_MAX; }
uint32_t dma_cycles_to_deliverable_irq(uint32_t mask) {
(void)mask;
return UINT32_MAX;
}
uint32_t sio_cycles_to_irq(uint32_t mask) { (void)mask; return UINT32_MAX; }
/* SPU sample-event scheduler (golden 1a973806): psx_cycles.c consults it; stub here. */
static uint32_t s_spu_next_sample = UINT32_MAX;
uint32_t psx_spu_sample_event_cycles_to_next(void) { return s_spu_next_sample; }
void psx_spu_sample_event_service(void) {}
int psx_get_in_exception(void) { return 0; }
void starvation_watchdog_check(void) {}
void starvation_ring_pc_sample(void) {}
int psx_netplay_active(void) { return 0; }
int psx_selfcheck_enabled(void) { return 0; }
void dirty_ram_ld_delay_discard(void) {}
void dirty_ram_irq_ambient_resync_after_restore(void) {}
static uint32_t local;
static unsigned checks;
#define CHECK(x) do {checks++;if(!(x)){fprintf(stderr,"FAIL line%d: %s\n",__LINE__,#x);return 1;}}while(0)
static void reset(unsigned elapsed,unsigned batch,unsigned local_value) {
psx_cycles_reset_for_boot();s_cd_ready=1;s_spu_next_sample=UINT32_MAX;
psx_advance_cycles(1000+elapsed);
psx_next_service_cycle=UINT64_MAX;
g_psx_cyc_batch=batch;g_psx_cyc_batch_limit=64;g_psx_cyc_bb_defer=1;
local=local_value;g_psx_cyc_local_acc=local_value?&local:NULL;
}
int main(void) {
CPUState cpu={0};
/* Authored timing equivalent of DPCS / ADDIU / SWC2 with two remaining
* load-overlap credits. Source return(ret-1) gives a seven-cycle deadline.
* SWC2's one pending base cycle must be part of its stall comparison. */
reset(0,0,0);cpu.read_absorb_which=23;cpu.read_absorb[23]=2;
cpu.ld_which_t=32;cpu.ld_absorb=5;
psx_cyc_step(&cpu,0);psx_gte_set(&cpu,psx_gte_cmd_latency(0x10));
psx_cyc_step(&cpu,(1u<<2)|(1u<<14));psx_cyc_step(&cpu,0);
psx_gte_stall(&cpu);psx_cyc_batch_flush();
CHECK(psx_cycle_count==1007);
/* Every command latency, pending source (batch/local/both), and prior
* command overlap. Deadline ownership is max(published time,old deadline). */
for(unsigned cmd=0;cmd<64;cmd++)for(unsigned pending=0;pending<=64;pending++)
for(unsigned mode=0;mode<3;mode++)for(unsigned lead=0;lead<=43;lead+=43) {
reset(0,mode==1?0:pending,mode==0?0:pending);cpu.gte_ts_done=1000+lead;
unsigned elapsed=(mode==2?2:1)*pending;
uint64_t at=1000+(elapsed>lead?elapsed:lead);
psx_gte_set(&cpu,psx_gte_cmd_latency(cmd));
CHECK(psx_cycle_count==at && cpu.gte_ts_done==at+psx_gte_cmd_latency(cmd));
CHECK(g_psx_cyc_batch==0 && local==0);
}
/* Reads additionally return only the actual stall as the new load credit.
* Ordinary writes/stores preserve the existing credit and delayed slot. */
for(unsigned elapsed=0;elapsed<=64;elapsed++)for(unsigned pending=0;pending<=elapsed;pending++)
for(unsigned mode=0;mode<2;mode++)for(unsigned read=0;read<2;read++) {
memset(&cpu,0,sizeof(cpu));cpu.gte_ts_done=1043;cpu.ld_absorb=19;cpu.ld_which_t=32;
cpu.read_absorb_which=5;cpu.read_absorb[5]=11;
reset(elapsed-pending,mode?0:pending,mode?pending:0);
if(read)psx_gte_read(&cpu,7);else psx_gte_stall(&cpu);
unsigned stall=elapsed<43?43-elapsed:0;
CHECK(psx_cycle_count==1000+elapsed+stall);
CHECK(cpu.ld_absorb==(read?stall:19) && cpu.ld_which_t==(read?7:32));
CHECK(cpu.read_absorb[5]==11 && g_psx_cyc_batch==0 && local==0);
}
printf("PASS %u production GTE pending-charge/deadline checks\n",checks);return 0;
}
+30 -1
View File
@@ -275,6 +275,35 @@ int test_reads() {
return 0;
}
int test_hardware_register_semantics() {
CPUState cpu{};
gte_write_data(&cpu, 1, 0x00008001u);
if (gte_read_data(&cpu, 1) != 0xFFFF8001u)
return fail_value("VZ sign extension", 0, 1, 0x00008001u,
0xFFFF8001u, gte_read_data(&cpu, 1));
gte_write_data(&cpu, 23, 0xDEADBEEFu);
if (gte_read_data(&cpu, 23) != 0xDEADBEEFu)
return fail_value("RES1 round trip", 0, 23, 0xDEADBEEFu,
0xDEADBEEFu, gte_read_data(&cpu, 23));
gte_write_ctrl(&cpu, 4, 0x00008002u);
if (gte_read_ctrl(&cpu, 4) != 0xFFFF8002u)
return fail_value("matrix tail sign extension", 0, 4, 0x00008002u,
0xFFFF8002u, gte_read_ctrl(&cpu, 4));
gte_write_ctrl(&cpu, 26, 0x0000E810u);
if (gte_read_ctrl(&cpu, 26) != 0xFFFFE810u)
return fail_value("H read sign extension", 0, 26, 0x0000E810u,
0xFFFFE810u, gte_read_ctrl(&cpu, 26));
gte_write_ctrl(&cpu, 31, 0x00800000u);
if (gte_read_ctrl(&cpu, 31) != 0x80800000u)
return fail_value("FLAG error summary", 0, 31, 0x00800000u,
0x80800000u, gte_read_ctrl(&cpu, 31));
return 0;
}
int test_writes() {
for (unsigned iteration = 0; iteration < 64; ++iteration) {
CPUState seed;
@@ -397,7 +426,6 @@ int test_command_marshaling() {
return fail_state("command marshal", iteration, function, cmd,
expected, actual);
if (actual.gte_data[15] != actual.gte_data[14] ||
actual.gte_data[23] != 0u ||
actual.gte_data[28] != actual.gte_data[29] ||
actual.gte_data[31] != gte_read_data(&actual, 31))
return fail_value("command canonical aliases", iteration,
@@ -703,6 +731,7 @@ int test_precision_speculative_transaction() {
} // namespace
int main() {
if (int rc = test_hardware_register_semantics()) return rc;
if (int rc = test_canonicalizer()) return rc;
if (int rc = test_reads()) return rc;
if (int rc = test_writes()) return rc;
+33
View File
@@ -0,0 +1,33 @@
#include "guest_tty.h"
#include <assert.h>
#include <stdint.h>
#include <string.h>
int main(void)
{
uint8_t out[8] = {0};
uint64_t total = 99;
psx_guest_tty_reset();
assert(psx_guest_tty_snapshot(out, sizeof(out), &total) == 0);
assert(total == 0);
psx_guest_tty_putchar('A');
psx_guest_tty_putchar('B');
psx_guest_tty_putchar(0);
psx_guest_tty_putchar('\n');
assert(psx_guest_tty_snapshot(out, sizeof(out), &total) == 4);
assert(total == 4);
assert(memcmp(out, "AB\0\n", 4) == 0);
memset(out, 0, sizeof(out));
assert(psx_guest_tty_snapshot(out, 2, &total) == 2);
assert(total == 4);
assert(out[0] == 0 && out[1] == '\n');
psx_guest_tty_reset();
assert(psx_guest_tty_snapshot(out, sizeof(out), &total) == 0);
assert(total == 0);
return 0;
}
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
"""Guard architectural JALR link-register and non-$ra transfer handling."""
from pathlib import Path
import sys
def main() -> int:
root = Path(__file__).resolve().parents[2]
source = (root / "runtime/src/dirty_ram_interp.c").read_text(encoding="utf-8")
start = source.index("case 0x09: { /* JALR rd, rs */")
end = source.index("case 0x0C: /* SYSCALL */", start)
jalr = source[start:end]
if "if (rd != 0) cpu->gpr[rd] = return_pc;" not in jalr:
raise AssertionError("JALR must write the encoded nonzero link register")
if "cpu->gpr[rd ? rd : 31]" in jalr:
raise AssertionError("JALR rd=0 must not be rewritten as an implicit $ra link")
chain = jalr.index("if (rd != 31)")
compiled = jalr.index("interp_enter_compiled(cpu, target)")
nonlocal_call = jalr.index("dispatch_nonlocal_call(cpu, target")
if not chain < compiled < nonlocal_call:
raise AssertionError(
"non-$ra JALR must pc-chain before any call-unit optimization"
)
non_ra = jalr[chain:compiled]
if "cpu->pc = target;" not in non_ra or "CRET(CRES_PCCHAIN, 1);" not in non_ra:
raise AssertionError("non-$ra JALR is missing its faithful pc-chain handoff")
print("JALR link-register guards: ok")
return 0
if __name__ == "__main__":
sys.exit(main())
+43
View File
@@ -0,0 +1,43 @@
cmake_minimum_required(VERSION 3.20)
project(PSXConformanceTestROM C CXX)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
get_filename_component(_default_root "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
set(PSXRECOMP_ROOT "${_default_root}" CACHE PATH "Path to PSXRecomp")
set(CONFORMANCE_RUN_DIR "" CACHE PATH "Prepared conformance fixture directory")
set(CONFORMANCE_DEBUG_PORT "4601" CACHE STRING "Runtime debug port")
# The headless conformance binary does not expose product UI features. Keep
# optional runtime integrations out so a clean fixture build needs no unrelated
# private/submodule state.
set(PSX_REWIND OFF CACHE BOOL "Disable rewind in conformance fixture" FORCE)
set(PSX_NETPLAY OFF CACHE BOOL "Disable netplay in conformance fixture" FORCE)
set(PSX_RECOMP_UI OFF CACHE BOOL "Disable launcher UI in conformance fixture" FORCE)
set(PSX_DEBUG_TOOLS ON CACHE BOOL "Enable the structured oracle endpoint" FORCE)
if(NOT CONFORMANCE_RUN_DIR)
message(FATAL_ERROR "Set -DCONFORMANCE_RUN_DIR=<prepared fixture directory>")
endif()
get_filename_component(CONFORMANCE_RUN_DIR "${CONFORMANCE_RUN_DIR}" ABSOLUTE)
file(GLOB _game_full "${CONFORMANCE_RUN_DIR}/generated/test.exe_full_*.c")
if(NOT _game_full AND EXISTS "${CONFORMANCE_RUN_DIR}/generated/test.exe_full.c")
set(_game_full "${CONFORMANCE_RUN_DIR}/generated/test.exe_full.c")
endif()
set(_game_dispatch "${CONFORMANCE_RUN_DIR}/generated/test.exe_dispatch.c")
if(NOT _game_full OR NOT EXISTS "${_game_dispatch}")
message(FATAL_ERROR "Fixture has not been recompiled: ${CONFORMANCE_RUN_DIR}")
endif()
include("${PSXRECOMP_ROOT}/runtime/runtime.cmake")
psxrecomp_add_runtime_target(psx-conformance
GAME_GENERATED_FULL_C ${_game_full}
GAME_GENERATED_DISPATCH_C "${_game_dispatch}"
DEBUG_PORT "${CONFORMANCE_DEBUG_PORT}"
WINDOW_TITLE "PSX Conformance"
DEFAULT_GAME_CONFIG_PATH "${CONFORMANCE_RUN_DIR}/game.toml"
EXE_NAME "PSXConformance"
)
+70
View File
@@ -0,0 +1,70 @@
# External PS1 conformance fixtures
This harness runs exact, pinned public PS-X EXE tests through PSXRecomp and
compares the guest console output with the fixture's expected PS1 log. It is a
small regression oracle, not a replacement for matched hardware capture.
Initial fixtures from `JaCzekanski/ps1-tests` release `build-158`:
- `cpu-cop`: 17 CPU exception/coprocessor-access assertions.
- `gte-test-all`: 1,150 GTE register/opcode vectors.
The MIT fixture binaries are not vendored. `fixtures.json` records their exact
release, paths, and SHA-256 identities. The synthetic boot disc additionally
requires `license_data.dat` extracted locally from a PlayStation disc owned by
the operator; that file and every prepared run remain outside Git.
## Prepare
Build `psxrecomp-toml`, `psxrecomp-game`, and an OpenBIOS backend first. Then:
```powershell
python .\tools\conformance_testrom\conformance.py prepare `
--fixture cpu-cop `
--source-root D:\private\ps1-tests-build-158 `
--output D:\validation\cpu-cop-v1 `
--framework-root D:\src\psxrecomp `
--recompiler-build D:\build\psxrecomp-recompiler `
--mkpsxiso D:\tools\mkpsxiso.exe `
--license-data D:\private\license_data.dat
```
The command refuses an occupied output directory, verifies every private input,
generates a synthetic licensed disc, recompiles the test, and writes a
hash-bound `prepare-receipt.json`.
## Build and run
```powershell
cmake -S .\tools\conformance_testrom -B D:\build\cpu-cop `
-G Ninja `
-DCMAKE_BUILD_TYPE=Release `
-DCMAKE_C_COMPILER=C:\mingw64\bin\gcc.exe `
-DCMAKE_CXX_COMPILER=C:\mingw64\bin\g++.exe `
-DCONFORMANCE_RUN_DIR=D:\validation\cpu-cop-v1 `
-DCONFORMANCE_DEBUG_PORT=4601
cmake --build D:\build\cpu-cop --target psx-conformance
python .\tools\conformance_testrom\conformance.py run `
--run-dir D:\validation\cpu-cop-v1 `
--executable D:\build\cpu-cop\PSXConformance.exe `
--bios .\bios\openbios.bin `
--port 4601
```
Use the same MinGW toolchain as the runtime. A default Windows Clang/MSVC
configuration may select MASM for libchdr's optional assembly and fail when
`ml64.exe` is not installed.
`run` starts the runtime headlessly, retrieves the bounded guest-TTY ring over
the existing JSON debug protocol, performs an exact normalized comparison, and
writes `result.json`. Normalization removes capture-format `% ` prefixes, blank
outer lines, CR/LF differences, and the derived `Total tests:` line which some
binaries print but their pinned expected logs omit. Assertion text and pass/fail
counts remain exact; the complete normalized capture is retained in the result.
## Oracle status
The release's expected `psx.log` is the first automated oracle. Promotion to a
hardware-qualified result requires Eagle to run the same hashed `test.bin` and
capture the same normalized console result or another unambiguous pass surface.
+309
View File
@@ -0,0 +1,309 @@
#!/usr/bin/env python3
"""Prepare and run pinned PS1 test-ROM fixtures against PSXRecomp."""
from __future__ import annotations
import argparse
import difflib
import hashlib
import json
import os
from pathlib import Path
import shutil
import socket
import subprocess
import sys
import time
HERE = Path(__file__).resolve().parent
FIXTURES_PATH = HERE / "fixtures.json"
def sha256(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest().upper()
def require_hash(path: Path, expected: str) -> None:
actual = sha256(path)
if actual != expected.upper():
raise RuntimeError(f"hash mismatch for {path}: {actual} != {expected}")
def run_checked(argv: list[str], cwd: Path) -> None:
print("+", subprocess.list2cmdline(argv))
subprocess.run(argv, cwd=cwd, check=True)
def normalized_log(data: bytes) -> str:
text = data.decode("utf-8", errors="replace").replace("\r\n", "\n").replace("\r", "\n")
lines = text.splitlines()
while lines and not lines[0]:
lines.pop(0)
while lines and not lines[-1]:
lines.pop()
lines = [line[2:] if line.startswith("% ") else line for line in lines]
return "\n".join(lines) + ("\n" if lines else "")
def comparable_log(text: str) -> str:
# Some ps1-tests binaries print a derived Total count that their pinned
# expected log omits. Preserve it in actual_raw_normalized/result.json,
# but do not let this non-assertion summary line decide conformance.
lines = [line for line in text.splitlines()
if not (line.startswith("Total tests: ") and line[13:].isdigit())]
return "\n".join(lines) + ("\n" if lines else "")
def load_catalog() -> dict:
return json.loads(FIXTURES_PATH.read_text(encoding="utf-8"))
def git_head(root: Path) -> str:
return subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=root, text=True
).strip()
def prepare(args: argparse.Namespace) -> int:
catalog = load_catalog()
spec = catalog["fixtures"].get(args.fixture)
if spec is None:
raise RuntimeError(f"unknown fixture: {args.fixture}")
source_root = args.source_root.resolve()
output = args.output.resolve()
framework = args.framework_root.resolve()
recompiler_build = args.recompiler_build.resolve()
mkpsxiso = args.mkpsxiso.resolve()
license_data = args.license_data.resolve()
if output.exists():
raise RuntimeError(f"output already exists; preserve it and choose a new path: {output}")
source_exe = source_root / spec["exe"]
source_expected = source_root / spec["expected"]
require_hash(source_exe, spec["exe_sha256"])
require_hash(source_expected, spec["expected_sha256"])
for required in (
recompiler_build / "psxrecomp-toml.exe",
recompiler_build / "psxrecomp-game.exe",
framework / "bios" / "OpenBIOS.toml",
mkpsxiso,
license_data,
):
if not required.is_file():
raise RuntimeError(f"missing required input: {required}")
output.mkdir(parents=True)
disc = output / "disc"
disc.mkdir()
shutil.copy2(source_exe, output / "test.exe")
shutil.copy2(source_expected, output / "expected.log")
shutil.copy2(license_data, disc / "license_data.dat")
run_checked([
str(recompiler_build / "psxrecomp-toml.exe"), "test.exe",
"--output", "game.toml", "--seeds", "seeds.txt",
"--name", args.fixture, "--id", "PSXT-00001",
], output)
game_toml = output / "game.toml"
text = game_toml.read_text(encoding="utf-8")
bios_profile = (framework / "bios" / "OpenBIOS.toml").as_posix()
text = text.replace(
'seeds = ""',
'seeds = "seeds.txt"',
).replace(
'out_dir = "generated"',
f'out_dir = "generated"\nbios_config = "{bios_profile}"',
).replace(
'# debug_port = 4370',
f'debug_port = {args.port}',
)
game_toml.write_text(text, encoding="utf-8", newline="\n")
(disc / "SYSTEM.CNF").write_text(
"BOOT = cdrom:\\TEST_000.01;1\nTCB = 4\nEVENT = 10\nSTACK = 801FFFF0\n",
encoding="ascii", newline="\n")
volume = "PSXTCPU" if args.fixture == "cpu-cop" else "PSXTGTE"
(disc / "disc.xml").write_text(f'''<?xml version="1.0" encoding="UTF-8"?>
<iso_project image_name="test.bin" cue_sheet="test.cue" no_xa="0">
<track type="data" xa_edc="true" new_type="true">
<identifiers system="PLAYSTATION" application="PLAYSTATION"
volume="{volume}" volume_set="{volume}" publisher="PSXRECOMP"
data_preparer="MKPSXISO" copyright="SYSTEM.CNF;1"/>
<license file="license_data.dat"/>
<directory_tree>
<file name="system.cnf" type="data" source="SYSTEM.CNF"/>
<file name="test_000.01" type="data" source="../test.exe"/>
</directory_tree>
</track>
</iso_project>
''', encoding="utf-8", newline="\n")
run_checked([str(mkpsxiso), "-y", "disc.xml"], disc)
run_checked([str(recompiler_build / "psxrecomp-game.exe"), "--config", "game.toml"], output)
receipt = {
"schema_version": 1,
"fixture": args.fixture,
"class": spec["class"],
"expected_assertions": spec["expected_assertions"],
"source": catalog["source"],
"framework": {"path": str(framework), "commit": git_head(framework)},
"inputs": {
"test.exe": sha256(output / "test.exe"),
"expected.log": sha256(output / "expected.log"),
"license_data.dat": sha256(disc / "license_data.dat"),
"psxrecomp-toml.exe": sha256(recompiler_build / "psxrecomp-toml.exe"),
"psxrecomp-game.exe": sha256(recompiler_build / "psxrecomp-game.exe"),
"mkpsxiso.exe": sha256(mkpsxiso),
},
"outputs": {
"disc/test.bin": sha256(disc / "test.bin"),
"disc/test.cue": sha256(disc / "test.cue"),
"generated/test.exe_dispatch.c": sha256(output / "generated" / "test.exe_dispatch.c"),
},
"oracle": {
"upstream_expected_log": "available",
"matched_hardware_recapture": "pending",
},
"debug_port": args.port,
}
(output / "prepare-receipt.json").write_text(
json.dumps(receipt, indent=2) + "\n", encoding="utf-8", newline="\n")
print(f"prepared {args.fixture}: {output}")
return 0
def tcp_query(port: int, payload: dict, timeout: float = 5.0) -> dict:
wire = (json.dumps(payload, separators=(",", ":")) + "\n").encode("ascii")
with socket.create_connection(("127.0.0.1", port), timeout=timeout) as sock:
sock.sendall(wire)
stream = sock.makefile("rb")
line = stream.readline()
if not line:
raise RuntimeError("debug server closed without a response")
return json.loads(line)
def capture_tty(port: int, timeout: float) -> bytes:
deadline = time.monotonic() + timeout
last = b""
while time.monotonic() < deadline:
try:
response = tcp_query(port, {"cmd": "guest_tty_dump", "tail": 65536})
if not response.get("ok"):
raise RuntimeError(f"guest_tty_dump failed: {response}")
last = bytes.fromhex(response.get("hex", ""))
if "Done.\n" in normalized_log(last):
return last
except (ConnectionError, OSError):
pass
time.sleep(0.1)
return last
def compare_capture(run_dir: Path, actual: bytes, result_path: Path) -> bool:
expected_path = run_dir / "expected.log"
expected = expected_path.read_bytes()
expected_norm = comparable_log(normalized_log(expected))
actual_raw_norm = normalized_log(actual)
actual_norm = actual_raw_norm
expected_lines = expected_norm.splitlines()
if expected_lines:
actual_lines = actual_raw_norm.splitlines()
try:
start = actual_lines.index(expected_lines[0])
except ValueError:
pass
else:
actual_norm = comparable_log("\n".join(actual_lines[start:]) + "\n")
passed = actual_norm == expected_norm
diff = "" if passed else "\n".join(difflib.unified_diff(
expected_norm.splitlines(), actual_norm.splitlines(),
fromfile="expected", tofile="actual", lineterm=""))
result = {
"schema_version": 1,
"status": "pass" if passed else "fail",
"expected_sha256": sha256(expected_path),
"actual_sha256": hashlib.sha256(actual).hexdigest().upper(),
"expected_normalized": expected_norm,
"actual_raw_normalized": actual_raw_norm,
"actual_normalized": actual_norm,
"diff": diff,
}
result_path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8", newline="\n")
return passed
def run_fixture(args: argparse.Namespace) -> int:
run_dir = args.run_dir.resolve()
executable = args.executable.resolve()
bios = args.bios.resolve()
for path in (run_dir / "game.toml", run_dir / "disc" / "test.cue", executable, bios):
if not path.is_file():
raise RuntimeError(f"missing required run input: {path}")
argv = [str(executable), "--no-launcher", "--headless",
"--game", str(run_dir / "game.toml"),
"--bios", str(bios), "--disc", str(run_dir / "disc" / "test.cue")]
creationflags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
proc = subprocess.Popen(argv, cwd=run_dir, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, creationflags=creationflags)
try:
actual = capture_tty(args.port, args.timeout)
finally:
proc.terminate()
try:
proc.wait(timeout=3)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=3)
result_path = run_dir / "result.json"
passed = compare_capture(run_dir, actual, result_path)
print(f"{run_dir.name}: {'PASS' if passed else 'FAIL'} ({len(actual)} guest-TTY bytes)")
print(result_path)
return 0 if passed else 1
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command", required=True)
prep = sub.add_parser("prepare")
prep.add_argument("--fixture", required=True, choices=sorted(load_catalog()["fixtures"]))
prep.add_argument("--source-root", type=Path, required=True)
prep.add_argument("--output", type=Path, required=True)
prep.add_argument("--framework-root", type=Path, required=True)
prep.add_argument("--recompiler-build", type=Path, required=True)
prep.add_argument("--mkpsxiso", type=Path, required=True)
prep.add_argument("--license-data", type=Path, required=True)
prep.add_argument("--port", type=int, default=4601)
prep.set_defaults(func=prepare)
run = sub.add_parser("run")
run.add_argument("--run-dir", type=Path, required=True)
run.add_argument("--executable", type=Path, required=True)
run.add_argument("--bios", type=Path, required=True)
run.add_argument("--port", type=int, default=4601)
run.add_argument("--timeout", type=float, default=30.0)
run.set_defaults(func=run_fixture)
return parser
def main() -> int:
try:
args = build_parser().parse_args()
return args.func(args)
except (OSError, RuntimeError, subprocess.CalledProcessError) as exc:
print(f"conformance: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
+28
View File
@@ -0,0 +1,28 @@
{
"schema_version": 1,
"source": {
"repository": "https://github.com/JaCzekanski/ps1-tests",
"license": "MIT",
"release": "build-158",
"release_commit": "5f73d61b27a8d51269b54dde1c456d2d2f7dc865",
"release_archive_sha256": "5DE0E82854C1A511697A7ADE381C2D778EC4B2FFF4A7D7AC79E8098C35EB859B"
},
"fixtures": {
"cpu-cop": {
"exe": "cpu/cop/cop.exe",
"exe_sha256": "D31D3CEC66ABE8DB7935B51590F206602346284DA2AC20EA8614339924EA9981",
"expected": "cpu/cop/psx.log",
"expected_sha256": "E3B09837989C4FD53E4D192AC869F437D965A4D71342BB80D038B5CC813C729C",
"expected_assertions": 17,
"class": "CPU exception and coprocessor access"
},
"gte-test-all": {
"exe": "gte/test-all/test-all.exe",
"exe_sha256": "479ADC925DA6CC4844B567A033FABC06B2AB934FFBBB79C5DAD698BE20944B9E",
"expected": "gte/test-all/psx.log",
"expected_sha256": "78DB534A7B02B4BC4C521A20A406743F296DADA95C5220568F2DF6E0AC37B7A0",
"expected_assertions": 1150,
"class": "GTE registers and valid opcodes"
}
}
}