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