Refresh netplay validation against master retaining audited startup cache and local views

This commit is contained in:
Matthew Stanley
2026-10-07 11:59:16 -07:00
30 changed files with 1910 additions and 85 deletions
+12
View File
@@ -1051,6 +1051,18 @@ remain user-owned and outside the bundled package directory.
Register via `mod_register_media_preparer` in mod_packages.h. Ship any worker
and its runtime dependencies with the title: players should only supply media.
Launcher preparation is synchronous by default. A title may set
`PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE=ON` only after auditing all its trusted
preparers and services for serialized worker execution without UI, SDL, GL,
or main-thread affinity. With a recomp-ui header advertising
`RECOMP_LAUNCHER_HAS_WORKER_MOD_COMMIT`, preboot offline PLAY then commits on an
owned worker while the launcher renders an exclusive progress view. The UI
does not access the provider until that worker has joined; close is queued
until completion. Verification, preparation, persistence, and failure gates
are unchanged. Older UI pins, other titles, netplay, and in-session commits
retain synchronous behavior. Plugin activation remains on the runtime's
existing post-commit path, not the preparation worker.
The `prepare_resources` method is separate from read-only `resolve`, so editing
launcher settings does not trigger conversions. Runtime activation still sees
only fully verified immutable resources.
+26 -5
View File
@@ -221,6 +221,15 @@ mod; `psx_memory.h`), scratchpad, I-cache tags, I_STAT/I_MASK, timers, DMA
and GPU registers (without the widescreen side effects of a savestate load), the
VRAM rect (hr colour, mask stencil, raw 16-bit mirror, native-wide band, CPU
VRAM rows), the renderer's coherency bookkeeping, and every clock value.
RAM-keyed GPU presentation provenance is restored too: sprite/HUD/background
tags, primitive roles, reveal-clear/repeat/mask proofs, and the automatic UI
prepass. A synthetic draw can rewrite and retag the same packet addresses;
restoring only their RAM would leave the canonical draw comparing its original
words against a synthetic proof. Packet freshness, the full-composite latch,
backdrop phase, and scene classification evidence return to their pre-pass
values. Diagnostic counters and rings still include synthetic activity.
A watchdog abort leaves by longjmp from inside guest code, skipping the
exits of the frames it leaves, so the host nesting those frames own is put
back from the checkpoint too: the cycle-deferral depth, the native overlay
@@ -294,13 +303,17 @@ first pass copies the rect out (`backups_reused` in `render_pass_stats`).
Passes cost host time inside the game's frame: they run on the emulation
thread. The budget per frame is `PSX_RENDER_PASS_BUDGET` percent (default 80)
of the presenter's idle time plus the presents beyond two per frame, learnt
from the previous frame, over a smoothed per-pass cost. When the budget runs
of the presenter's idle time plus the presents beyond two per frame and prior
replay time, learnt from the previous frame, over a smoothed per-pass cost.
Zero measured credit means zero budget, including startup and a frame after
replay was shed. It does not grant a fraction of the nominal frame: that would
repeatedly readmit expensive replay on an already overloaded host.
When the budget runs
out, fewer passes are rendered, and a pass that costs more than the budget is
not planned at all. The cost is measured per presented image size: until
three passes have been measured at the current size (the first plans, and
after an aspect or internal-resolution change) a plan asks for one pass, so
an expensive size costs at most one pass per frame while it is learnt. The
after an aspect or internal-resolution change) a positive-budget plan asks for
one pass, so an expensive size costs at most one pass per frame while it is learnt. The
three seed the average with their median, so one slow pass on a busy host
does not price passes out. A pass that creates pass textures or
framebuffers (the first frames at a size, or a slot filled for the first
@@ -310,7 +323,7 @@ busy host, code run for the first time) that costs several times the steady
state; an estimate that high prices every plan out, so no pass would ever
correct it. An estimate that no pass has been measured against for 30
plans that wanted passes (about a second of a 30 Hz game) is measured again:
the warm-up restarts, with one pass per plan (`cost_rewarms` in
the warm-up restarts, with one pass per positive-budget plan (`cost_rewarms` in
`render_pass_stats`). When the new median is not at least a quarter below
the old estimate, the old one was right (a size that is truly too
expensive, or a host at its limit), and the next wait doubles, up to 960
@@ -336,6 +349,14 @@ internal resolutions; a size change frees the old set.
source path and resource stage). FBO status and GL errors are numeric enums;
`gl_error_before` is distinct from errors produced during that allocation.
Success does not erase the record; a new mod session clears it.
`admission` records the last eligible planner call's guest `frame`, measured
`idle_ms`, `present_ms`, `prior_pass_ms`, two-present `present_reserve_ms`,
clamped `spare_ms`, nominal `frame_ms`, effective `cost_ms` (zero during a
warm-up), `budget_ms`, `wanted` and `planned`. Its `plans` and
`zero_credit_refusals` are lifetime counters; use deltas over an active window.
The latter counts wanted plans shed with zero budget. Backend refusal leaves
this snapshot unchanged, so compare its frame and count before interpreting
it as current evidence. Queries neither run a plan nor reset admission history.
- `render_pass_refuse on=1` (TCP) or `PSX_RENDER_PASS_REFUSE=1`: the backend
declines passes (`BACKEND`), to test a plugin's fallback.
- `render_pass_dump path=<dir> count=<n>`: PNGs of the next n frames' images
+30
View File
@@ -432,6 +432,35 @@ If an inspection need isn't covered by the existing commands, **do not fall back
The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` is absolute: **no `fprintf(stderr, …)` in source code, ever, for any reason**.
## `host_launch_timings` — retained host startup and mod-commit costs
`{"cmd":"host_launch_timings","count":256}` reads the newest completed host
scopes, oldest completion first. The fixed 256-entry ring is always on, including
before TCP starts, and reading it never drains or resets it. `total` is the
lifetime completion count; `overwritten` counts completions displaced by ring
wrap. `count` is clamped to 1..256. Only completed work is represented.
Each entry has `seq`, `scope_id`, `parent_id`, `stage`, `start_us`, `duration_us`,
`ok`, `package`, `feature`, and `labels_truncated`. Times use a monotonic host
clock, independent of guest frames/cycles. A parent includes its children:
**nested durations overlap and must not be summed**. Sequence orders completion;
scope IDs identify starts. Separate threads have independent parent chains.
Package/feature labels retain at most 63 bytes each; no owner-media paths are
recorded. `ok=0` indicates an unsuccessful or incomplete scope exit, including
exceptions; it is not a replacement for the existing launch error message.
`provider_commit` encloses the launcher's offline mod callback;
`provider_netplay_commit` identifies its netplay counterpart. `runtime_commit`
encloses the later main-thread commit/clear. Within `commit`, stages distinguish
`disc_hash` (absent when the existing digest is reused), `prepare_resources`
(with nested per-package/feature `media_provider`), `resolve`, `overlay_verify`,
`derived_disc`, `save_state`, and `build_disc_index`. `overlay_worker` records
the concurrent startup work; `overlay_join` measures only the later wait.
`plugin_activation` contains per-package/feature `plugin_callback` measurements,
including Resident Loading when enabled. No work is moved off-thread and no
verification or cache policy changes. These scopes do not cover every action
between Play and TCP availability.
<!-- BEGIN AUTOGENERATED COMMAND INDEX -- edit tools/gen_tcp_commands.py, not this block -->
## Complete command index (generated)
@@ -588,6 +617,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `guest_tty_dump` | ✓ | | |
| `history` | ✓ | ✓ | ✓ |
| `hle_dump` | ✓ | | ✓ |
| `host_launch_timings` | ✓ | | ✓ |
| `idle_skip` | ✓ | | |
| `imask_trace` | ✓ | | |
| `input_route_append` | ✓ | | |
+1 -1
View File
@@ -1 +1 @@
36e72e6875b4a8567d163199dff9ac9f840ef895a780db6ccd3126db4b8493a9
df844aebb7ff1a0e2bdb4b472c3d6a1586b0e0e2012c261b80480ee6e0a27b01
+1 -1
View File
@@ -1 +1 @@
c86327fcff32f4cabcb49c990da7e2b2b6af021d6de60877ec1a50f119b92dbc
4737d2f5a48b6185367edfc266b344793ba43c5a8305511fb0d4b4dc39f34603
+12
View File
@@ -1,10 +1,22 @@
#pragma once
#include <filesystem>
#include <string>
#include <system_error>
namespace PSXRecompV4 {
// generic_string() can collapse a MinGW UNC root from "//server" to
// "/server". Persist the native spelling, changing separators only on
// Windows; a backslash in a POSIX filename is an ordinary character.
inline std::string host_path_forward_slashes(const std::filesystem::path& path) {
std::string result = path.string();
#ifdef _WIN32
for (char& c : result) if (c == '\\') c = '/';
#endif
return result;
}
// 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.
+2 -3
View File
@@ -3036,10 +3036,9 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
std::ofstream f(path, std::ios::trunc);
if (!f.is_open()) return false;
// TOML strings use forward slashes so backslash escaping is never an issue.
// Windows paths use forward slashes so separators need no TOML escaping.
auto fwd = [](const fs::path& p) {
std::string str = p.generic_string();
return str;
return host_path_forward_slashes(p);
};
// Paths inside the game folder are stored relative to it, so a portable
// folder still finds its disc, BIOS and memory cards after it is moved or
@@ -11,6 +11,7 @@
* anchoring resolves it on the new PC.
*/
#include "config_loader.h"
#include "host_path.h"
#include <cstdio>
#include <filesystem>
@@ -71,6 +72,33 @@ int main() {
check(back.has_memcard1_path && back.memcard1_path == fs::path("saves/card1.mcd"),
"relative card path reads back unchanged");
#ifdef _WIN32
// A NAS selection must survive a launcher save and reopen. No network
// asset is opened: only the task-local settings file is written/read.
const fs::path expected("//server/share/Game files/disc.chd");
for (const char* spelling : {
"//server/share/Game files/disc.chd",
R"(\\server\share\Game files\disc.chd)"}) {
PSXRecompV4::UserSettings network{};
network.disc_path = spelling; network.has_disc_path = true;
network.bios_path = spelling; network.has_bios_path = true;
network.memcard1_path = spelling; network.has_memcard1_path = true;
check(PSXRecompV4::save_user_settings(settings, network), "UNC settings save");
const auto restored = PSXRecompV4::load_user_settings(settings);
check(restored.has_disc_path &&
restored.disc_path.native() == expected.native(),
"saved UNC disc keeps both leading separators");
check(restored.has_bios_path &&
restored.bios_path.native() == expected.native(),
"saved UNC BIOS keeps both leading separators");
check(restored.has_memcard1_path &&
restored.memcard1_path.native() == expected.native(),
"saved UNC memory card keeps both leading separators");
check(PSXRecompV4::host_resolve(game, restored.disc_path).native() == expected.native(),
"reopened UNC disc never becomes a path on the current drive");
}
#endif
fs::remove_all(game.parent_path(), ec);
if (failures) return 1;
std::printf("portable_settings_paths_test: all checks passed\n");
+34 -1
View File
@@ -23,6 +23,12 @@ psxrecomp_add_runtime_target(psx-runtime
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_pgxp the same way.
if(BUILD_TESTING)
find_package(Threads REQUIRED)
add_executable(host_launch_timing_test
tests/test_host_launch_timing.cpp tests/host_launch_timing_helper.cpp)
target_include_directories(host_launch_timing_test PRIVATE include)
target_link_libraries(host_launch_timing_test PRIVATE Threads::Threads)
add_test(NAME host_launch_timing_test COMMAND host_launch_timing_test)
add_executable(disasm_test tests/test_disasm.cpp src/disasm_shim.cpp
../recompiler/src/mips_decoder.cpp)
target_include_directories(disasm_test PRIVATE include ../recompiler/include)
@@ -136,6 +142,12 @@ if(BUILD_TESTING)
set(PSX_GL_READBACK_COMPILER_BIN "${_gl_test_default_bin}" CACHE PATH "GCC/MinGW bin for GL fixture")
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME host_launch_timings_protocol_test COMMAND ${CMAKE_COMMAND} -E env
"CC=${CMAKE_C_COMPILER}" "CXX=${CMAKE_CXX_COMPILER}" "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_host_launch_timings_protocol.py")
set_tests_properties(host_launch_timings_protocol_test PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME tcp_command_index_check COMMAND "${Python3_EXECUTABLE}"
"${PSXRECOMP_ROOT}/tools/gen_tcp_commands.py" --check)
add_test(NAME wtrace_dump_test COMMAND ${CMAKE_COMMAND} -E env
"CC=${CMAKE_C_COMPILER}" "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_wtrace_dump.py")
@@ -689,7 +701,12 @@ if(BUILD_TESTING)
add_executable(native_wide_projection_test tests/test_native_wide_projection.c
src/psx_video_timing.c
src/gpu_timeline.c)
foreach(_anchor_test ws_view_anchor ws_view_anchor_gpu native_wide_projection)
add_executable(gpu_render_pass_provenance_test tests/test_gpu_render_pass_provenance.c
src/ws_ui_group.c
src/psx_video_timing.c
src/gpu_timeline.c)
foreach(_anchor_test ws_view_anchor ws_view_anchor_gpu native_wide_projection
gpu_render_pass_provenance)
target_include_directories(${_anchor_test}_test PRIVATE include src)
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${_anchor_test}_test PRIVATE -ffunction-sections -fdata-sections)
@@ -704,6 +721,7 @@ if(BUILD_TESTING)
add_test(NAME ws_view_anchor_test COMMAND ws_view_anchor_test)
add_test(NAME ws_view_anchor_gpu_test COMMAND ws_view_anchor_gpu_test)
add_test(NAME native_wide_projection_test COMMAND native_wide_projection_test)
add_test(NAME gpu_render_pass_provenance_test COMMAND gpu_render_pass_provenance_test)
add_executable(ws_repeat_rect_test tests/test_ws_repeat_rect.c)
target_include_directories(ws_repeat_rect_test PRIVATE include)
@@ -970,6 +988,21 @@ if(BUILD_TESTING)
endif()
endif()
add_test(NAME netplay_view_mods_test COMMAND netplay_view_mods_test)
get_target_property(mod_runtime_sources mod_runtime_test SOURCES)
add_executable(mod_runtime_cache_test ${mod_runtime_sources})
target_include_directories(mod_runtime_cache_test PRIVATE
include ../recompiler/include ../recompiler/lib/toml11)
target_compile_definitions(mod_runtime_cache_test PRIVATE
PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE=1)
target_link_libraries(mod_runtime_cache_test PRIVATE chdr-static)
if(WIN32)
if(MSVC)
target_link_options(mod_runtime_cache_test PRIVATE /STACK:16777216)
else()
target_link_options(mod_runtime_cache_test PRIVATE -Wl,--stack,16777216)
endif()
endif()
add_test(NAME mod_runtime_cache_test COMMAND mod_runtime_cache_test)
add_executable(mod_resident_test
tests/test_mod_resident.cpp
+11
View File
@@ -99,6 +99,17 @@ void gl_renderer_pass_service_presents(void);
* presents past the frame's planned end (the next flip was late; the newest
* image held), stale pass-cost estimates measured again. */
void gl_renderer_pass_diag(uint64_t out[10]);
/* Always-on snapshot of the last eligible planner call and lifetime count of
* wanted plans refused with zero measured credit. Times are host milliseconds;
* reads do not reset counters or invoke the planner. Backend refusals leave
* this snapshot unchanged. */
typedef struct GLRenderPassPlanDiag {
uint64_t plans, frame, zero_credit_refusals;
double idle_ms, present_ms, prior_pass_ms, present_reserve_ms, spare_ms;
double frame_ms, cost_ms, budget_ms;
uint32_t wanted, planned;
} GLRenderPassPlanDiag;
void gl_renderer_pass_plan_diag(GLRenderPassPlanDiag *out);
/* Out-of-rect VRAM writes journaled and rolled back (lifetime count). */
uint64_t gl_renderer_pass_journaled(void);
/* Passes that reused the previous pass's VRAM backup (lifetime count). */
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include <filesystem>
#include <string>
#include <sstream>
#include <cwchar>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <sys/stat.h>
#endif
namespace PSXRecompV4 {
// A failed identity is never a cache hit. Change time protects restored mtimes.
inline bool host_file_identity(const std::filesystem::path& path, std::string& identity) {
identity.clear();
std::ostringstream out;
#if defined(_WIN32)
HANDLE file = CreateFileW(path.c_str(), FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
if (file == INVALID_HANDLE_VALUE) return false;
struct HandleScope { HANDLE value; ~HandleScope() { CloseHandle(value); } } handle{file};
WCHAR filesystem_name[32]{};
// NTFS/ReFS expose change time and native file identity. For a remote
// handle also require SMB3+, rather than assuming every NAS is unsupported.
FILE_REMOTE_PROTOCOL_INFO remote{};
const bool is_remote = GetFileInformationByHandleEx(file, FileRemoteProtocolInfo,
&remote, sizeof remote) != FALSE;
const DWORD protocol_error = is_remote ? ERROR_SUCCESS : GetLastError();
bool local_handle = false;
if (!is_remote) {
// A failed protocol query proves nothing about locality. Resolve the
// handle itself: mapped drives become UNC paths here and cannot take
// the local fallback when a remote provider omits protocol metadata.
WCHAR final_path[32768]{};
const DWORD length = GetFinalPathNameByHandleW(file, final_path, 32768, FILE_NAME_NORMALIZED);
if (length >= 7 && length < 32768 && final_path[0] == L'\\' &&
final_path[1] == L'\\' && final_path[2] == L'?' && final_path[3] == L'\\' &&
final_path[5] == L':' && final_path[6] == L'\\') {
WCHAR drive_root[] = {final_path[4], L':', L'\\', L'\0'};
const UINT drive_type = GetDriveTypeW(drive_root);
local_handle = drive_type == DRIVE_FIXED || drive_type == DRIVE_REMOVABLE;
}
}
const bool reliable_protocol = is_remote
? remote.Protocol == 0x00020000 && remote.ProtocolMajorVersion >= 3 // WNNC_NET_LANMAN
: local_handle && (protocol_error == ERROR_INVALID_PARAMETER || protocol_error == ERROR_NOT_SUPPORTED);
const bool reliable_fs = reliable_protocol &&
GetVolumeInformationByHandleW(file, nullptr, 0, nullptr, nullptr, nullptr,
filesystem_name, 32) &&
(wcscmp(filesystem_name, L"NTFS") == 0 || wcscmp(filesystem_name, L"ReFS") == 0);
BY_HANDLE_FILE_INFORMATION info{};
FILE_BASIC_INFO basic{};
const bool ok = reliable_fs && GetFileInformationByHandle(file, &info) &&
GetFileInformationByHandleEx(file, FileBasicInfo, &basic, sizeof basic) &&
!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
(info.nFileIndexHigh != 0 || info.nFileIndexLow != 0) &&
info.dwVolumeSerialNumber != 0 && basic.ChangeTime.QuadPart != 0 &&
basic.LastWriteTime.QuadPart != 0;
if (!ok) return false;
out << "win1:" << info.dwVolumeSerialNumber << ':' << info.nFileIndexHigh << ':'
<< info.nFileIndexLow << ':' << info.nFileSizeHigh << ':' << info.nFileSizeLow
<< ':' << basic.LastWriteTime.QuadPart << ':' << basic.ChangeTime.QuadPart;
#else
struct stat info{};
if (stat(path.c_str(), &info) || !S_ISREG(info.st_mode) || info.st_ino == 0) return false;
out << "posix1:" << info.st_dev << ':' << info.st_ino << ':' << info.st_size;
#if defined(__APPLE__)
out << ':' << info.st_mtimespec.tv_sec << ':' << info.st_mtimespec.tv_nsec
<< ':' << info.st_ctimespec.tv_sec << ':' << info.st_ctimespec.tv_nsec;
#else
out << ':' << info.st_mtim.tv_sec << ':' << info.st_mtim.tv_nsec
<< ':' << info.st_ctim.tv_sec << ':' << info.st_ctim.tv_nsec;
#endif
#endif
identity = out.str();
return true;
}
}
+145
View File
@@ -0,0 +1,145 @@
#pragma once
#include <stdint.h>
/* Always-on host diagnostics. Completed scopes survive startup for the debug
* TCP reader. Nested scopes overlap: do not sum a parent and its children.
* No guest clocks, filesystem paths, logging, or environment switches. */
#define HOST_LAUNCH_TIMING_CAPACITY 256u
#define HOST_LAUNCH_TIMING_LABEL 64u
typedef enum HostLaunchStage {
HOST_LAUNCH_PROVIDER_COMMIT,
HOST_LAUNCH_RUNTIME_COMMIT,
HOST_LAUNCH_COMMIT,
HOST_LAUNCH_DISC_HASH,
HOST_LAUNCH_PREPARE_RESOURCES,
HOST_LAUNCH_MEDIA_PROVIDER,
HOST_LAUNCH_RESOLVE,
HOST_LAUNCH_OVERLAY_VERIFY,
HOST_LAUNCH_DERIVED_DISC,
HOST_LAUNCH_SAVE_STATE,
HOST_LAUNCH_BUILD_DISC_INDEX,
HOST_LAUNCH_OVERLAY_WORKER,
HOST_LAUNCH_OVERLAY_JOIN,
HOST_LAUNCH_PLUGIN_ACTIVATION,
HOST_LAUNCH_PLUGIN_CALLBACK,
HOST_LAUNCH_PROVIDER_NETPLAY_COMMIT
} HostLaunchStage;
typedef struct HostLaunchTimingEvent {
uint64_t seq; /* completion order, starting at 1 */
uint64_t scope_id; /* unique start identity, independent of seq */
uint64_t parent_id; /* same-thread enclosing scope, or 0 */
uint64_t start_us; /* steady-clock epoch, not guest time */
uint64_t duration_us;
uint32_t stage;
uint32_t ok; /* explicit success; early return/throw = 0 */
uint32_t labels_truncated;
char package_id[HOST_LAUNCH_TIMING_LABEL];
char feature_id[HOST_LAUNCH_TIMING_LABEL];
} HostLaunchTimingEvent;
#ifdef __cplusplus
extern "C" {
#endif
/* Newest min(cap, retained) completions, oldest first. Null/zero output is a
* metadata query. total is lifetime completed count; overwritten excludes
* additional entries omitted by a reader's smaller cap. Never drains/reset. */
uint32_t host_launch_timing_snapshot(HostLaunchTimingEvent* out, uint32_t cap,
uint64_t* total, uint64_t* overwritten);
const char* host_launch_timing_stage_name(uint32_t stage);
#ifdef __cplusplus
}
#include <algorithm>
#include <array>
#include <chrono>
#include <cstring>
#include <mutex>
namespace PSXRecompV4 {
/* Header-only to keep package-manager fixtures independent of mod runtime.
* The external-linkage inline accessors' local statics have one instance
* across C++ translation units, not one ring per includer. */
class HostLaunchTimingRing {
std::mutex mutex_;
std::array<HostLaunchTimingEvent, HOST_LAUNCH_TIMING_CAPACITY> events_{};
uint64_t started_ = 0, completed_ = 0;
public:
uint64_t begin() {
std::lock_guard<std::mutex> lock(mutex_);
return ++started_;
}
void finish(HostLaunchTimingEvent event) {
std::lock_guard<std::mutex> lock(mutex_);
event.seq = ++completed_;
events_[(event.seq - 1) % events_.size()] = event;
}
uint32_t snapshot(HostLaunchTimingEvent* out, uint32_t cap,
uint64_t* total, uint64_t* overwritten) {
std::lock_guard<std::mutex> lock(mutex_);
if (total) *total = completed_;
if (overwritten) *overwritten = completed_ > events_.size()
? completed_ - events_.size() : 0;
if (!out) return 0;
const uint32_t n = static_cast<uint32_t>(std::min<uint64_t>(
std::min<uint64_t>(cap, events_.size()), completed_));
for (uint32_t i = 0; i < n; ++i) {
const uint64_t seq = completed_ - n + i + 1;
out[i] = events_[(seq - 1) % events_.size()];
}
return n;
}
};
inline HostLaunchTimingRing& host_launch_timing_ring() {
static HostLaunchTimingRing ring;
return ring;
}
struct HostLaunchTimingParent { HostLaunchTimingRing* ring; uint64_t id; };
inline HostLaunchTimingParent& host_launch_timing_parent() {
static thread_local HostLaunchTimingParent parent{};
return parent;
}
inline uint64_t host_launch_now_us() {
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
}
class HostLaunchTimingScope {
HostLaunchTimingRing& ring_;
HostLaunchTimingEvent event_{};
HostLaunchTimingParent previous_;
void label(char* out, const char* text) {
if (!text) return;
const auto size = std::strlen(text);
const auto n = std::min<size_t>(size, HOST_LAUNCH_TIMING_LABEL - 1u);
std::memcpy(out, text, n);
event_.labels_truncated |= size != n;
}
public:
explicit HostLaunchTimingScope(HostLaunchStage stage,
const char* package = nullptr, const char* feature = nullptr,
HostLaunchTimingRing& ring = host_launch_timing_ring())
: ring_(ring), previous_(host_launch_timing_parent()) {
event_.scope_id = ring_.begin();
event_.parent_id = previous_.ring == &ring_ ? previous_.id : 0;
event_.stage = stage;
label(event_.package_id, package);
label(event_.feature_id, feature);
event_.start_us = host_launch_now_us();
host_launch_timing_parent() = {&ring_, event_.scope_id};
}
HostLaunchTimingScope(const HostLaunchTimingScope&) = delete;
HostLaunchTimingScope& operator=(const HostLaunchTimingScope&) = delete;
~HostLaunchTimingScope() {
event_.duration_us = host_launch_now_us() - event_.start_us;
host_launch_timing_parent() = previous_;
ring_.finish(event_);
}
void success() { event_.ok = 1; }
};
} // namespace PSXRecompV4
#endif
+9 -2
View File
@@ -318,7 +318,14 @@ struct ModPrepareContext {
using ModMediaPreparer = std::function<bool(const ModPrepareContext&,
std::map<std::string, std::filesystem::path>&, std::string&)>;
// Implementations are linked trusted code. A manifest cannot execute a command.
bool mod_register_media_preparer(const std::string& id, ModMediaPreparer callback);
// Normally called synchronously during launch preparation. A title enabling
// PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE also permits preboot offline preparation
// on a launcher-owned worker. Every registered callback/service used by that
// title must then support serialized worker calls without SDL/UI/GL or
// main-thread affinity. Registration must finish before opening the launcher;
// activation callbacks still run separately, before renderer initialization.
bool mod_register_media_preparer(const std::string& id, ModMediaPreparer callback,
ModMediaPreparer cache_probe = {});
struct ModFeatureSelection {
bool enabled = false;
@@ -453,7 +460,7 @@ public:
bool prepare_resources(const std::string& game_id,
const std::filesystem::path& disc_path,
const std::filesystem::path& cache_root,
std::string* error = nullptr);
std::string* error = nullptr, bool cached_only = false);
bool install_archive(const std::filesystem::path& archive,
std::string* installed_id = nullptr,
+8
View File
@@ -21,6 +21,14 @@ bool mod_runtime_initialize(const std::filesystem::path& root,
bool mod_runtime_commit(const std::filesystem::path& disc_path = {},
std::string* error = nullptr,
bool save_selection = true);
/* Prepare a preboot launcher selection; the audited runtime retains a ticket. */
bool mod_runtime_prepare_for_launcher(const std::filesystem::path& disc_path,
std::string* error = nullptr);
/* Audited preboot only: load and verify a plan using existing receipts.
* A miss never invokes a media converter or computes a disc digest. */
bool mod_runtime_try_prepare_cached(const std::filesystem::path& disc_path);
/* Prepare enabled media without installing a guest plan or saving choices. */
bool mod_runtime_prepare_resources(const std::filesystem::path& disc_path,
std::string* error = nullptr);
+2 -1
View File
@@ -128,7 +128,8 @@ int render_pass_cost_note_plan(RenderPassCost *cost);
/* Next frame's pass budget from the last frame: the host time the presenter
* spent idle-waiting plus the time passes used, scaled by `share` (0..1) and
* clamped to [0, frame_length]. No history (both zero) -> share of the frame. */
* clamped to [0, frame_length]. Both zero means no measured credit, including
* startup: an unknown cost warms up only when positive credit is available. */
double render_pass_budget(double idle_ticks, double pass_ticks,
double frame_length, double share);
+8
View File
@@ -82,6 +82,11 @@ else()
option(PSX_DEBUG_TOOLS "Build with TCP debug server + heartbeat + per-block recording" ON)
endif()
# A title may opt in only after auditing every trusted media preparer used by
# its mod commit. Older recomp-ui pins ignore this capability and remain sync.
option(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE
"Title certifies preboot mod commit/preparers have no UI or thread affinity" OFF)
# PSX_STATIC_RUNTIME: produce a 100% self-contained MinGW exe.
#
# A default MinGW build dynamically imports three NON-system DLLs —
@@ -2085,6 +2090,9 @@ function(psxrecomp_add_runtime_target target)
if(PSX_MOD_DEVELOPER_CHANNEL)
target_compile_definitions(${target} PRIVATE PSX_MOD_DEVELOPER_CHANNEL=1)
endif()
if(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
target_compile_definitions(${target} PRIVATE PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE=1)
endif()
if(has_game_dispatch)
target_compile_definitions(${target} PRIVATE PSX_HAS_GAME_DISPATCH=1)
endif()
+65 -2
View File
@@ -17,6 +17,7 @@
#endif
#include <time.h>
#include "debug_server.h"
#include "host_launch_timing.h"
#include "psx_video_timing.h"
#include "psx_netplay.h"
#include "psx_bss.h"
@@ -8222,6 +8223,53 @@ static void handle_netplay_status(int id, const char *json)
debug_server_send_line(out);
}
/* Startup host work recorded before TCP was initialized; always-on, read-only.
* Completion order is not start order. Parents include their children. */
static void handle_host_launch_timings(int id, const char *json)
{
int requested = json_get_int(json, "count", HOST_LAUNCH_TIMING_CAPACITY);
if (requested < 1) requested = 1;
if (requested > (int)HOST_LAUNCH_TIMING_CAPACITY)
requested = HOST_LAUNCH_TIMING_CAPACITY;
HostLaunchTimingEvent *events = (HostLaunchTimingEvent *)malloc(
(size_t)requested * sizeof(*events));
if (!events) { send_err(id, "alloc failed"); return; }
uint64_t total = 0, overwritten = 0;
uint32_t n = host_launch_timing_snapshot(events, (uint32_t)requested,
&total, &overwritten);
/* Two labels may each expand sixfold when JSON-escaped. */
size_t cap = 512u + (size_t)n * 1536u;
char *buf = (char *)malloc(cap);
if (!buf) { free(events); send_err(id, "alloc failed"); return; }
size_t off = (size_t)snprintf(buf, cap,
"{\"id\":%d,\"ok\":true,\"clock\":\"steady_us\","
"\"nested_durations_overlap\":true,\"capacity\":%u,"
"\"total\":%llu,\"overwritten\":%llu,\"entries\":[",
id, HOST_LAUNCH_TIMING_CAPACITY, (unsigned long long)total,
(unsigned long long)overwritten);
for (uint32_t i = 0; i < n; ++i) {
const HostLaunchTimingEvent *e = &events[i];
char package[HOST_LAUNCH_TIMING_LABEL * 6u + 1u];
char feature[HOST_LAUNCH_TIMING_LABEL * 6u + 1u];
json_escape_string(package, sizeof(package), e->package_id);
json_escape_string(feature, sizeof(feature), e->feature_id);
off += (size_t)snprintf(buf + off, cap - off,
"%s{\"seq\":%llu,\"scope_id\":%llu,\"parent_id\":%llu,"
"\"stage\":\"%s\",\"start_us\":%llu,\"duration_us\":%llu,"
"\"ok\":%u,\"package\":\"%s\",\"feature\":\"%s\","
"\"labels_truncated\":%u}",
i ? "," : "", (unsigned long long)e->seq,
(unsigned long long)e->scope_id, (unsigned long long)e->parent_id,
host_launch_timing_stage_name(e->stage),
(unsigned long long)e->start_us, (unsigned long long)e->duration_us,
e->ok, package, feature, e->labels_truncated);
}
snprintf(buf + off, cap - off, "]}");
debug_server_send_line(buf);
free(buf);
free(events);
}
static void handle_mod_counters(int id, const char *json)
{
extern int psx_mod_counters_snapshot(const char **names, uint64_t *counts,
@@ -8504,9 +8552,11 @@ static void handle_render_pass_stats(int id, const char *json)
{
(void)json;
RenderPassStats st;
GLRenderPassPlanDiag admission;
uint64_t gd[10], image_bytes = 0;
uint32_t image_textures;
char failure_json[2048];
char admission_json[768];
char abort_detail_json[sizeof st.last_abort_detail * 6 + 1];
DirtyRamSpanFailure sf;
render_pass_get_stats(&st);
@@ -8540,6 +8590,18 @@ static void handle_render_pass_stats(int id, const char *json)
b->fbo_status, b->gl_error_before, b->gl_error);
}
gl_renderer_pass_diag(gd);
gl_renderer_pass_plan_diag(&admission);
snprintf(admission_json, sizeof admission_json,
"{\"plans\":%llu,\"frame\":%llu,\"zero_credit_refusals\":%llu,"
"\"idle_ms\":%.3f,\"present_ms\":%.3f,\"prior_pass_ms\":%.3f,"
"\"present_reserve_ms\":%.3f,\"spare_ms\":%.3f,\"frame_ms\":%.3f,"
"\"cost_ms\":%.3f,\"budget_ms\":%.3f,\"wanted\":%u,\"planned\":%u}",
(unsigned long long)admission.plans,
(unsigned long long)admission.frame,
(unsigned long long)admission.zero_credit_refusals,
admission.idle_ms, admission.present_ms, admission.prior_pass_ms,
admission.present_reserve_ms, admission.spare_ms, admission.frame_ms,
admission.cost_ms, admission.budget_ms, admission.wanted, admission.planned);
image_textures = gl_renderer_pass_image_textures(&image_bytes);
send_fmt("{\"id\":%d,\"ok\":true,\"plans\":%llu,\"planned\":%llu,"
"\"wanted\":%llu,\"refused\":%llu,\"passes\":%llu,"
@@ -8564,7 +8626,7 @@ static void handle_render_pass_stats(int id, const char *json)
"\"spans\":%llu,\"span_failures\":%llu,"
"\"local_views\":%llu,\"local_attempts\":%llu,"
"\"local_status\":%u,"
"\"last_failure\":%s,\"last_abort_detail\":\"%s\","
"\"admission\":%s,\"last_failure\":%s,\"last_abort_detail\":\"%s\","
"\"span_fail\":{\"reason\":%u,\"pc\":\"0x%08X\",\"start\":\"0x%08X\","
"\"stop\":\"0x%08X\",\"ra\":\"0x%08X\",\"after\":\"0x%08X\","
"\"insns\":%llu}}",
@@ -8601,7 +8663,7 @@ static void handle_render_pass_stats(int id, const char *json)
(unsigned long long)st.local_views,
(unsigned long long)st.local_attempts,
psx_mod_render_local_view_status(),
failure_json, abort_detail_json,
admission_json, failure_json, abort_detail_json,
(unsigned)sf.reason, (unsigned)sf.pc, (unsigned)sf.start_pc,
(unsigned)sf.stop_pc, (unsigned)sf.ra, (unsigned)sf.after,
(unsigned long long)sf.insns);
@@ -15324,6 +15386,7 @@ static const CmdEntry s_commands[] = {
{ "ws_hud_mode", handle_ws_hud_mode },
{ "ws_tag_stats", handle_ws_tag_stats },
{ "mod_counters", handle_mod_counters },
{ "host_launch_timings", handle_host_launch_timings },
{ "resident_status", handle_resident_status },
{ "netplay_status", handle_netplay_status },
{ "resident_events", handle_resident_events },
+110 -3
View File
@@ -7476,10 +7476,110 @@ int gpu_snapshot_read(const uint8_t *p, uint32_t len) {
}
/* ---- Render-pass GPU checkpoint (render_pass.c) ---------------------------
* The same register set as a savestate (gpu_snap_emit), restored WITHOUT
* gpu_snapshot_read's side effects (widescreen scene history and HUD anchor
* tags belong to the live frame, which a pass must not reset). After the
* restore the renderer's mirrored draw state (area, offset, texture window,
* gpu_snapshot_read's side effects. Packet provenance belongs to the RAM it
* describes: a synthetic builder can rewrite and retag a canonical packet,
* so rolling its RAM back must also restore its original tags, not clear them.
* The renderer's mirrored draw state (area, offset, texture window,
* mask bits, native-wide target) is re-synced exactly as GP0(E2..E6) would. */
typedef struct {
WsTag ws_tags[WS_TAG_BUCKETS];
WsHudAnchorTag ws_hud_anchor_tags[WS_HUD_ANCHOR_TABLE_SIZE];
WsHudAnchorTag ws_background_tags[WS_HUD_ANCHOR_TABLE_SIZE];
WsHudAnchorTag ws_reveal_clear_tags[WS_HUD_ANCHOR_TABLE_SIZE];
WsHudAnchorTag ws_screen_mask_tags[WS_HUD_ANCHOR_TABLE_SIZE];
WsRadialScreenMaskTag ws_radial_screen_mask_tags[WS_RADIAL_MASK_TAG_COUNT];
WsRepeatRectTag ws_repeat_rect_tags[WS_REPEAT_RECT_TAG_TABLE_SIZE];
WsPrimitiveRole ws_primitive_roles[WS_ROLE_BUCKETS];
WsUiPrepassItem ws_ui_prepass[WS_UI_PREPASS_MAX];
WsUiPrepassNode ws_ui_prepass_nodes[WS_UI_PREPASS_NODE_MAX];
uint32_t ws_ui_prepass_count, ws_ui_prepass_node_count;
uint16_t ws_ui_prepass_rank;
int ws_auto_ui_dense, ws_shared_ui_copy;
uint32_t ws_background_tag_frame, ws_last_tag_stamp;
int ws_background_tags_used;
uint32_t s_bg_phase_frame;
int s_bg_phase_over;
/* Synthetic GTE/GP0 draws update these signals, and native-wide target
* selection reads them even though no guest frame has elapsed. */
uint32_t ws_gte_frame, ws_gte_count, ws_gte_prev_verts;
uint32_t ws_last_gte_stamp, ws_last_world3d_stamp, ws_sust_world3d_stamp;
uint32_t ws_ovh_frame, ws_ovh_count, ws_ovh_prev;
uint32_t ws_last_ovh_stamp, ws_sust_ovh_stamp;
WsSceneLatch ws_scene_latch;
WsSceneHold s_ws_scene_hold;
uint32_t s_ws_fmv_frame_cache;
int s_ws_fmv_cached;
} GpuPassPresentationState;
static GpuPassPresentationState s_pass_presentation;
/* One transfer list keeps save and restore symmetric. The prepass arrays are
* read only through their counts, so copy just their populated prefixes.
* Counters/rings remain cumulative; configuration and callbacks are unchanged. */
static void gpu_pass_provenance_transfer(int restore) {
#define PASS_ARRAY(name) do { \
if (restore) memcpy(name, s_pass_presentation.name, sizeof(name)); \
else memcpy(s_pass_presentation.name, name, sizeof(name)); \
} while (0)
#define PASS_VALUE(name) do { \
if (restore) name = s_pass_presentation.name; \
else s_pass_presentation.name = name; \
} while (0)
PASS_ARRAY(ws_tags);
PASS_ARRAY(ws_hud_anchor_tags);
PASS_ARRAY(ws_background_tags);
PASS_ARRAY(ws_reveal_clear_tags);
PASS_ARRAY(ws_screen_mask_tags);
PASS_ARRAY(ws_radial_screen_mask_tags);
PASS_ARRAY(ws_repeat_rect_tags);
PASS_ARRAY(ws_primitive_roles);
PASS_VALUE(ws_ui_prepass_count);
PASS_VALUE(ws_ui_prepass_node_count);
PASS_VALUE(ws_ui_prepass_rank);
PASS_VALUE(ws_auto_ui_dense);
PASS_VALUE(ws_shared_ui_copy);
PASS_VALUE(ws_background_tag_frame);
PASS_VALUE(ws_background_tags_used);
PASS_VALUE(ws_last_tag_stamp);
PASS_VALUE(s_bg_phase_frame);
PASS_VALUE(s_bg_phase_over);
if (restore) {
memcpy(ws_ui_prepass, s_pass_presentation.ws_ui_prepass,
ws_ui_prepass_count * sizeof(ws_ui_prepass[0]));
memcpy(ws_ui_prepass_nodes, s_pass_presentation.ws_ui_prepass_nodes,
ws_ui_prepass_node_count * sizeof(ws_ui_prepass_nodes[0]));
} else {
memcpy(s_pass_presentation.ws_ui_prepass, ws_ui_prepass,
ws_ui_prepass_count * sizeof(ws_ui_prepass[0]));
memcpy(s_pass_presentation.ws_ui_prepass_nodes, ws_ui_prepass_nodes,
ws_ui_prepass_node_count * sizeof(ws_ui_prepass_nodes[0]));
}
#undef PASS_ARRAY
#undef PASS_VALUE
}
static void gpu_pass_scene_transfer(int restore) {
#define PASS_VALUE(name) do { \
if (restore) name = s_pass_presentation.name; \
else s_pass_presentation.name = name; \
} while (0)
PASS_VALUE(ws_gte_frame);
PASS_VALUE(ws_gte_count);
PASS_VALUE(ws_gte_prev_verts);
PASS_VALUE(ws_last_gte_stamp);
PASS_VALUE(ws_last_world3d_stamp);
PASS_VALUE(ws_sust_world3d_stamp);
PASS_VALUE(ws_ovh_frame);
PASS_VALUE(ws_ovh_count);
PASS_VALUE(ws_ovh_prev);
PASS_VALUE(ws_last_ovh_stamp);
PASS_VALUE(ws_sust_ovh_stamp);
PASS_VALUE(ws_scene_latch);
PASS_VALUE(s_ws_scene_hold);
PASS_VALUE(s_ws_fmv_frame_cache);
PASS_VALUE(s_ws_fmv_cached);
#undef PASS_VALUE
}
static uint8_t s_pass_regs[512];
static uint32_t s_pass_regs_len;
static uint32_t s_pass_poll_count;
@@ -7499,6 +7599,8 @@ int gpu_pass_checkpoint_save(void) {
s_pass_doff_max = g_doff_max_this;
s_pass_doff_cnt = g_doff_cnt_this;
s_pass_split_this = split_trace_this;
gpu_pass_provenance_transfer(0);
gpu_pass_scene_transfer(0);
return 1;
}
@@ -7512,12 +7614,17 @@ void gpu_pass_checkpoint_restore(void) {
g_doff_max_this = s_pass_doff_max;
g_doff_cnt_this = s_pass_doff_cnt;
split_trace_this = s_pass_split_this;
gpu_pass_provenance_transfer(1);
gpu_pass_scene_transfer(1);
gr_set_texture_window(texture_window_value);
gr_set_draw_area((int)draw_area_left, (int)draw_area_top,
(int)draw_area_right, (int)draw_area_bottom);
gr_set_draw_offset(draw_offset_x, draw_offset_y);
gr_set_mask_bits((int)set_mask_bit, (int)check_mask_bit);
ws_nw_sync_target();
/* Target sync may classify the scene before render_pass.c restores RAM.
* Do not retain history learned from the synthetic draw or that query. */
gpu_pass_scene_transfer(1);
s_pass_regs_len = 0;
}
+27 -3
View File
@@ -6342,6 +6342,7 @@ static uint64_t s_present_ticks_accum = 0, s_present_ticks_last = 0;
static double s_present_cost_ema = 0.0; /* host ticks per present */
static uint32_t s_intervals_since_plan = 0;
static int s_pass_budget_pct = -1;
static GLRenderPassPlanDiag s_pass_plan_diag;
static int s_pass_verify = -1;
static uint8_t *s_pv_hr = NULL, *s_pv_raw = NULL;
@@ -6431,7 +6432,7 @@ uint32_t gl_renderer_pass_plan(uint32_t period_vblanks,
uint32_t *alpha_q16, uint32_t max,
uint32_t *wanted) {
RenderPassPlanInput in;
double freq, sp;
double freq, sp, spare;
uint32_t cap;
int live;
@@ -6467,8 +6468,8 @@ uint32_t gl_renderer_pass_plan(uint32_t period_vblanks,
{
/* Presents beyond two per frame are traded for passes: a shed
* present is coalesced, a shed pass is a missing motion sample. */
double spare = (double)s_idle_ticks_last + (double)s_present_ticks_last -
2.0 * s_present_cost_ema;
spare = (double)s_idle_ticks_last + (double)s_present_ticks_last -
2.0 * s_present_cost_ema;
if (spare < 0.0) spare = 0.0;
in.budget = render_pass_budget(spare, (double)s_pass_ticks_last,
in.frame_length,
@@ -6486,10 +6487,33 @@ uint32_t gl_renderer_pass_plan(uint32_t period_vblanks,
in.pass_cost = 0.0;
n = render_pass_plan_phases(&in, alpha_q16, NULL);
}
/* Record the actual input used, including a cost rewarm, without
* making a debug query drive admission or reset its history. */
{
double ms = 1000.0 / freq;
s_pass_plan_diag.plans++;
s_pass_plan_diag.frame = s_frame_count;
s_pass_plan_diag.idle_ms = (double)s_idle_ticks_last * ms;
s_pass_plan_diag.present_ms = (double)s_present_ticks_last * ms;
s_pass_plan_diag.prior_pass_ms = (double)s_pass_ticks_last * ms;
s_pass_plan_diag.present_reserve_ms = 2.0 * s_present_cost_ema * ms;
s_pass_plan_diag.spare_ms = spare * ms;
s_pass_plan_diag.frame_ms = in.frame_length * ms;
s_pass_plan_diag.cost_ms = in.pass_cost * ms;
s_pass_plan_diag.budget_ms = in.budget * ms;
s_pass_plan_diag.wanted = want;
s_pass_plan_diag.planned = n;
if (want && !n && !(in.budget > 0.0))
s_pass_plan_diag.zero_credit_refusals++;
}
return n;
}
}
void gl_renderer_pass_plan_diag(GLRenderPassPlanDiag *out) {
if (out) *out = s_pass_plan_diag;
}
void gl_renderer_pass_note_cost(uint64_t ticks) {
if (s_pass_cost_w != s_interp_w || s_pass_cost_h != s_interp_h) {
memset(&s_pass_cost, 0, sizeof s_pass_cost);
+17 -1
View File
@@ -106,6 +106,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
#include "mod_local_input_policy.h"
#include "mod_session_baseline.h"
#include "mod_runtime.h"
#include "host_launch_timing.h"
#include "mod_packages.h"
#include "present_image_ring.h"
#include "gpu_timeline.h"
@@ -2640,7 +2641,8 @@ static void write_cached_path(const char* argv0, const char* filename,
// Relative inside the game folder so a moved portable folder still works;
// read_cached_path anchors relative paths on the exe directory.
if (f.is_open())
f << PSXRecompV4::relative_to_folder(path, exe_dir_from_argv(argv0)).generic_string() << "\n";
f << PSXRecompV4::host_path_forward_slashes(
PSXRecompV4::relative_to_folder(path, exe_dir_from_argv(argv0))) << "\n";
}
/* Nobody is at the screen: never block on a modal dialog or a file picker. */
@@ -15991,6 +15993,7 @@ int main(int argc, char** argv) {
std::thread overlay_init_thread;
std::exception_ptr overlay_init_exc;
auto run_deferred_overlay_init = [&]() {
PSXRecompV4::HostLaunchTimingScope timing(HOST_LAUNCH_OVERLAY_WORKER);
std::filesystem::path exe_dir = exe_dir_from_argv(argv[0]);
std::string cache_dir = (exe_dir / "cache").string();
std::filesystem::path captures_path =
@@ -16176,6 +16179,7 @@ int main(int argc, char** argv) {
captures_path.string().c_str());
}
code_provider_init(cfg_backend, gcc_avail);
timing.success();
};
if (deferred_overlay_cache) {
@@ -16685,6 +16689,12 @@ int main(int argc, char** argv) {
SDL_Quit();
return qrc;
}
#endif
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
// Once per offline preboot: verified receipts make the selected plan
// ready before the launcher opens. A miss leaves preparation to UI.
if (!net_cfg.enabled && !ls.netplay_launch.enabled && !rui_initial_disc.empty())
PSXRecompV4::mod_runtime_try_prepare_cached(rui_initial_disc);
#endif
char rui_out_disc[1024] = {0};
launcher_boot_timing_mark("host:before_run_window");
@@ -17142,6 +17152,7 @@ int main(int argc, char** argv) {
#endif
if (overlay_init_thread.joinable()) {
PSXRecompV4::HostLaunchTimingScope timing(HOST_LAUNCH_OVERLAY_JOIN);
overlay_init_thread.join();
if (overlay_init_exc) {
try {
@@ -17152,6 +17163,7 @@ int main(int argc, char** argv) {
return 1;
}
}
timing.success();
}
if (game_config_path || disc_override_path || !resolved_disc.empty()) {
@@ -17167,6 +17179,7 @@ int main(int argc, char** argv) {
/* Netplay is vanilla unless the title opted into content negotiation
* ([netplay] content_negotiation); either way the user's persisted
* offline mod selection is left untouched. */
PSXRecompV4::HostLaunchTimingScope timing(HOST_LAUNCH_RUNTIME_COMMIT);
std::string mod_error;
if (net_cfg.enabled) {
if (!netplay_commit_mods(resolved_disc, &mod_error)) {
@@ -17182,6 +17195,7 @@ int main(int argc, char** argv) {
mod_error.c_str());
return 1;
}
timing.success();
}
/* Session start: every session runs this after its mod commit or netplay
* clear -- the first boot here, and the lobby rematch, which re-enters at
@@ -19311,6 +19325,7 @@ soft_return_lobby:
}
}
{
PSXRecompV4::HostLaunchTimingScope timing(HOST_LAUNCH_RUNTIME_COMMIT);
std::string mod_error;
if (net_cfg.enabled) {
if (!netplay_commit_mods(resolved_disc, &mod_error)) {
@@ -19330,6 +19345,7 @@ soft_return_lobby:
SDL_Quit();
return 1;
}
timing.success();
}
/* `goto session_reboot` re-enters below the first-boot session
* block, so run the same session start here, after the
+15 -7
View File
@@ -1,4 +1,5 @@
#include "mod_packages.h"
#include "host_launch_timing.h"
#include "crc32.h"
#include "mod_plugins.h"
@@ -33,8 +34,9 @@ std::map<std::string, ModBuiltinResolver>& builtin_resolvers() {
return value;
}
std::map<std::string, ModMediaPreparer>& media_preparers() {
static std::map<std::string, ModMediaPreparer> value;
struct MediaPreparer { ModMediaPreparer prepare, probe; };
std::map<std::string, MediaPreparer>& media_preparers() {
static std::map<std::string, MediaPreparer> value;
return value;
}
@@ -1443,8 +1445,8 @@ std::string fingerprint_text(const std::string& text) {
} // namespace
bool mod_register_media_preparer(const std::string& id, ModMediaPreparer callback) {
return valid_id(id) && callback && media_preparers().emplace(id, std::move(callback)).second;
bool mod_register_media_preparer(const std::string& id, ModMediaPreparer callback, ModMediaPreparer cache_probe) {
return valid_id(id) && callback && media_preparers().emplace(id, MediaPreparer{std::move(callback), std::move(cache_probe)}).second;
}
bool mod_register_builtin_resolver(const std::string& id, ModBuiltinResolver resolver) {
@@ -3437,7 +3439,7 @@ std::string ModPackageManager::feature_option_value(
}
bool ModPackageManager::prepare_resources(const std::string& game_id,
const fs::path& disc_path, const fs::path& cache_root, std::string* error) {
const fs::path& disc_path, const fs::path& cache_root, std::string* error, bool cached_only) {
auto effective = effective_selections(nullptr, nullptr, nullptr);
auto pending = selections_;
try {
@@ -3462,8 +3464,14 @@ bool ModPackageManager::prepare_resources(const std::string& game_id,
}
std::map<std::string, fs::path> outputs;
std::string reason;
if (!provider->second(context, outputs, reason))
throw std::runtime_error(package->name + ": " + reason);
{
HostLaunchTimingScope timing(HOST_LAUNCH_MEDIA_PROVIDER,
package->id.c_str(), feature.id.c_str());
const auto& callback = cached_only ? provider->second.probe : provider->second.prepare;
if (!callback || !callback(context, outputs, reason))
throw std::runtime_error(package->name + ": " + reason);
timing.success();
}
for (const auto& [name, path] : outputs) {
const auto* resource = find_resource(*package, feature.id, name);
if (!resource || resource->input_only || resource->sha256.empty())
+390 -42
View File
@@ -1,7 +1,9 @@
#include "mod_runtime.h"
#include "host_launch_timing.h"
#include "cpu_state.h"
#include "disc_path.h"
#include "host_file_identity.h"
#include "iso_reader.h"
#include "mod_packages.h"
#include "mod_plugins.h"
@@ -14,6 +16,22 @@
#include "cpu_state.h"
#include "psx_lobby_client.h"
extern "C" uint32_t host_launch_timing_snapshot(HostLaunchTimingEvent* out,
uint32_t cap, uint64_t* total, uint64_t* overwritten) {
return PSXRecompV4::host_launch_timing_ring().snapshot(out, cap, total, overwritten);
}
extern "C" const char* host_launch_timing_stage_name(uint32_t stage) {
static const char* const names[] = {
"provider_commit", "runtime_commit", "commit", "disc_hash",
"prepare_resources", "media_provider", "resolve", "overlay_verify",
"derived_disc", "save_state", "build_disc_index", "overlay_worker",
"overlay_join", "plugin_activation", "plugin_callback",
"provider_netplay_commit"
};
return stage < sizeof(names) / sizeof(names[0]) ? names[stage] : "unknown";
}
#if defined(RECOMP_LAUNCHER)
#include "recomp_launcher.h"
#endif
@@ -89,6 +107,14 @@ struct RuntimeMods {
std::filesystem::path disc_path;
std::filesystem::path effective_disc_path;
uint32_t entry_phys = 0;
uint64_t preparation_revision = 1;
uint64_t ticket_revision = 0;
std::map<std::filesystem::path, std::string> ticket_files;
bool ticket_ready = false;
bool catalog_stamped = false;
std::map<std::filesystem::path, std::string> loaded_catalog;
bool cached_prepare = false;
bool provider_preparing = false;
bool initialized = false;
bool main_applied = false;
bool disc_enabled = false;
@@ -106,6 +132,96 @@ RuntimeMods& state() {
return value;
}
std::filesystem::path media_cache_root() {
std::filesystem::path root;
#if defined(_WIN32)
if (const char* local = std::getenv("LOCALAPPDATA")) root = local;
#else
if (const char* cache = std::getenv("XDG_CACHE_HOME")) root = cache;
else if (const char* home_dir = std::getenv("HOME")) root = std::filesystem::path(home_dir) / ".cache";
#endif
if (root.empty()) root = std::filesystem::temp_directory_path();
return root / "psxrecomp/imports";
}
bool dependency_files(RuntimeMods& s, const std::filesystem::path& disc,
std::map<std::filesystem::path, std::string>& files) {
files.clear();
auto add = [&](const std::filesystem::path& path) {
if (path.empty()) return true;
std::string stamp;
if (!host_file_identity(path, stamp)) return false;
files[path] = std::move(stamp);
return true;
};
if (!add(disc) || !add(resolve_disc_path(disc).data)) return false;
std::error_code ec;
size_t entries = 0;
for (std::filesystem::recursive_directory_iterator it(s.manager.root(), ec), end;
!ec && it != end; it.increment(ec)) {
if (++entries > 4096) return false;
if (it->is_regular_file(ec) && !add(it->path())) return false;
}
if (ec) return false;
for (const auto& [id, versions] : s.manager.packages()) {
(void)versions;
const auto* package = s.manager.selected_package(id);
if (!package) continue;
for (const auto& resource : package->resources) {
if (!s.manager.feature_enabled(id, resource.feature_id) &&
!s.manager.feature_implicitly_enabled(id, resource.feature_id)) continue;
const auto path = s.manager.feature_resource_path(id, resource.feature_id, resource.id);
if (!add(path)) return false;
}
}
return add(s.effective_disc_path);
}
bool catalog_files(RuntimeMods& s, std::map<std::filesystem::path, std::string>& files) {
files.clear();
std::error_code ec;
if (!std::filesystem::exists(s.manager.root(), ec)) return !ec;
size_t entries = 0;
for (std::filesystem::recursive_directory_iterator it(s.manager.root(), ec), end;
!ec && it != end; it.increment(ec)) {
if (++entries > 4096) return false;
if (it->path().filename() != "manifest.toml") continue;
std::string stamp;
if (!host_file_identity(it->path(), stamp)) return false;
files[it->path()] = std::move(stamp);
}
return !ec;
}
bool prepared_ticket_matches(const std::filesystem::path& disc) {
auto& s = state();
if (!s.ticket_ready || s.main_applied || !mod_runtime_session_plan_fp().empty() ||
disc != s.disc_path || s.ticket_revision != s.preparation_revision) return false;
std::map<std::filesystem::path, std::string> files;
if (!dependency_files(s, disc, files) || files != s.ticket_files) return false;
auto manager = s.manager;
if (!manager.prepare_resources(s.game_id, disc, media_cache_root(), nullptr, true)) return false;
for (const auto& [id, versions] : s.manager.packages()) {
(void)versions;
const auto* package = s.manager.selected_package(id);
if (!package) continue;
for (const auto& resource : package->resources)
if (manager.feature_resource_path(id, resource.feature_id, resource.id) !=
s.manager.feature_resource_path(id, resource.feature_id, resource.id)) return false;
}
return true;
}
class ScopedFlag {
bool& flag;
const bool previous;
public:
explicit ScopedFlag(bool& value) : flag(value), previous(value) { flag = true; }
~ScopedFlag() { flag = previous; }
ScopedFlag(const ScopedFlag&) = delete;
ScopedFlag& operator=(const ScopedFlag&) = delete;
};
/* Guest calls made from an entry hook can deliver VBlank callbacks before
* returning. Every callback owns its resource/completion context; restoring
* only entry hooks would let VBlank erase the interrupted plugin or complete
@@ -426,6 +542,78 @@ bool sha256_file(const std::filesystem::path& path, std::string& out,
return true;
}
bool measured_disc_digest(const std::filesystem::path& path, std::string& digest, std::string* error) {
HostLaunchTimingScope timing(HOST_LAUNCH_DISC_HASH);
if (!path.empty()) psx_mod_counter_add("startup.disc_hash", 1);
const bool ok = sha256_file(path, digest, error);
if (ok) timing.success();
return ok;
}
std::string sha256_text(const std::string& value) {
psx_sha256_ctx hash; psx_sha256_init(&hash);
psx_sha256_update(&hash, reinterpret_cast<const uint8_t*>(value.data()), value.size());
uint8_t bytes[32]; psx_sha256_final(&hash, bytes);
std::ostringstream text;
for (uint8_t byte : bytes) text << std::hex << std::setw(2) << std::setfill('0') << unsigned(byte);
return text.str();
}
// Offline only: semantics match sha256_file (CUE data / CHD raw sectors).
bool offline_disc_digest(const std::filesystem::path& path, std::string& digest,
std::string* error, bool cached_only) {
digest.clear();
if (path.empty()) return true;
const auto resolved = resolve_disc_path(path);
std::string stamp, mount_stamp;
const bool reliable = host_file_identity(resolved.data, stamp) &&
host_file_identity(resolved.mount, mount_stamp);
const std::string key = "disc-sha-v2\n" + resolved.mount.generic_string() + "\n" +
resolved.data.generic_string() + "\n" + stamp + "\n" + mount_stamp + "\n";
const bool cacheable = reliable && key.size() <= 4096;
const auto directory = media_cache_root() / "disc-digests-v2";
const auto receipt = directory / (sha256_text(key) + ".txt");
if (cacheable) {
std::ifstream file(receipt, std::ios::binary);
std::array<char, 4227> buffer{};
file.read(buffer.data(), buffer.size());
const std::string record(buffer.data(), size_t(file.gcount()));
if (file.eof() && record.size() == key.size() + 130 &&
record.compare(0, key.size(), key) == 0) {
const std::string value = record.substr(key.size(), 64);
const std::string body = key + value + "\n";
if (std::all_of(value.begin(), value.end(), [](char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); }) &&
record == body + sha256_text(body) + "\n") {
digest = value;
psx_mod_counter_add("startup.disc_digest_hit", 1);
return true;
}
}
}
if (cached_only) return false;
if (!measured_disc_digest(path, digest, error)) return false;
std::string after, mount_after;
if (!cacheable || !host_file_identity(resolved.data, after) || after != stamp ||
!host_file_identity(resolved.mount, mount_after) || mount_after != mount_stamp) return true;
std::error_code ec;
std::filesystem::create_directories(directory, ec);
if (ec) return true;
#if defined(_WIN32)
const auto temporary = receipt.string() + "." + std::to_string(GetCurrentProcessId()) + ".tmp";
#else
const auto temporary = receipt.string() + "." + std::to_string(getpid()) + ".tmp";
#endif
const std::string body = key + digest + "\n";
{ std::ofstream file(temporary, std::ios::binary | std::ios::trunc);
if (!(file << body << sha256_text(body) << '\n')) return true; }
#if defined(_WIN32)
MoveFileExW(std::filesystem::path(temporary).c_str(), receipt.c_str(), MOVEFILE_REPLACE_EXISTING);
#else
std::filesystem::rename(temporary, receipt, ec);
#endif
return true;
}
std::filesystem::path raw_image_path(const std::filesystem::path& path,
std::string* error) {
std::string extension = path.extension().string();
@@ -1129,6 +1317,8 @@ int provider_version_get(void*, const char* package_id, int index,
template <typename Callback>
int mutate(Callback callback) {
state().ticket_ready = false;
++state().preparation_revision;
std::string error;
if (!callback(error)) {
set_error(error);
@@ -1149,6 +1339,9 @@ int provider_install(void*, const char* path) {
std::string id, version;
if (!state().manager.install_archive(path, &id, &version, &error)) return false;
if (!state().manager.scan(&error)) return false;
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
state().catalog_stamped = catalog_files(state(), state().loaded_catalog);
#endif
return state().manager.select_version(id, version, &error);
});
}
@@ -1156,7 +1349,11 @@ int provider_install(void*, const char* path) {
int provider_remove(void*, const char* id, const char* version) {
if (!id || !version) return 0;
return mutate([&](std::string& error) {
return state().manager.remove_version(id, version, &error);
if (!state().manager.remove_version(id, version, &error)) return false;
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
state().catalog_stamped = catalog_files(state(), state().loaded_catalog);
#endif
return true;
});
}
@@ -1182,17 +1379,21 @@ int provider_set_option(void*, const char* id, const char* option, const char* v
}
int provider_commit(void*, const char* image_path) {
HostLaunchTimingScope timing(HOST_LAUNCH_PROVIDER_COMMIT);
std::string error;
if (!mod_runtime_commit(image_path ? std::filesystem::path(image_path) :
std::filesystem::path(), &error)) {
const bool ok = mod_runtime_prepare_for_launcher(
image_path ? std::filesystem::path(image_path) : std::filesystem::path(), &error);
if (!ok) {
set_error(error);
return 0;
}
state().error.clear();
timing.success();
return 1;
}
int provider_commit_netplay(void*, const char* image_path) {
HostLaunchTimingScope timing(HOST_LAUNCH_PROVIDER_NETPLAY_COMMIT);
std::string error;
/* Without [netplay] content_negotiation every netplay session is vanilla. */
if (!(mod_runtime_netplay_content_negotiation()
@@ -1203,6 +1404,7 @@ int provider_commit_netplay(void*, const char* image_path) {
return 0;
}
state().error.clear();
timing.success();
return 1;
}
@@ -1261,6 +1463,8 @@ bool mod_runtime_initialize(const std::filesystem::path& root,
const std::filesystem::path& exe_path,
std::string* error) {
RuntimeMods& s = state();
s.ticket_ready = false;
++s.preparation_revision;
offline_manager_before_netplay.reset();
mod_runtime_set_session_plan_fp({});
s.manager.set_root({});
@@ -1288,6 +1492,10 @@ bool mod_runtime_initialize(const std::filesystem::path& root,
s.manager.set_root(root);
s.game_id = game_id;
s.entry_phys = game_entry_pc & 0x1FFFFFFFu;
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
std::map<std::filesystem::path, std::string> catalog_before;
const bool catalog_before_stamped = catalog_files(s, catalog_before);
#endif
if (!s.manager.scan(&s.error) || !s.manager.load_state(&s.error)) {
if (error) *error = s.error;
return false;
@@ -1348,11 +1556,17 @@ bool mod_runtime_initialize(const std::filesystem::path& root,
s.exe_sha256.clear();
s.error.clear();
}
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
s.catalog_stamped = catalog_before_stamped && catalog_files(s, s.loaded_catalog) &&
catalog_before == s.loaded_catalog;
#endif
s.initialized = true;
return true;
}
bool mod_runtime_clear_for_netplay(std::string* error) {
state().ticket_ready = false;
++state().preparation_revision;
RuntimeMods& s = state();
if (!s.initialized) {
if (error) error->clear();
@@ -1587,21 +1801,15 @@ static bool netplay_resources_verified(const ModResolution& plan,
}
bool mod_runtime_prepare_resources(const std::filesystem::path& disc_path, std::string* error) {
HostLaunchTimingScope timing(HOST_LAUNCH_PREPARE_RESOURCES);
RuntimeMods& s = state();
if (!s.initialized) return true;
std::filesystem::path media_cache;
#if defined(_WIN32)
if (const char* local = std::getenv("LOCALAPPDATA")) media_cache = local;
#else
if (const char* cache = std::getenv("XDG_CACHE_HOME")) media_cache = cache;
else if (const char* home_dir = std::getenv("HOME")) media_cache = std::filesystem::path(home_dir) / ".cache";
#endif
if (media_cache.empty()) media_cache = std::filesystem::temp_directory_path();
media_cache /= "psxrecomp/imports";
if (!s.manager.prepare_resources(s.game_id, disc_path, media_cache, &s.error)) {
if (!s.initialized) { timing.success(); return true; }
const auto media_cache = media_cache_root();
if (!s.manager.prepare_resources(s.game_id, disc_path, media_cache, &s.error, s.cached_prepare)) {
if (error) *error = s.error;
return false;
}
timing.success();
return true;
}
@@ -1615,9 +1823,18 @@ static void note_disc(RuntimeMods& s, const std::filesystem::path& disc_path) {
s.disc_sha256 = std::move(digest);
}
/* Capture before resolution: a resolver may mutate a resource while running. */
struct PreparedPlanInputs {
std::filesystem::path disc_path;
std::string disc_identity, data_identity;
std::map<std::filesystem::path, std::string> dependencies;
bool disc_stamped = false, stable = false;
};
static bool install_plan(RuntimeMods& s, ModResolution plan,
bool save_selection, bool verify_netplay,
std::string* error) {
std::string* error,
const PreparedPlanInputs* inputs = nullptr) {
s.validation = plan;
/* A derived activation is not in state.toml, so name it: a player (or a
* test) reading the log can see why a hidden feature is running. */
@@ -1642,28 +1859,52 @@ static bool install_plan(RuntimeMods& s, ModResolution plan,
if (error) *error = s.error;
return false;
}
for (const ModResolution::Overlay& overlay : plan.overlays) {
if (overlay.expected_sha256.empty()) continue;
std::string actual;
if (!sha256_disc_range(
s.disc_path, overlay.target, overlay.location,
overlay.payload.size(), actual, &s.error) ||
actual != overlay.expected_sha256) {
if (s.error.empty())
s.error = overlay.package_id + "/" + overlay.feature_id +
": stock overlay range checksum failed";
{
HostLaunchTimingScope overlay_timing(HOST_LAUNCH_OVERLAY_VERIFY);
for (const ModResolution::Overlay& overlay : plan.overlays) {
if (overlay.expected_sha256.empty()) continue;
std::string actual;
if (!sha256_disc_range(
s.disc_path, overlay.target, overlay.location,
overlay.payload.size(), actual, &s.error) ||
actual != overlay.expected_sha256) {
if (s.error.empty())
s.error = overlay.package_id + "/" + overlay.feature_id +
": stock overlay range checksum failed";
if (error) *error = s.error;
return false;
}
}
overlay_timing.success();
}
std::filesystem::path effective_disc;
{
HostLaunchTimingScope derived_timing(HOST_LAUNCH_DERIVED_DISC);
if (!materialize_derived_disc(s, plan, effective_disc, &s.error)) {
if (error) *error = s.error;
return false;
}
derived_timing.success();
}
std::filesystem::path effective_disc;
if (!materialize_derived_disc(s, plan, effective_disc, &s.error)) {
if (error) *error = s.error;
return false;
}
if (save_selection && !s.manager.save_state(&s.error)) {
if (error) *error = s.error;
return false;
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
std::map<std::filesystem::path, std::string> after_resolve;
std::string disc_after, data_after;
const bool unchanged_disc = inputs && (inputs->disc_path.empty() ||
(inputs->disc_stamped && host_file_identity(inputs->disc_path, disc_after) &&
inputs->disc_identity == disc_after &&
host_file_identity(resolve_disc_path(inputs->disc_path).data, data_after) &&
inputs->data_identity == data_after));
const bool unchanged_inputs = unchanged_disc && inputs->stable &&
dependency_files(s, inputs->disc_path, after_resolve) &&
inputs->dependencies == after_resolve;
#endif
if (save_selection) {
HostLaunchTimingScope save_timing(HOST_LAUNCH_SAVE_STATE);
if (!s.manager.save_state(&s.error)) {
if (error) *error = s.error;
return false;
}
save_timing.success();
}
/* Hooks follow activation, never a bare commit: a new plan runs none of
* its function-entry hooks until mod_runtime_activate_plugins(). */
@@ -1672,30 +1913,122 @@ static bool install_plan(RuntimeMods& s, ModResolution plan,
s.audio_tracks.clear();
s.extent_start = 0;
s.plan = std::move(plan);
build_disc_index(s);
{
HostLaunchTimingScope index_timing(HOST_LAUNCH_BUILD_DISC_INDEX);
build_disc_index(s);
index_timing.success();
}
s.effective_disc_path = std::move(effective_disc);
s.main_applied = false;
s.error.clear();
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
if (save_selection && (s.provider_preparing || s.cached_prepare) &&
s.catalog_stamped && unchanged_inputs && session_plan_fp().empty()) {
s.ticket_revision = s.preparation_revision;
s.ticket_ready = dependency_files(s, inputs->disc_path, s.ticket_files);
}
#endif
return true;
}
bool mod_runtime_commit(const std::filesystem::path& disc_path, std::string* error,
bool save_selection) {
bool mod_runtime_commit(const std::filesystem::path& disc_path, std::string* error, bool save_selection) {
HostLaunchTimingScope timing(HOST_LAUNCH_COMMIT);
RuntimeMods& s = state();
if (!s.initialized) return true;
s.netplay_view_plan = false;
if (disc_path != s.disc_path) {
if (!s.initialized) { timing.success(); return true; }
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
std::map<std::filesystem::path, std::string> catalog;
if (s.catalog_stamped && (!catalog_files(s, catalog) || catalog != s.loaded_catalog)) {
s.ticket_ready = false;
s.error = "Mod catalog changed after loading; reopen the launcher to rescan it.";
if (error) *error = s.error;
return false;
}
if (save_selection && prepared_ticket_matches(disc_path)) {
if (!s.provider_preparing) s.ticket_ready = false;
clear_function_entry_hooks();
s.disc_extents.clear(); s.audio_tracks.clear(); s.extent_start = 0;
s.main_applied = false;
s.error.clear();
psx_mod_counter_add("startup.plan_reuse", 1);
timing.success();
return true;
}
#endif
s.ticket_ready = false;
PreparedPlanInputs inputs;
inputs.disc_path = disc_path;
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
inputs.disc_stamped = disc_path.empty() ||
(host_file_identity(disc_path, inputs.disc_identity) &&
host_file_identity(resolve_disc_path(disc_path).data, inputs.data_identity));
#endif
#if !defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
if (disc_path != s.disc_path)
#endif
{
std::string hash_error, digest;
if (!sha256_file(disc_path, digest, &hash_error)) digest.clear();
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
const bool hashed = save_selection
? offline_disc_digest(disc_path, digest, &hash_error, s.cached_prepare)
: measured_disc_digest(disc_path, digest, &hash_error);
#else
const bool hashed = measured_disc_digest(disc_path, digest, &hash_error);
#endif
if (!hashed && s.cached_prepare) return false;
if (!hashed) digest.clear();
s.disc_path = disc_path;
s.disc_sha256 = std::move(digest);
}
if (!mod_runtime_prepare_resources(disc_path, error)) return false;
return install_plan(s, s.manager.resolve(s.game_id, s.exe_sha256, s.disc_sha256),
save_selection, !save_selection, error);
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
inputs.stable = dependency_files(s, disc_path, inputs.dependencies);
#else
inputs.stable = false;
#endif
ModResolution plan;
{
HostLaunchTimingScope resolve_timing(HOST_LAUNCH_RESOLVE);
plan = s.manager.resolve(s.game_id, s.exe_sha256, s.disc_sha256);
if (plan.ok) resolve_timing.success();
}
const bool ok = install_plan(s, std::move(plan), save_selection,
!save_selection, error, &inputs);
if (ok) timing.success();
return ok;
}
bool mod_runtime_prepare_for_launcher(const std::filesystem::path& disc_path, std::string* error) {
ScopedFlag flag(state().provider_preparing);
return mod_runtime_commit(disc_path, error, true);
}
bool mod_runtime_try_prepare_cached(const std::filesystem::path& disc_path) {
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
try {
auto& s = state();
if (!s.initialized || s.main_applied || !session_plan_fp().empty()) return false;
auto probe = s.manager;
if (!probe.prepare_resources(s.game_id, disc_path, media_cache_root(), nullptr, true)) return false;
ScopedFlag flag(s.cached_prepare);
const bool ok = mod_runtime_commit(disc_path, nullptr, true);
if (ok && s.ticket_ready) psx_mod_counter_add("startup.preload_hit", 1);
return ok && s.ticket_ready;
} catch (...) {
// Preload is best effort before the launcher exists. The ordinary
// provider path reports full-preparation errors once UI is available.
state().ticket_ready = false;
return false;
}
#else
(void)disc_path;
return false;
#endif
}
void mod_runtime_set_session_plan_fp(const std::string& fp) {
state().ticket_ready = false;
++state().preparation_revision;
session_plan_fp() = fp;
}
@@ -1884,6 +2217,15 @@ const std::filesystem::path& mod_runtime_effective_disc_path() {
#if defined(RECOMP_LAUNCHER)
const RecompLauncherCModProvider* mod_runtime_launcher_provider() {
#if defined(RECOMP_LAUNCHER_HAS_WORKER_MOD_COMMIT) && defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
provider.commit_worker_safe = 1;
#if defined(RECOMP_LAUNCHER_HAS_PREPARED_MOD_COMMIT)
provider.try_commit = [](void*, const char* path) -> int {
return prepared_ticket_matches(path ? std::filesystem::path(path) : std::filesystem::path()) ? 1 : 0;
};
provider.preparation_revision = [](void*) -> unsigned long long { return state().preparation_revision; };
#endif
#endif
return &provider;
}
@@ -2278,22 +2620,28 @@ bool mod_runtime_read_disc_file_sectors(const std::string& path, uint32_t max_by
extern "C" void mod_runtime_activate_plugins(void) {
using namespace PSXRecompV4;
HostLaunchTimingScope timing(HOST_LAUNCH_PLUGIN_ACTIVATION);
RuntimeMods& s = state();
s.ticket_ready = false;
psx_projection_reset_session();
gpu_ws_set_native_scene_predicate(nullptr);
psx_ram_reset_size_request();
gpu_hd_textures_shutdown();
if (!s.initialized || !s.plan.ok) return;
if (!s.initialized || !s.plan.ok) { timing.success(); return; }
s.disc_extents.clear();
s.audio_tracks.clear();
s.extent_start = 0;
s.activating = true;
for (const ModResolution::Plugin& plugin : s.plan.plugins) {
HostLaunchTimingScope plugin_timing(HOST_LAUNCH_PLUGIN_CALLBACK,
plugin.package_id.c_str(), plugin.feature_id.c_str());
PluginCallbackScope scope(s, &plugin);
mod_invoke_activation_plugin(plugin.id);
plugin_timing.success();
}
s.activating = false;
build_function_entry_hooks(s);
timing.success();
}
extern "C" void mod_runtime_on_vblank(void) {
+3 -2
View File
@@ -196,8 +196,9 @@ double render_pass_budget(double idle_ticks, double pass_ticks,
if (!(frame_length > 0.0)) return 0.0;
if (!(share > 0.0)) return 0.0;
if (share > 1.0) share = 1.0;
if (!(idle_ticks > 0.0) && !(pass_ticks > 0.0))
return frame_length * share;
/* Both zero also describes a mature, overloaded frame after a skipped
* pass. Inventing a nominal allowance there alternates expensive replay
* with shedding. Unknown costs may warm up when measured credit returns. */
b = ((idle_ticks > 0.0 ? idle_ticks : 0.0) +
(pass_ticks > 0.0 ? pass_ticks : 0.0)) * share;
if (b > frame_length) b = frame_length;
@@ -0,0 +1,9 @@
#include "host_launch_timing.h"
/* Separate translation unit: catches accidentally per-includer ring/parent
* state, which would hide media-provider work from the runtime TCP reader. */
void host_launch_timing_fixture_nested() {
PSXRecompV4::HostLaunchTimingScope scope(HOST_LAUNCH_MEDIA_PROVIDER,
"fixture.package", "fixture.feature");
scope.success();
}
@@ -0,0 +1,256 @@
/* Exercise the real GPU checkpoint and GP0 provenance consumers without a GL
* context. RAM rollback deliberately follows GPU restore, as render_pass.c
* does: a later canonical DMA must find the proof for its restored words. */
#define GPU_EXEC_REAL_UI_GROUP
#define main gpu_packet_fixture_main
#include "test_gpu_textured_dot_nw_shift_exec.c"
#undef main
static const uint32_t command_addr = 0x10004u;
static const uint32_t canonical[] = {
0x28020304u, 0, 320, 240u << 16, (240u << 16) | 320u
};
static void packet_to_ram(const uint32_t *words, uint32_t count) {
memcpy(&test_ram[command_addr / 4u], words, count * sizeof(words[0]));
}
static void packet_to_gp0(const uint32_t *words, uint32_t count) {
gp0_cmd_source_addr = command_addr;
gp0_words_needed = (int)count;
memcpy(gp0_cmd_buf, words, count * sizeof(words[0]));
}
static void start_scene(void) {
reset_gpu_state_for_test();
configure_native_wide_16_9();
ws_hud_anchor_clear(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE);
ws_hud_anchor_clear(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE);
ws_radial_mask_clear(ws_radial_screen_mask_tags);
ws_repeat_rect_tag_clear(ws_repeat_rect_tags);
memset(ws_primitive_roles, 0, sizeof(ws_primitive_roles));
ws_ui_prepass_count = ws_ui_prepass_node_count = 0;
ws_ui_prepass_rank = 0xFFFFu;
ws_reset_scene_history();
ws_nw_phase_backdrop = 0;
s_bg_phase_frame = UINT32_MAX;
packet_to_ram(canonical, 5);
packet_to_gp0(canonical, 5);
}
static void test_retagged_canonical_packet(void) {
start_scene();
gpu_ws_tag_background_prim(command_addr - 4u);
gpu_ws_tag_hud_prim(command_addr - 4u, -1);
gpu_ws_tag_world_primitive(command_addr - 4u, 1);
gpu_ws_tag_hud_primitive(command_addr - 4u, -1);
const int32_t offset = ws_nw_offset();
assert(offset > 0);
GpuWsTagStats before, after;
gpu_ws_get_tag_stats(NULL, &before);
/* Repeated in-between draws reuse the same packet address and guest frame. */
for (uint32_t pass = 1; pass <= 3; ++pass) {
uint32_t synthetic[5];
memcpy(synthetic, canonical, sizeof(synthetic));
synthetic[1] += pass;
assert(gpu_pass_checkpoint_save());
packet_to_ram(synthetic, 5);
packet_to_gp0(synthetic, 5);
gpu_ws_tag_background_prim(command_addr - 4u);
gpu_ws_tag_hud_prim(command_addr - 4u, 1);
gpu_ws_tag_world_primitive(command_addr - 4u, 0);
gpu_ws_tag_hud_primitive(command_addr - 4u, 1);
int32_t delta;
assert(psx_ws_prim_in_backdrop() == 1);
assert(ws_nw_explicit_hud_delta(&delta) && delta == offset);
gpu_pass_checkpoint_restore();
packet_to_ram(canonical, 5);
packet_to_gp0(canonical, 5);
assert(psx_ws_prim_in_backdrop() == 1);
assert(ws_nw_explicit_hud_delta(&delta) && delta == -offset);
assert(ws_tagged_world_primitive());
assert(ws_tagged_hud_edge() == -1);
assert(gpu_ws_background_requires_full_composite());
packet_to_gp0(synthetic, 5);
assert(!ws_nw_explicit_background());
assert(!ws_nw_explicit_hud_delta(&delta));
packet_to_gp0(canonical, 5);
}
gpu_ws_get_tag_stats(NULL, &after);
assert(after.tag_calls == before.tag_calls + 3);
assert(after.hit >= before.hit + 6); /* Diagnostic activity is not rolled back. */
}
static void test_pass_only_tags_and_phase(void) {
/* No image is consumed: discarded/watchdog-aborted passes use this same
* GPU restore. render_pass_abort_test covers the watchdog control flow
* separately with a mocked GPU/presenter. */
start_scene();
ws_nw_phase_backdrop = 1;
ws_bg_phase_note(0x28u);
assert(psx_ws_prim_in_backdrop() == 1);
assert(!gpu_ws_background_requires_full_composite());
assert(gpu_pass_checkpoint_save());
gpu_ws_tag_background_prim(command_addr - 4u);
gpu_ws_tag_hud_prim(command_addr - 4u, 1);
gpu_ws_tag_world_primitive(command_addr - 4u, 1);
ws_bg_phase_note(0x30u);
assert(gpu_ws_background_requires_full_composite());
gpu_pass_checkpoint_restore();
packet_to_ram(canonical, 5);
packet_to_gp0(canonical, 5);
assert(!ws_nw_explicit_background());
assert(!ws_nw_explicit_hud_delta(NULL));
assert(!gpu_ws_background_stretch_active());
assert(!gpu_ws_background_requires_full_composite());
assert(!ws_tagged_world_primitive());
assert(psx_ws_prim_in_backdrop() == 1); /* Original early-backdrop phase. */
}
static void test_hud_dot_draw(void) {
start_scene();
set_dot_packet(command_addr, 20, 20);
gp0_words_needed = 3;
uint32_t dot[3];
memcpy(dot, gp0_cmd_buf, sizeof(dot));
gpu_ws_tag_hud_prim(command_addr - 4u, -1);
assert(gpu_pass_checkpoint_save());
set_dot_packet(command_addr, 80, 20);
gpu_ws_tag_hud_prim(command_addr - 4u, 1);
exec_dot_and_expect(80 + ws_nw_offset(), 20);
gpu_pass_checkpoint_restore();
packet_to_ram(dot, 3);
packet_to_gp0(dot, 3);
exec_dot_and_expect(20 - ws_nw_offset(), 20);
}
static void test_screen_mask_packet(void) {
start_scene();
gpu_ws_tag_screen_mask_quad(command_addr - 4u);
gpu_ws_tag_radial_screen_mask_quad(command_addr - 4u, 2.f);
assert(gpu_pass_checkpoint_save());
uint32_t synthetic[5];
memcpy(synthetic, canonical, sizeof(synthetic));
synthetic[1] = 16;
packet_to_ram(synthetic, 5);
gpu_ws_tag_screen_mask_quad(command_addr - 4u);
gpu_ws_tag_radial_screen_mask_quad(command_addr - 4u, 3.f);
gpu_pass_checkpoint_restore();
packet_to_ram(canonical, 5);
packet_to_gp0(canonical, 5);
assert(ws_hud_anchor_lookup(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE,
command_addr, gp0_cmd_buf, 5, (uint32_t)s_frame_count, NULL));
int32_t x[4] = {0, 320, 0, 320}, y[4] = {0, 0, 240, 240};
assert(ws_nw_radial_mask_transform(x, y));
assert(x[0] == -160 && x[1] == 480); /* Canonical scale 2, not pass scale 3. */
}
static void test_repeat_rect_packet(void) {
start_scene();
const uint32_t rect[] = {0x64808080u, 0, 0, (64u << 16) | 32u};
uint32_t synthetic[4];
memcpy(synthetic, rect, sizeof(synthetic));
synthetic[1] = 8;
packet_to_ram(rect, 4);
packet_to_gp0(rect, 4);
gpu_ws_tag_black_reveal_rect(command_addr - 4u);
gpu_ws_tag_repeat_rect(command_addr - 4u, 128);
assert(gpu_pass_checkpoint_save());
packet_to_ram(synthetic, 4);
gpu_ws_tag_black_reveal_rect(command_addr - 4u);
gpu_ws_tag_repeat_rect(command_addr - 4u, 64);
gpu_pass_checkpoint_restore();
packet_to_ram(rect, 4);
packet_to_gp0(rect, 4);
assert(ws_hud_anchor_lookup(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE,
command_addr, gp0_cmd_buf, 4, (uint32_t)s_frame_count, NULL));
assert(ws_repeat_rect_tag_lookup(ws_repeat_rect_tags, command_addr,
gp0_cmd_buf, 4, (uint32_t)s_frame_count) == 128);
}
static void test_scene_evidence(void) {
start_scene();
ws_full_2d = 0;
gpu_ws_set_gte_game_mode(1);
assert(gpu_ws_present_native_43());
assert(gpu_pass_checkpoint_save());
psx_ws_note_gte_project(3);
assert(!gpu_ws_present_native_43());
gpu_pass_checkpoint_restore();
packet_to_ram(canonical, 5);
assert(gpu_ws_present_native_43());
gpu_ws_set_gte_game_mode(0);
}
static void test_sprite_anchor(void) {
start_scene();
/* Sprite-anchor coordinates are consumed by the projection/squash path;
* native-wide uses the same table only for packet identity. */
ws_mode = 1;
ws_xnum = 3;
ws_xden = 4;
const uint32_t anchor_addr = 0x20000u;
ws_anchor_addr = anchor_addr;
CPUState cpu = {0};
cpu.read_word = psx_read_word;
cpu.gpr[4] = command_addr - 4u;
test_ram[anchor_addr / 4u] = 40;
psx_ws_sprite_tag(&cpu);
assert(gpu_pass_checkpoint_save());
test_ram[anchor_addr / 4u] = 80;
psx_ws_sprite_tag(&cpu);
gpu_pass_checkpoint_restore();
test_ram[anchor_addr / 4u] = 40;
int32_t anchor;
assert(ws_tagged_anchor(&anchor) && anchor == 40);
}
static void test_auto_ui_prepass(void) {
start_scene();
ws_mode = 1;
ws_xnum = 3;
ws_xden = 4;
gpu_ws_set_auto_ui_squash(1);
const uint32_t hud[] = {
0x28802040u, pack_vertex(20, 20), pack_vertex(40, 20),
pack_vertex(20, 32), pack_vertex(40, 32)
};
uint32_t synthetic[5];
memcpy(synthetic, hud, sizeof(synthetic));
synthetic[1] = pack_vertex(24, 20);
synthetic[3] = pack_vertex(24, 32);
test_ram[(command_addr - 4u) / 4u] = (5u << 24) | 0xFFFFFFu;
const uint32_t ot_head = 0xFF00u;
test_ram[ot_head / 4u] = command_addr - 4u; /* Empty OT entry establishes rank 0. */
packet_to_ram(hud, 5);
gpu_ws_prepass_linked_list(ot_head);
packet_to_gp0(hud, 5);
int32_t anchor;
assert(ws_auto_ui_anchor(&anchor));
assert(gpu_pass_checkpoint_save());
packet_to_ram(synthetic, 5);
gpu_ws_prepass_linked_list(ot_head);
gpu_pass_checkpoint_restore();
packet_to_ram(hud, 5);
gpu_ws_validate_linked_list_node(command_addr - 4u, 5);
packet_to_gp0(hud, 5);
assert(ws_auto_ui_anchor(&anchor));
packet_to_gp0(synthetic, 5);
assert(!ws_auto_ui_anchor(&anchor));
gpu_ws_set_auto_ui_squash(0);
}
int main(void) {
test_retagged_canonical_packet();
test_pass_only_tags_and_phase();
test_hud_dot_draw();
test_screen_mask_packet();
test_repeat_rect_packet();
test_sprite_anchor();
test_auto_ui_prepass();
test_scene_evidence();
puts("gpu_render_pass_provenance: PASS");
return 0;
}
+96
View File
@@ -0,0 +1,96 @@
#include "host_launch_timing.h"
#include <iostream>
#include <stdexcept>
#include <string>
#include <thread>
#include <type_traits>
using namespace PSXRecompV4;
void host_launch_timing_fixture_nested();
static int failures;
static void check(bool value, const char* message) {
if (!value) { std::cerr << "FAIL: " << message << '\n'; ++failures; }
}
int main() {
static_assert(std::is_trivially_copyable<HostLaunchTimingEvent>::value,
"C-facing snapshot must remain POD");
HostLaunchTimingEvent events[HOST_LAUNCH_TIMING_CAPACITY]{};
uint64_t total = 0, overwritten = 0;
{
HostLaunchTimingScope parent(HOST_LAUNCH_COMMIT);
host_launch_timing_fixture_nested();
parent.success();
}
auto n = host_launch_timing_ring().snapshot(events, 256, &total, &overwritten);
check(n == 2 && total == 2 && overwritten == 0, "shared ring across translation units");
check(events[0].parent_id == events[1].scope_id && events[1].parent_id == 0,
"shared nested parent across translation units");
check(events[0].scope_id > events[1].scope_id && events[0].seq < events[1].seq,
"completion order differs from start order");
check(events[0].start_us >= events[1].start_us &&
events[0].start_us + events[0].duration_us <=
events[1].start_us + events[1].duration_us, "child time inside parent");
check(events[0].ok && events[1].ok &&
std::string(events[0].package_id) == "fixture.package", "success and metadata retained");
HostLaunchTimingRing ring;
try {
HostLaunchTimingScope failed(HOST_LAUNCH_PREPARE_RESOURCES, nullptr, nullptr, ring);
HostLaunchTimingScope child(HOST_LAUNCH_MEDIA_PROVIDER, nullptr, nullptr, ring);
throw std::runtime_error("fixture failure");
} catch (const std::runtime_error&) { }
{
const std::string long_label(100, 'x');
HostLaunchTimingScope next(HOST_LAUNCH_RESOLVE, long_label.c_str(), "short", ring);
next.success();
}
n = ring.snapshot(events, 256, &total, &overwritten);
check(n == 3 && !events[0].ok && !events[1].ok && events[2].ok,
"exception exits remain failures and next call succeeds");
check(events[0].parent_id == events[1].scope_id && events[2].parent_id == 0,
"exception restores enclosing context");
check(events[2].labels_truncated && std::strlen(events[2].package_id) == 63 &&
std::string(events[2].feature_id) == "short", "bounded labels report truncation");
for (unsigned i = 0; i < HOST_LAUNCH_TIMING_CAPACITY; ++i) {
HostLaunchTimingScope scope(HOST_LAUNCH_DISC_HASH, nullptr, nullptr, ring);
scope.success();
}
n = ring.snapshot(events, 256, &total, &overwritten);
check(n == 256 && total == 259 && overwritten == 3, "wrap retains fixed capacity");
for (uint32_t i = 1; i < n; ++i)
check(events[i].seq == events[i - 1].seq + 1, "wrapped snapshot oldest first");
n = ring.snapshot(events, 2, &total, &overwritten);
check(n == 2 && events[0].seq == 258 && events[1].seq == 259,
"small reader receives newest entries without draining");
check(ring.snapshot(nullptr, 256, &total, &overwritten) == 0 && total == 259,
"metadata query does not consume entries");
HostLaunchTimingRing concurrent;
auto writer = [&]() {
for (unsigned i = 0; i < 40; ++i) {
HostLaunchTimingScope parent(HOST_LAUNCH_COMMIT, nullptr, nullptr, concurrent);
HostLaunchTimingScope child(HOST_LAUNCH_RESOLVE, nullptr, nullptr, concurrent);
child.success(); parent.success();
}
};
std::thread a(writer), b(writer);
a.join(); b.join();
n = concurrent.snapshot(events, 256, &total, &overwritten);
check(n == 160 && total == 160 && overwritten == 0, "concurrent completion count preserved");
for (uint32_t i = 0; i < n; ++i) {
check(events[i].seq == i + 1 && events[i].ok, "concurrent sequence publication");
if (events[i].stage == HOST_LAUNCH_COMMIT)
check(events[i].parent_id == 0, "thread-local roots do not inherit other writer");
else {
bool found = false;
for (uint32_t j = i + 1; j < n; ++j)
if (events[j].scope_id == events[i].parent_id &&
events[j].stage == HOST_LAUNCH_COMMIT) found = true;
check(found, "concurrent child belongs to a completed parent");
}
}
return failures ? 1 : 0;
}
@@ -0,0 +1,112 @@
"""Execute the production TCP handler/helpers and real ring through the C ABI.
No server, game, SDL, or persistent log. Missing compilers are an explicit skip.
"""
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import unittest
from test_wtrace_dump import matching_brace
ROOT = Path(__file__).resolve().parents[2]
def function(source, name):
match = re.search(r'^(?:static |extern "C" )[^;{}]+\b' + name +
r'\([^;{}]*\)\s*\{', source, re.M)
if not match:
raise AssertionError(f"production function missing: {name}")
return source[match.start():matching_brace(source, match.end() - 1) + 1]
class HostLaunchProtocol(unittest.TestCase):
@classmethod
def setUpClass(cls):
cc, cxx = os.environ.get("CC", "cc"), os.environ.get("CXX", "c++")
if not shutil.which(cc) or not shutil.which(cxx):
raise unittest.SkipTest("set CC and CXX to the native compilers")
cls.temp = tempfile.TemporaryDirectory()
cls.addClassCleanup(cls.temp.cleanup)
temp = Path(cls.temp.name)
debug = (ROOT / "runtime/src/debug_server.c").read_text(encoding="utf-8")
runtime = (ROOT / "runtime/src/mod_runtime.cpp").read_text(encoding="utf-8")
harness = '''#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "host_launch_timing.h"
void seed_host_launch_timing_fixture(int count);
static void debug_server_send_line(const char *line) { puts(line); }
static void send_err(int id, const char *error) { (void)id; (void)error; abort(); }
'''
for name in ("json_get_int", "json_escape_string", "handle_host_launch_timings"):
harness += function(debug, name) + "\n"
harness += '''int main(int argc, char **argv) {
if (argc != 3) return 2;
seed_host_launch_timing_fixture(atoi(argv[2]));
handle_host_launch_timings(42, argv[1]);
return 0;
}
'''
bridge = '#include "host_launch_timing.h"\n'
for name in ("host_launch_timing_snapshot", "host_launch_timing_stage_name"):
bridge += function(runtime, name) + "\n"
bridge += '''extern "C" void seed_host_launch_timing_fixture(int count) {
char label[64];
for (unsigned i = 0; i < 63; ++i) label[i] = i % 3 == 0 ? '"' : i % 3 == 1 ? '\\\\' : '\\1';
label[63] = 0;
for (int i = 0; i < count; ++i) {
PSXRecompV4::HostLaunchTimingScope scope(HOST_LAUNCH_MEDIA_PROVIDER, label, label);
if (i % 2 == 0) scope.success();
}
}
'''
(temp / "handler.c").write_text(harness, encoding="utf-8")
(temp / "bridge.cpp").write_text(bridge, encoding="utf-8")
include = str(ROOT / "runtime/include")
cls.exe = temp / ("protocol.exe" if os.name == "nt" else "protocol")
for args in ([cc, "-std=c99", "-Wall", "-Wextra", "-Werror", "-I", include,
"-c", str(temp / "handler.c"), "-o", str(temp / "handler.o")],
[cxx, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-pthread",
"-I", include, str(temp / "bridge.cpp"), str(temp / "handler.o"),
"-o", str(cls.exe)]):
subprocess.run(args, check=True, capture_output=True, text=True)
def query(self, count=None, seeded=300):
request = {"cmd": "host_launch_timings"}
if count is not None:
request["count"] = count
output = subprocess.check_output([str(self.exe), json.dumps(request), str(seeded)], text=True)
return json.loads(output)
def test_wrapped_maximum_labels_and_default_count(self):
result = self.query()
self.assertEqual((result["capacity"], result["total"], result["overwritten"]), (256, 300, 44))
self.assertTrue(result["nested_durations_overlap"])
self.assertEqual([e["seq"] for e in result["entries"]], list(range(45, 301)))
for e in result["entries"]:
self.assertEqual(e["stage"], "media_provider")
self.assertEqual(e["package"], '"\\\x01' * 21)
self.assertEqual(e["feature"], e["package"])
self.assertEqual(e["labels_truncated"], 0)
self.assertEqual(e["ok"], int(e["seq"] % 2 == 1))
def test_count_clamping(self):
for count, expected in ((0, 1), (-10, 1), (1, 1), (3, 3), (256, 256), (1000, 256)):
result = self.query(count)
self.assertEqual(len(result["entries"]), expected)
self.assertEqual(result["entries"][-1]["seq"], 300)
def test_empty_ring(self):
result = self.query(seeded=0)
self.assertEqual((result["total"], result["overwritten"], result["entries"]), (0, 0, []))
if __name__ == "__main__":
suite = unittest.defaultTestLoader.loadTestsFromTestCase(HostLaunchProtocol)
result = unittest.TextTestRunner().run(suite)
raise SystemExit(77 if result.skipped else 0 if result.wasSuccessful() else 1)
+277
View File
@@ -1,4 +1,6 @@
#include "mod_runtime.h"
#include "host_launch_timing.h"
#include "host_file_identity.h"
#include "mod_packages.h"
#include "mod_plugins.h"
#include "psx_sha256.h"
@@ -11,6 +13,8 @@
#include <array>
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <stdexcept>
#include <filesystem>
#include <fstream>
#include <iostream>
@@ -331,8 +335,77 @@ static std::string sha256_hex(const std::vector<uint8_t>& bytes) {
return out;
}
static uint64_t launch_timing_total() {
uint64_t total = 0;
host_launch_timing_snapshot(nullptr, 0, &total, nullptr);
return total;
}
static void check_commit_timing(uint64_t before, bool ok, bool hashed) {
HostLaunchTimingEvent events[HOST_LAUNCH_TIMING_CAPACITY]{};
const auto n = host_launch_timing_snapshot(events, HOST_LAUNCH_TIMING_CAPACITY,
nullptr, nullptr);
const HostLaunchTimingEvent* commit = nullptr;
for (uint32_t i = 0; i < n; ++i)
if (events[i].seq > before && events[i].stage == HOST_LAUNCH_COMMIT)
commit = &events[i];
check(commit && commit->ok == unsigned(ok), "real commit outcome retained by C snapshot");
if (!commit) return;
uint32_t stages = 0;
for (uint32_t i = 0; i < n; ++i) {
const auto& e = events[i];
if (e.seq <= before || e.parent_id != commit->scope_id) continue;
stages |= 1u << e.stage;
check(e.start_us >= commit->start_us &&
e.start_us + e.duration_us <= commit->start_us + commit->duration_us,
"real commit child measurement nested inside total");
}
check(bool(stages & (1u << HOST_LAUNCH_DISC_HASH)) == hashed,
"disc hash timing follows actual digest reuse");
const uint32_t required = (1u << HOST_LAUNCH_PREPARE_RESOURCES) |
(1u << HOST_LAUNCH_RESOLVE);
check((stages & required) == required, "real preparation and resolution observed");
if (ok) {
const uint32_t final = (1u << HOST_LAUNCH_OVERLAY_VERIFY) |
(1u << HOST_LAUNCH_DERIVED_DISC) | (1u << HOST_LAUNCH_SAVE_STATE) |
(1u << HOST_LAUNCH_BUILD_DISC_INDEX);
check((stages & final) == final, "real successful commit final stages observed");
} else {
check(!(stages & (1u << HOST_LAUNCH_BUILD_DISC_INDEX)),
"rejected commit does not invent final-stage completion");
}
}
extern "C" int psx_mod_counters_snapshot(const char**, uint64_t*, uint64_t*, int, uint64_t*);
static uint64_t startup_counter(const char* requested) {
const char* names[128]{}; uint64_t counts[128]{}, frames[128]{};
const int count = psx_mod_counters_snapshot(names, counts, frames, 128, nullptr);
for (int i = 0; i < count && i < 128; ++i)
if (std::string(names[i]) == requested) return counts[i];
return 0;
}
int main() {
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
const fs::path root = fs::temp_directory_path() / "psxrecomp-mod-runtime-cache-test";
const auto isolated_cache = root / "test-cache";
#if defined(_WIN32)
check(_wputenv_s(L"LOCALAPPDATA", isolated_cache.c_str()) == 0,
"isolate audited digest receipts from owner cache");
#else
check(setenv("XDG_CACHE_HOME", isolated_cache.c_str(), 1) == 0,
"isolate audited digest receipts from owner cache");
#endif
#if defined(_WIN32)
const char* configured_cache = std::getenv("LOCALAPPDATA");
#else
const char* configured_cache = std::getenv("XDG_CACHE_HOME");
#endif
check(configured_cache && fs::path(configured_cache) == isolated_cache,
"CRT getenv observes the isolated audited cache");
#else
const fs::path root = fs::temp_directory_path() / "psxrecomp-mod-runtime-test";
#endif
std::error_code ec;
fs::remove_all(root, ec);
const std::vector<uint8_t> stock(8 * 2352, 0);
@@ -568,8 +641,10 @@ int main() {
check(PSXRecompV4::mod_runtime_initialize(
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
error.c_str());
const auto first_commit_timing = launch_timing_total();
check(PSXRecompV4::mod_runtime_commit(cue_path, &error),
"CUE and its data-track BIN must have the same mod target identity");
check_commit_timing(first_commit_timing, true, true);
/* Committed but not yet activated: no hook may run. */
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u);
check(instruction_hits == 0 && !g_psx_mod_instruction_hooks, "instruction hooks await activation");
@@ -1205,7 +1280,13 @@ int main() {
check(PSXRecompV4::mod_runtime_initialize(loose_root, "READER", 0, {}, &error), "loose resource initialize");
check(PSXRecompV4::mod_runtime_commit(iso_path, &error), "unverified file remains usable offline");
check(PSXRecompV4::mod_runtime_plan_fingerprint_portable().empty(), "unverified resource has no online fingerprint");
const auto rejected_commit_timing = launch_timing_total();
check(!PSXRecompV4::mod_runtime_commit(iso_path, &error, false), "online commit rejects unverified resource");
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
check_commit_timing(rejected_commit_timing, false, true);
#else
check_commit_timing(rejected_commit_timing, false, false);
#endif
check(!PSXRecompV4::mod_runtime_commit_for_direct_netplay(iso_path, &error), "LAN also refuses unverified resources");
test_match_caps = {};
test_match_caps.valid = 1;
@@ -1469,6 +1550,202 @@ int main() {
check(!psx_mod_function_entry(&game_cpu, 0x80005000u) &&
game_netplay_filter_hits == 1,
"game filter must disarm when netplay stops");
// Reliable identity detects byte replacement even when size/mtime are restored.
{
const auto identity_path = root / "identity.bin";
write_bytes(identity_path, {1, 2, 3, 4});
const auto original_time = fs::last_write_time(identity_path);
std::string before, after;
const bool reliable = PSXRecompV4::host_file_identity(identity_path, before);
write_bytes(identity_path, {4, 3, 2, 1});
fs::last_write_time(identity_path, original_time);
check(!reliable || (PSXRecompV4::host_file_identity(identity_path, after) && before != after),
"file identity rejects same-size restored-mtime replacement");
check(!PSXRecompV4::host_file_identity(root, after), "directories are never file cache identities");
check(!PSXRecompV4::host_file_identity(root / "absent.bin", after), "missing identity is a cache miss");
}
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
{
const auto cached_root = root / "audited-cache";
const auto output_path = root / "audited-output.z64";
const auto input_path = root / "audited-input.bin";
const auto disc = root / "audited-disc.iso";
fs::copy_file(iso_path, disc, fs::copy_options::overwrite_existing);
write_bytes(input_path, {1,2,3,4});
std::string input_receipt, output_receipt;
int prepare_calls = 0;
bool throw_probe = false, mutate_on_resolve = false, throw_resolver = false;
const std::string manifest =
"format_version = 9\nid = \"cache.test\"\nversion = \"1.0.0\"\nname = \"Cache\"\nprepare = \"test.cache.prepare\"\nresolver = \"builtin:test.cache.resolve\"\n"
"[[target]]\ngame_id = \"READER\"\n"
"[[feature]]\nid = \"active\"\nname = \"Active\"\ndefault_enabled = true\n"
"[[resource]]\nfeature = \"active\"\nid = \"source\"\nlabel = \"Source\"\ninput_only = true\nshared_source = \"cache.original\"\nrequired = true\n"
"[[resource]]\nfeature = \"active\"\nid = \"rom\"\nlabel = \"ROM\"\nformat = \"n64-rom\"\nhidden = true\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n"
"[[feature]]\nid = \"second\"\nname = \"Second\"\ndefault_enabled = false\n"
"[[resource]]\nfeature = \"second\"\nid = \"rom\"\nlabel = \"ROM\"\nformat = \"n64-rom\"\nhidden = true\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n";
check(PSXRecompV4::mod_register_builtin_resolver("test.cache.resolve",
[&](const auto&, const auto&, const auto&, auto&, auto&) {
if (throw_resolver) throw std::runtime_error("resolver fixture exception");
if (mutate_on_resolve) write_bytes(input_path, {7,7,7,7});
return true;
}), "register resolver for mutation and unwind regression");
const auto manifest_path = cached_root / "packages/cache.test/1.0.0/manifest.toml";
write_text(manifest_path, manifest);
check(PSXRecompV4::mod_register_media_preparer("test.cache.prepare",
[&](const PSXRecompV4::ModPrepareContext&, std::map<std::string, fs::path>& outputs, std::string&) {
++prepare_calls;
write_bytes(output_path, rom);
PSXRecompV4::host_file_identity(input_path, input_receipt);
PSXRecompV4::host_file_identity(output_path, output_receipt);
outputs["rom"] = output_path;
return true;
},
[&](const PSXRecompV4::ModPrepareContext& context, std::map<std::string, fs::path>& outputs, std::string&) {
if (throw_probe) throw std::runtime_error("probe fixture exception");
std::string input, output;
if (context.feature_id == "second" ||
!PSXRecompV4::host_file_identity(input_path, input) || input != input_receipt ||
!PSXRecompV4::host_file_identity(output_path, output) || output != output_receipt) return false;
outputs["rom"] = output_path; return true;
}), "register audited preparation and cheap identity probe");
PSXRecompV4::ModPackageManager source_selection(cached_root);
check(source_selection.scan(&error) &&
source_selection.set_feature_resource_path("cache.test", "active", "source", input_path, &error) &&
source_selection.save_state(&error), "persist shared input selection for audited fixture");
PSXRecompV4::mod_runtime_end_netplay(); test_match_caps = {};
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error), "audited initialize");
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls == 0,
"cold cached-only launch does not prepare missing receipt");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == 1,
"cold launcher creates verified preparation");
const auto reused = startup_counter("startup.plan_reuse");
const auto hashed = startup_counter("startup.disc_hash");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) &&
PSXRecompV4::mod_runtime_commit(disc, &error) && prepare_calls == 1 &&
startup_counter("startup.plan_reuse") == reused + 2 && startup_counter("startup.disc_hash") == hashed,
"duplicate provider and main commit reuse a prepared plan without converter or hash");
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error) &&
PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls == 1 &&
startup_counter("startup.disc_hash") == hashed,
"warm initialize preloads receipts with no preparer or raw hash");
mod_runtime_activate_plugins();
check(PSXRecompV4::mod_runtime_commit(disc, &error) && prepare_calls == 2,
"activation invalidates the preboot ticket");
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error) &&
PSXRecompV4::mod_runtime_try_prepare_cached(disc), "restore warm plan for source invalidation");
const auto input_time = fs::last_write_time(input_path);
write_bytes(input_path, {4,3,2,1}); fs::last_write_time(input_path, input_time);
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc), "external shared input replacement invalidates title receipt");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == 3,
"changed source goes through full preparation");
auto corrupt_rom = rom; corrupt_rom[32] ^= 1; write_bytes(output_path, corrupt_rom);
// Even if the probe claims the file is current, the first warm resolve
// must verify output bytes against the declared SHA.
PSXRecompV4::host_file_identity(output_path, output_receipt);
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error) &&
!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls == 3,
"warm output corruption fails immutable-byte verification");
fs::remove(output_path);
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls == 3,
"missing output fails without invoking converter");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == 4,
"full preparation repairs missing output");
throw_probe = true;
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc), "throwing cached probe fails safely");
throw_probe = false;
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error), "cached flag restored after failure");
// Probe results are transactional across multiple enabled features.
PSXRecompV4::ModPackageManager transactional(cached_root);
check(transactional.scan(&error) && transactional.load_state(&error) &&
transactional.set_feature_enabled("cache.test", "second", true, &error) &&
transactional.set_feature_resource_path("cache.test", "active", "rom", rom_path, &error), "enable second probe fixture");
const auto original_output = transactional.feature_resource_path("cache.test", "active", "rom");
check(!transactional.prepare_resources("READER", disc, root / "cache", &error, true) &&
transactional.feature_resource_path("cache.test", "active", "rom") == original_output,
"later cache miss discards earlier pending output selections");
// A throw inside resolve must restore cached-only/provider modes.
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error), "invalidate ticket for resolver exception fixture");
throw_resolver = true;
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc),
"best-effort preload contains resolver exceptions before launcher UI");
throw_resolver = false;
fs::remove(output_path);
const auto before_unwind = prepare_calls;
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == before_unwind + 1,
"cached-only mode restored after resolve exception");
// A dependency changed during resolution must never publish readiness.
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error), "invalidate ticket for resolver mutation fixture");
const auto before_mutation = prepare_calls;
mutate_on_resolve = true;
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error), "full resolution can finish with concurrent input mutation");
mutate_on_resolve = false;
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == before_mutation + 2,
"unstable resolve never publishes a reusable ticket");
// Altering a syntactically valid digest must fail its receipt checksum.
std::filesystem::path cache_dir;
#if defined(_WIN32)
if (const char* local = std::getenv("LOCALAPPDATA")) cache_dir = local;
#else
if (const char* cache = std::getenv("XDG_CACHE_HOME")) cache_dir = cache;
else if (const char* home_dir = std::getenv("HOME")) cache_dir = fs::path(home_dir) / ".cache";
#endif
if (cache_dir.empty()) cache_dir = fs::temp_directory_path();
cache_dir /= "psxrecomp/imports/disc-digests-v2";
bool corrupted_receipt = false;
for (const auto& entry : fs::directory_iterator(cache_dir)) {
std::ifstream saved(entry.path(), std::ios::binary);
std::string receipt(std::istreambuf_iterator<char>(saved), {});
if (receipt.find(disc.generic_string()) == std::string::npos || receipt.size() < 130) continue;
const size_t digest_offset = receipt.size() - 130;
receipt[digest_offset] = receipt[digest_offset] == '0' ? '1' : '0';
write_text(entry.path(), receipt); corrupted_receipt = true;
}
check(corrupted_receipt && PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error) &&
!PSXRecompV4::mod_runtime_try_prepare_cached(disc), "corrupt well-formed digest receipt misses warm startup");
const auto before_corrupt_hash = startup_counter("startup.disc_hash");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) &&
startup_counter("startup.disc_hash") == before_corrupt_hash + 1,
"full launch repairs corrupted digest with authoritative source hash");
// Catalog changes cannot certify stale in-memory parsed manifests.
write_text(manifest_path, manifest + "\n# external catalog mutation\n");
check(!PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) &&
error.find("catalog changed") != std::string::npos,
"changed manifest requires rescan instead of resealing stale package");
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error), "explicit initialize rescans changed catalog");
const int before_netplay = prepare_calls;
const auto net_hash = startup_counter("startup.disc_hash");
check(PSXRecompV4::mod_runtime_commit_for_direct_netplay(disc, &error) &&
prepare_calls == before_netplay + 1 && startup_counter("startup.disc_hash") > net_hash,
"netplay ignores preboot ticket and hashes the source fully");
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc), "active netplay never preloads an offline ticket");
PSXRecompV4::mod_runtime_end_netplay();
// Legacy preparers have no cheap receipt and must never claim warm readiness.
const auto legacy_root = root / "legacy-cache";
int legacy_calls = 0;
check(PSXRecompV4::mod_register_media_preparer("test.legacy.prepare",
[&](const PSXRecompV4::ModPrepareContext&, std::map<std::string, fs::path>& outputs, std::string&) {
++legacy_calls; outputs["rom"] = output_path; return true;
}), "register legacy preparer without probe");
std::string legacy_manifest = manifest;
legacy_manifest.replace(legacy_manifest.find("test.cache.prepare"), std::string("test.cache.prepare").size(), "test.legacy.prepare");
write_text(legacy_root / "packages/cache.test/1.0.0/manifest.toml", legacy_manifest);
check(PSXRecompV4::mod_runtime_initialize(legacy_root, "READER", 0, {}, &error) &&
!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && legacy_calls == 0,
"legacy preparer always misses cached-only startup");
}
#else
{
const auto defaults = root / "default-hashing";
fs::create_directories(defaults);
check(PSXRecompV4::mod_runtime_initialize(defaults, "READER", 0, {}, &error), "default hashing fixture initialize");
const auto hash_count = startup_counter("startup.disc_hash");
check(PSXRecompV4::mod_runtime_commit(iso_path, &error) &&
PSXRecompV4::mod_runtime_commit(iso_path, &error) &&
startup_counter("startup.disc_hash") == hash_count + 1,
"default runtime preserves hash-only-on-selected-path-change behavior");
check(!PSXRecompV4::mod_runtime_try_prepare_cached(iso_path), "default runtime does not opt into cached startup");
}
#endif
fs::remove_all(root, ec);
if (failures) return 1;
std::cout << "mod runtime tests passed\n";
+33 -9
View File
@@ -1,10 +1,10 @@
"""Relative paths persisted in settings.toml and the bios.cfg / disc.cfg
sidecars resolve from the executable directory, not the working directory.
Compiles the real adapters from main.cpp (normalize_disc_path_for_launch,
resolve_persisted_disc_path, anchor_on_exe_dir) into a probe and runs it from
an unrelated working directory. Also checks that every persisted-path consumer
in main.cpp goes through an adapter. No game data is used.
Compiles the real adapters and sidecar writer/reader from main.cpp, plus
relative_to_folder from config_loader.cpp, into a probe and runs it from an
unrelated working directory. Checks save/reopen as well as path consumers.
No game data is used.
"""
import argparse
import os
@@ -46,6 +46,13 @@ def main():
'anchor_on_exe_dir'):
start = text.index('static std::filesystem::path ' + name + '(')
adapters.append(text[start:text.index('\n}\n', start) + 2])
config = (root.parent / 'recompiler/src/config_loader.cpp').read_text(encoding='utf-8')
start = config.index('fs::path relative_to_folder(')
relative_adapter = config[start:config.index('\n}\n', start) + 2]
for declaration in ('static std::filesystem::path read_cached_path(',
'static void write_cached_path('):
start = text.index(declaration)
adapters.append(text[start:text.index('\n}\n', start) + 2])
with tempfile.TemporaryDirectory(prefix='persisted paths spaces ') as tmp:
base = Path(tmp)
product, cwd = base / 'moved product', base / 'unrelated cwd'
@@ -58,22 +65,34 @@ def main():
# The probe's exe_dir_from_argv() stands in for the real one: the
# "argv0" it receives is the product directory itself.
source.write_text('''#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include "host_path.h"
#include "disc_path.h"
namespace fs = std::filesystem;
namespace PSXRecompV4 {
''' + relative_adapter + '''
}
static std::filesystem::path exe_dir_from_argv(const char* dir) {
return std::filesystem::path(dir);
}
static std::filesystem::path sidecar_cfg_path(const char* dir, const char* filename) {
return exe_dir_from_argv(dir) / filename;
}
''' + '\n'.join(adapters) + '''
int main(int argc, char** argv) {
(void)argc;
const std::string mode = argv[1];
const std::filesystem::path path = argv[2];
const std::filesystem::path resolved =
std::filesystem::path resolved =
mode == "persisted" ? resolve_persisted_disc_path(path, argv[3])
: mode == "anchor" ? anchor_on_exe_dir(argv[3], path)
: normalize_disc_path_for_launch(path);
if (mode == "sidecar") {
write_cached_path(argv[3], "disc.cfg", path);
resolved = read_cached_path(argv[3], "disc.cfg");
}
// libstdc++'s generic_string() collapses a UNC "\\\\server" root to "/server";
// print the native spelling with forward slashes so both C++ libraries agree.
std::string out = resolved.string();
@@ -109,17 +128,22 @@ int main(int argc, char** argv) {
check('persisted', '', '')
check('persisted', 'inputs/missing.chd', (product/'inputs/missing.chd').as_posix())
check('cli', relative, (cwd/relative).as_posix())
check('sidecar', product/relative, (product/relative).as_posix())
check('sidecar', '', '')
if os.name == 'nt':
check('persisted', r'\\server\share\Some Game [Disc 1].chd',
'//server/share/Some Game [Disc 1].chd')
for spelling in (r'\\server\share\Some Game [Disc 1].chd',
'//server/share/Some Game [Disc 1].chd'):
check('persisted', spelling, '//server/share/Some Game [Disc 1].chd')
check('sidecar', spelling, '//server/share/Some Game [Disc 1].chd')
# [bios] path, bios.cfg and [memcard] dir/card1/card2
check_same('anchor', '.', product)
check_same('anchor', 'cards/card1.mcd', product/'cards/card1.mcd')
check_same('anchor', 'bios/SCPH1001.BIN', product/'bios/SCPH1001.BIN')
check('anchor', product/'cards', (product/'cards').as_posix())
check('anchor', '', '')
print('persisted paths: disc (relative, absolute, empty, missing, CLI cwd, '
'Windows UNC) and BIOS/memory card (".", relative, absolute, empty) passed')
print('persisted paths: disc consumers and sidecar save/reopen '
'(relative, absolute, empty, missing, CLI cwd, Windows UNC) '
'and BIOS/memory card (".", relative, absolute, empty) passed')
if __name__ == '__main__':
+90 -2
View File
@@ -170,13 +170,17 @@ static void test_gen_flip_matches(void) {
}
static void test_budget_and_ema(void) {
CHECK(fabs(render_pass_budget(0, 0, 100.0, 0.5) - 50.0) < 1e-9,
"no history spends the share of the frame");
CHECK(render_pass_budget(0, 0, 100.0, 0.5) == 0.0,
"zero measured credit spends nothing, including startup");
CHECK(fabs(render_pass_budget(20.0, 10.0, 100.0, 0.8) - 24.0) < 1e-9,
"idle plus pass time, scaled");
CHECK(render_pass_budget(500.0, 0.0, 100.0, 0.8) == 100.0,
"never more than a frame");
CHECK(render_pass_budget(10.0, 10.0, 0.0, 0.8) == 0.0, "no frame, no budget");
CHECK(render_pass_budget(20.0, 10.0, 100.0, 2.0) == 30.0,
"share above one remains clamped");
CHECK(render_pass_budget(20.0, 10.0, 100.0, 0.0) == 0.0,
"zero share spends nothing");
CHECK(render_pass_ema(0.0, 4.0) == 4.0, "first sample seeds the average");
CHECK(fabs(render_pass_ema(4.0, 8.0) - 5.0) < 1e-9, "quarter-weight update");
CHECK(render_pass_ema(4.0, -1.0) == 4.0, "bad samples are ignored");
@@ -292,6 +296,89 @@ static void test_budget_and_ema(void) {
}
}
static void test_repeated_zero_credit_plans(void) {
RenderPassPlanInput in = {0};
RenderPassCost cost = {0};
uint32_t phases[RENDER_PASS_MAX_PHASES], wanted;
unsigned rewarms = 0;
double previous_pass = 0.0;
/* Millisecond host ticks: a 50 ms frame, 30 ms warmed replay and 80% share.
* Keep a real output grid inside each frame. With the former both-zero
* fallback, these calls alternated budget40/admit1 and budget24/admit0,
* each measured replay preventing a cost rewarm. */
in.frame_length = 50.0;
in.target_period = 1000.0 / 165.0;
in.max = 1;
for (unsigned i = 0; i < RENDER_PASS_COST_WARMUP; i++)
render_pass_cost_add(&cost, 30.0, 0);
for (unsigned i = 0; i < 20; i++) {
uint32_t n;
in.frame_start = 1000.0 + i * in.frame_length;
in.next_deadline = in.frame_start + in.target_period;
in.pass_cost = render_pass_cost_estimate(&cost);
in.budget = render_pass_budget(0.0, previous_pass, in.frame_length, 0.8);
n = render_pass_plan_phases(&in, phases, &wanted);
if (wanted && render_pass_cost_note_plan(&cost)) {
rewarms++;
in.pass_cost = 0.0;
n = render_pass_plan_phases(&in, phases, NULL);
}
CHECK(wanted > 0 && n == 0, "repeated zero credit never reinvents nominal budget");
previous_pass = 0.0;
if (n) {
render_pass_cost_add(&cost, 30.0, 0);
previous_pass = 30.0;
}
}
CHECK(rewarms == 0, "the zero-credit regression is independent of rewarming");
/* A later stale-estimate rewarm still cannot buy its measuring pass from
* zero credit. The cost stays unknown until measured idle returns. */
for (unsigned i = 20; i < 80; i++) {
uint32_t n;
in.frame_start += in.frame_length;
in.next_deadline = in.frame_start + in.target_period;
in.pass_cost = render_pass_cost_estimate(&cost);
in.budget = render_pass_budget(0.0, 0.0, in.frame_length, 0.8);
n = render_pass_plan_phases(&in, phases, &wanted);
if (wanted && render_pass_cost_note_plan(&cost)) {
rewarms++;
in.pass_cost = 0.0;
n = render_pass_plan_phases(&in, phases, NULL);
}
CHECK(n == 0, "zero-credit rewarm does not fund a measuring pass");
}
CHECK(rewarms == 1, "one rewarm stays unknown until actual credit returns");
/* When genuine idle returns, the same unknown cost left by that rewarm
* seeds normally and keeps replaying; do not reset the estimator here. */
for (unsigned i = 0; i < RENDER_PASS_COST_WARMUP + 2u; i++) {
uint32_t n;
in.frame_start += in.frame_length;
in.next_deadline = in.frame_start + in.target_period;
in.pass_cost = render_pass_cost_estimate(&cost);
in.budget = render_pass_budget(40.0, 0.0, 50.0, 0.8);
n = render_pass_plan_phases(&in, phases, &wanted);
CHECK(n == 1 && wanted > 0, "positive measured idle warms and admits replay");
CHECK(!render_pass_cost_note_plan(&cost), "measured affordable replay never rewarms");
if (n) render_pass_cost_add(&cost, 30.0, 0);
}
CHECK(render_pass_cost_estimate(&cost) == 30.0, "genuine idle seeds the measured cost");
/* Ordinary previous replay credit is still real input: 30*.8 cannot fund
* another 30 ms replay, but it can fund a measured 10 ms replay. */
in.pass_cost = render_pass_cost_estimate(&cost);
in.budget = render_pass_budget(0.0, 30.0, 50.0, 0.8);
CHECK(in.budget == 24.0 && render_pass_plan_phases(&in, phases, NULL) == 0,
"prior pass credit is retained without reinvention after shedding");
memset(&cost, 0, sizeof cost);
for (unsigned i = 0; i < RENDER_PASS_COST_WARMUP; i++)
render_pass_cost_add(&cost, 10.0, 0);
in.pass_cost = render_pass_cost_estimate(&cost);
CHECK(render_pass_plan_phases(&in, phases, NULL) == 1,
"ordinary prior credit still funds an affordable replay");
}
static void test_store_policy(void) {
CHECK(render_pass_mmio_class(0x1F801810u, 0x28000000u, 4) == -1, "GP0 reaches the GPU");
CHECK(render_pass_mmio_class(0x1F801814u, 0x04000002u, 4) == -1, "GP1 DMA mode allowed");
@@ -335,6 +422,7 @@ int main(void) {
test_gen_select_late();
test_gen_flip_matches();
test_budget_and_ema();
test_repeated_zero_credit_plans();
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}