mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
VR: report XR readiness without the debug server
This commit is contained in:
@@ -92,3 +92,27 @@ game-specific and must be calibrated independently. This API also works without
|
||||
a headset backend; it does not enable a headset mode by default.
|
||||
|
||||
The optional headset backend is described in [OPENXR_RENDERING.md](OPENXR_RENDERING.md).
|
||||
|
||||
|
||||
## Production startup and performance receipts
|
||||
|
||||
`PSX_OPENXR_READY_FILE=<fresh-path>` requests one JSON startup receipt, including
|
||||
in builds with `PSX_DEBUG_TOOLS=OFF`. Main publishes through a same-directory
|
||||
temporary and atomic replacement, so partial JSON is never visible. An existing
|
||||
destination is refused at startup; the caller owns a fresh path and removes it
|
||||
afterward. Schema 2 records XR session generations, counters and the savestate
|
||||
receipt. When `PSX_LOAD_SLOT` is set, success requires that load to settle
|
||||
successfully and a later submission from the current XR session. A failed load
|
||||
publishes `ready:false` with `state_load_failed`; session re-init publishes a
|
||||
new receipt after its first current-session submission. An invalid, refused, or
|
||||
netplay-disabled load request also publishes `ready:false` with a specific error
|
||||
instead of being treated as if no load were requested. A game-session reboot
|
||||
starts a fresh load lifecycle, so an earlier success cannot authorize the new
|
||||
session; the newly staged `PSX_LOAD_SLOT` must settle before readiness can be
|
||||
published. Without the variable the reporter immediately becomes a no-op.
|
||||
|
||||
`PSX_RUNTIME_PERF_DIAG=1` also prints VR pair/shed/empty/failure totals, the pair
|
||||
EMA/last milliseconds and rollback verification totals at its existing configured
|
||||
interval (`PSX_RUNTIME_PERF_DIAG_MS`). This exposes production behavior without
|
||||
per-instruction debug tracing. The pair average is an EMA, not a percentile.
|
||||
`PSX_FRAME_REPORT_MS` remains the production guest-frame cadence measurement.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Report XR readiness without the debug server
|
||||
|
||||
Source: [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp).
|
||||
- [5932092c](https://github.com/FractalEngineer/psxrecomp/commit/5932092cdf20aaa707e286dbd840c63b1f0d0598)
|
||||
|
||||
Original author: Yves <calibratedbeats@gmail.com>; retained as commit author
|
||||
and Co-authored-by on this reconstructed port.
|
||||
|
||||
Adapted: this feature and its tests. The readiness receipt uses same-directory
|
||||
temporary publication, refuses an existing destination, records XR session
|
||||
generations, and requires a submission from the current session after a
|
||||
requested load succeeds. Each game-session reboot starts a fresh load lifecycle;
|
||||
an earlier success cannot authorize the rebooted session. A failed, invalid, or
|
||||
refused load publishes a non-ready receipt without exposing partial JSON.
|
||||
Excluded: other VR features, game policy, assets and chronological
|
||||
investigation notes.
|
||||
Base: `fef7a1b6e3e1be7511eb82fa3b512258beacba04` (current upstream master or the preceding feature).
|
||||
|
||||
Validation: Windows/GCC 13.2 Release/OpenGL/OpenXR/PGXP MoH build passes with
|
||||
debug tools OFF and existing UI. Atomic replacement is covered by a standalone
|
||||
test; the readiness state-machine fixture covers success, XR re-init, reboot
|
||||
with an old success, pending and failed reboot loads, recovery, and refused
|
||||
requests. Fresh-boot headless readiness and missing-slot refusal pass. The older
|
||||
slot-2 fixture failed to load on current upstream; no compatibility guard/header
|
||||
was changed.
|
||||
|
||||
Consumer: [Medal of Honor v0.1.6](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/1957e28a5dc7f5daa79e2b9b6ed867eb06b41ace).
|
||||
Requires a framework pin bump and rebuild; no game/BIOS regeneration.
|
||||
@@ -50,6 +50,15 @@ if(BUILD_TESTING)
|
||||
add_test(NAME debug_socket_shutdown_test COMMAND debug_socket_shutdown_test)
|
||||
set_tests_properties(debug_socket_shutdown_test PROPERTIES TIMEOUT 15)
|
||||
|
||||
add_executable(atomic_file_write_test tests/test_atomic_file_write.cpp)
|
||||
target_include_directories(atomic_file_write_test PRIVATE include)
|
||||
target_compile_features(atomic_file_write_test PRIVATE cxx_std_17)
|
||||
add_test(NAME atomic_file_write_test COMMAND atomic_file_write_test)
|
||||
|
||||
add_executable(openxr_ready_state_test tests/test_openxr_ready_state.c)
|
||||
target_include_directories(openxr_ready_state_test PRIVATE include)
|
||||
add_test(NAME openxr_ready_state_test COMMAND openxr_ready_state_test)
|
||||
|
||||
add_executable(video_texture_filter_test tests/test_video_texture_filter.c src/video_texture_filter.c)
|
||||
target_include_directories(video_texture_filter_test PRIVATE include)
|
||||
add_test(NAME video_texture_filter_test COMMAND video_texture_filter_test)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace psx {
|
||||
|
||||
/* Publish a complete file by writing a same-directory temporary and replacing
|
||||
* the destination atomically. The destination is never observed half-written. */
|
||||
inline bool atomic_write_file(const std::filesystem::path& target,
|
||||
std::string_view data,
|
||||
std::string* error = nullptr) {
|
||||
if (target.empty() || target.filename().empty()) {
|
||||
if (error) *error = "destination has no filename";
|
||||
return false;
|
||||
}
|
||||
auto temporary = target;
|
||||
#ifdef _WIN32
|
||||
temporary += "." + std::to_string(GetCurrentProcessId()) + ".tmp";
|
||||
#else
|
||||
temporary += "." + std::to_string(getpid()) + ".tmp";
|
||||
#endif
|
||||
{
|
||||
std::ofstream out(temporary, std::ios::binary | std::ios::trunc);
|
||||
out.write(data.data(), static_cast<std::streamsize>(data.size()));
|
||||
out.flush();
|
||||
out.close();
|
||||
if (!out) {
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(temporary, ignored);
|
||||
if (error) *error = "temporary write failed";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (!MoveFileExW(temporary.c_str(), target.c_str(),
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(temporary, ignored);
|
||||
if (error) *error = "atomic replace failed";
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
std::error_code ec;
|
||||
std::filesystem::rename(temporary, target, ec);
|
||||
if (ec) {
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(temporary, ignored);
|
||||
if (error) *error = ec.message();
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace psx
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct PSXOpenXRReadyLoadRequest {
|
||||
uint64_t sequence;
|
||||
uint64_t execution_generation;
|
||||
uint32_t status_generation_before;
|
||||
int slot;
|
||||
const char *error;
|
||||
} PSXOpenXRReadyLoadRequest;
|
||||
|
||||
typedef struct PSXOpenXRReadyLoadStatus {
|
||||
uint32_t generation;
|
||||
int pending;
|
||||
int last_ok;
|
||||
int last_load;
|
||||
int last_slot;
|
||||
} PSXOpenXRReadyLoadStatus;
|
||||
|
||||
typedef struct PSXOpenXRReadyLoadState {
|
||||
uint64_t execution_generation;
|
||||
uint64_t request_sequence;
|
||||
uint32_t generation_floor;
|
||||
int expected;
|
||||
int request_seen;
|
||||
int resolved;
|
||||
int last_ok;
|
||||
int slot;
|
||||
const char *error;
|
||||
} PSXOpenXRReadyLoadState;
|
||||
|
||||
static inline void psx_openxr_ready_load_begin_execution(
|
||||
PSXOpenXRReadyLoadState *state, uint64_t execution_generation,
|
||||
int expected, int slot) {
|
||||
if (!state) return;
|
||||
state->execution_generation = execution_generation;
|
||||
state->request_sequence = 0;
|
||||
state->generation_floor = 0;
|
||||
state->expected = !!expected;
|
||||
state->request_seen = 0;
|
||||
state->resolved = !state->expected;
|
||||
state->last_ok = 0;
|
||||
state->slot = slot;
|
||||
state->error = 0;
|
||||
}
|
||||
|
||||
/* Returns nonzero when this is a new request for the current execution. */
|
||||
static inline int psx_openxr_ready_load_observe_request(
|
||||
PSXOpenXRReadyLoadState *state,
|
||||
const PSXOpenXRReadyLoadRequest *request) {
|
||||
if (!state || !request || !state->expected ||
|
||||
request->execution_generation != state->execution_generation ||
|
||||
request->sequence == 0 || request->sequence == state->request_sequence)
|
||||
return 0;
|
||||
state->request_sequence = request->sequence;
|
||||
state->request_seen = 1;
|
||||
state->generation_floor = request->status_generation_before;
|
||||
state->slot = request->slot;
|
||||
state->error = request->error;
|
||||
state->resolved = request->error != 0;
|
||||
state->last_ok = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Only a settled load after the request-generation floor can resolve success. */
|
||||
static inline int psx_openxr_ready_load_observe_status(
|
||||
PSXOpenXRReadyLoadState *state,
|
||||
const PSXOpenXRReadyLoadStatus *status) {
|
||||
if (!state || !status || !state->expected || !state->request_seen ||
|
||||
state->resolved ||
|
||||
status->pending || status->generation <= state->generation_floor ||
|
||||
!status->last_load || status->last_slot != state->slot)
|
||||
return 0;
|
||||
state->resolved = 1;
|
||||
state->last_ok = status->last_ok;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int psx_openxr_ready_load_ok(
|
||||
const PSXOpenXRReadyLoadState *state) {
|
||||
return state && (!state->expected || (state->resolved && state->last_ok));
|
||||
}
|
||||
|
||||
static inline int psx_openxr_ready_load_failed(
|
||||
const PSXOpenXRReadyLoadState *state) {
|
||||
return state && state->expected && state->resolved && !state->last_ok;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ typedef struct PSXOpenXRStats {
|
||||
uint64_t view_flags;
|
||||
const char *stage;
|
||||
uint64_t waits, submitted, empty, failures, predicted_time;
|
||||
uint64_t session_generation, submitted_session_generation;
|
||||
float ipd_m;
|
||||
float origin_up_dot;
|
||||
uint32_t origin_inversion_streak, origin_ambiguous;
|
||||
|
||||
@@ -128,6 +128,11 @@ int savestate_pending(void);
|
||||
|
||||
/* Machine-readable lifecycle receipt for deterministic debug harnesses. */
|
||||
void savestate_status_json(char* buf, size_t cap);
|
||||
typedef struct PSXSavestateStatus {
|
||||
uint32_t generation;
|
||||
int pending, last_ok, last_load, last_slot;
|
||||
} PSXSavestateStatus;
|
||||
void savestate_get_status(PSXSavestateStatus* out);
|
||||
|
||||
/* 1 once after a successful load restore (before scheduler longjmp). Clears. */
|
||||
int savestate_take_load_completed(void);
|
||||
|
||||
@@ -9088,6 +9088,7 @@ static void handle_openxr_stats(int id, const char *json) {
|
||||
"\"frame_open\":%u,\"result\":%d,\"stage\":\"%s\",\"runtime\":\"%s\","
|
||||
"\"last_failure\":\"%s\",\"last_failure_result\":%d,\"view_flags\":%llu,"
|
||||
"\"waits\":%llu,\"submitted\":%llu,\"empty\":%llu,\"failures\":%llu,"
|
||||
"\"session_generation\":%llu,\"submitted_session_generation\":%llu,"
|
||||
"\"predicted_time\":%llu,\"ipd_m\":%.6f,\"origin_up_dot\":%.6f,"
|
||||
"\"origin_inversion_streak\":%u,\"origin_ambiguous\":%u,\"units_per_meter\":%.6f,"
|
||||
"\"gl_version\":%llu,\"min_gl_version\":%llu,\"max_gl_version\":%llu,\"swapchain_format\":%lld,"
|
||||
@@ -9098,8 +9099,11 @@ static void handle_openxr_stats(int id, const char *json) {
|
||||
s.initialized,s.running,s.state,s.tracking,s.frame_open,s.result,
|
||||
s.stage?s.stage:"off",s.runtime,s.last_failure?s.last_failure:"",s.last_failure_result,
|
||||
(unsigned long long)s.view_flags,(unsigned long long)s.waits,
|
||||
(unsigned long long)s.submitted,(unsigned long long)s.empty,
|
||||
(unsigned long long)s.failures,(unsigned long long)s.predicted_time,s.ipd_m,
|
||||
(unsigned long long)s.submitted,(unsigned long long)s.empty,
|
||||
(unsigned long long)s.failures,
|
||||
(unsigned long long)s.session_generation,
|
||||
(unsigned long long)s.submitted_session_generation,
|
||||
(unsigned long long)s.predicted_time,s.ipd_m,
|
||||
s.origin_up_dot,s.origin_inversion_streak,s.origin_ambiguous,s.units_per_meter,
|
||||
(unsigned long long)s.gl_version,(unsigned long long)s.min_gl_version,
|
||||
(unsigned long long)s.max_gl_version,(long long)s.swapchain_format,s.submitted_layer,
|
||||
|
||||
+182
-4
@@ -34,6 +34,9 @@
|
||||
#include "starvation_ring.h"
|
||||
#include "load_accel.h"
|
||||
#include "savestate.h"
|
||||
#include "psx_openxr.h"
|
||||
#include "atomic_file_write.h"
|
||||
#include "openxr_ready_state.h"
|
||||
#include "psx_rewind.h"
|
||||
#include "psx_savestate_menu.h"
|
||||
#include "host_osd.h"
|
||||
@@ -4827,6 +4830,145 @@ static void runtime_perf_section_end(uint64_t start, uint64_t *total) {
|
||||
* waits on the render queue), the render thread's busy share, the dynamic
|
||||
* resolution level and Smooth motion's generated / real presents per second. */
|
||||
static uint64_t g_frame_report_wait_ticks = 0; /* offline pacer waits */
|
||||
static uint64_t g_openxr_ready_execution_generation = 0;
|
||||
static PSXOpenXRReadyLoadRequest g_openxr_ready_load_request{};
|
||||
|
||||
static void openxr_ready_record_load_request(int slot, const char *error) {
|
||||
PSXSavestateStatus status{};
|
||||
savestate_get_status(&status);
|
||||
g_openxr_ready_load_request.sequence++;
|
||||
if (g_openxr_ready_load_request.sequence == 0)
|
||||
g_openxr_ready_load_request.sequence = 1;
|
||||
g_openxr_ready_load_request.execution_generation =
|
||||
g_openxr_ready_execution_generation;
|
||||
g_openxr_ready_load_request.status_generation_before = status.generation;
|
||||
g_openxr_ready_load_request.slot = slot;
|
||||
g_openxr_ready_load_request.error = error;
|
||||
}
|
||||
|
||||
/* One opt-in startup receipt per XR session, including production builds without
|
||||
* debug TCP. The destination must not exist at process start. */
|
||||
static void openxr_ready_report_tick(uint64_t frames) {
|
||||
static bool initialized = false;
|
||||
static bool requested = false;
|
||||
static PSXOpenXRReadyLoadState load_state{};
|
||||
static uint64_t observed_execution = 0;
|
||||
static uint64_t observed_session = 0, submission_floor = 0;
|
||||
static bool published_for_session = false;
|
||||
static uint64_t retry_at_ms = 0;
|
||||
static std::filesystem::path path;
|
||||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
const char *env_path = std::getenv("PSX_OPENXR_READY_FILE");
|
||||
if (!env_path || !*env_path) return;
|
||||
path = env_path;
|
||||
std::error_code ec;
|
||||
const bool present = std::filesystem::exists(path, ec);
|
||||
if (ec || present) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: refusing %s OpenXR readiness path '%s'\n",
|
||||
ec ? "unreadable" : "existing", path.string().c_str());
|
||||
return;
|
||||
}
|
||||
requested = true;
|
||||
const char *load = std::getenv("PSX_LOAD_SLOT");
|
||||
const bool load_expected = load && *load;
|
||||
int load_slot = -1;
|
||||
if (load_expected) {
|
||||
char *end = nullptr;
|
||||
const long parsed = std::strtol(load, &end, 10);
|
||||
if (end && *end == '\0' && parsed >= 0 && parsed < SAVESTATE_SLOTS)
|
||||
load_slot = static_cast<int>(parsed);
|
||||
}
|
||||
psx_openxr_ready_load_begin_execution(
|
||||
&load_state, g_openxr_ready_execution_generation,
|
||||
load_expected, load_slot);
|
||||
if (load_expected && load_slot < 0)
|
||||
openxr_ready_record_load_request(
|
||||
-1, "state_load_request_invalid");
|
||||
observed_execution = g_openxr_ready_execution_generation;
|
||||
}
|
||||
if (!requested) return;
|
||||
|
||||
bool execution_changed =
|
||||
observed_execution != g_openxr_ready_execution_generation;
|
||||
if (execution_changed) {
|
||||
psx_openxr_ready_load_begin_execution(
|
||||
&load_state, g_openxr_ready_execution_generation,
|
||||
load_state.expected, load_state.slot);
|
||||
observed_execution = g_openxr_ready_execution_generation;
|
||||
}
|
||||
psx_openxr_ready_load_observe_request(
|
||||
&load_state, &g_openxr_ready_load_request);
|
||||
|
||||
PSXOpenXRStats xr{};
|
||||
psx_openxr_stats(&xr);
|
||||
if (execution_changed) {
|
||||
submission_floor = xr.submitted;
|
||||
published_for_session = false;
|
||||
}
|
||||
if (xr.session_generation && xr.session_generation != observed_session) {
|
||||
if (observed_session != 0) {
|
||||
psx_openxr_ready_load_begin_execution(
|
||||
&load_state, g_openxr_ready_execution_generation,
|
||||
load_state.expected, load_state.slot);
|
||||
psx_openxr_ready_load_observe_request(
|
||||
&load_state, &g_openxr_ready_load_request);
|
||||
}
|
||||
observed_session = xr.session_generation;
|
||||
submission_floor = xr.submitted;
|
||||
published_for_session = false;
|
||||
}
|
||||
|
||||
PSXSavestateStatus current_state{};
|
||||
savestate_get_status(¤t_state);
|
||||
PSXOpenXRReadyLoadStatus load_status = {
|
||||
current_state.generation, current_state.pending, current_state.last_ok,
|
||||
current_state.last_load, current_state.last_slot};
|
||||
if (psx_openxr_ready_load_observe_status(&load_state, &load_status))
|
||||
submission_floor = xr.submitted;
|
||||
|
||||
const uint64_t now = SDL_GetTicks();
|
||||
if (now < retry_at_ms || published_for_session) return;
|
||||
|
||||
const bool load_ok = psx_openxr_ready_load_ok(&load_state);
|
||||
const bool fresh_submission =
|
||||
xr.session_generation != 0 &&
|
||||
xr.submitted_session_generation == xr.session_generation &&
|
||||
xr.submitted > submission_floor;
|
||||
const bool load_failed = psx_openxr_ready_load_failed(&load_state);
|
||||
if (!load_failed && (!load_ok || !xr.enabled || !xr.running || !fresh_submission))
|
||||
return;
|
||||
|
||||
char state[512];
|
||||
savestate_status_json(state, sizeof state);
|
||||
char receipt[2048];
|
||||
std::snprintf(receipt, sizeof receipt,
|
||||
"{\"schema\":2,\"ready\":%d,\"error\":\"%s\",\"frame\":%llu,"
|
||||
"\"openxr\":{\"session_generation\":%llu,\"submitted_session_generation\":%llu,"
|
||||
"\"running\":%u,\"tracking\":%u,\"submitted\":%llu,"
|
||||
"\"empty\":%llu,\"failures\":%llu},\"savestate\":{%s}}\n",
|
||||
load_failed ? 0 : 1,
|
||||
load_failed
|
||||
? (load_state.error ? load_state.error : "state_load_failed")
|
||||
: "",
|
||||
(unsigned long long)frames,
|
||||
(unsigned long long)xr.session_generation,
|
||||
(unsigned long long)xr.submitted_session_generation,
|
||||
xr.running, xr.tracking, (unsigned long long)xr.submitted,
|
||||
(unsigned long long)xr.empty, (unsigned long long)xr.failures, state);
|
||||
std::string error;
|
||||
if (psx::atomic_write_file(path, receipt, &error)) {
|
||||
published_for_session = true;
|
||||
} else {
|
||||
retry_at_ms = now + 1000;
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: cannot publish OpenXR startup receipt '%s': %s\n",
|
||||
path.string().c_str(), error.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static void frame_report_tick(uint64_t frames) {
|
||||
static int interval_ms = -1;
|
||||
static uint64_t first_ticks = 0, last_ticks = 0, last_frames = 0;
|
||||
@@ -4970,6 +5112,22 @@ static void runtime_perf_diag_tick() {
|
||||
current.capture_triggers - last.capture_triggers,
|
||||
current.capture_overlays - last.capture_overlays,
|
||||
(unsigned long long)current.capture_last_dispatch_delta);
|
||||
PSXOpenXRStats xr{};
|
||||
psx_openxr_stats(&xr);
|
||||
if (xr.enabled) {
|
||||
RenderStereoStats stereo{};
|
||||
RenderPassStats passes{};
|
||||
render_stereo_get_stats(&stereo);
|
||||
render_pass_get_stats(&passes);
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: VR pairs=%llu shed=%llu empty=%llu failures=%llu "
|
||||
"avg_pair_ms=%.3f last_pair_ms=%.3f verify=%llu mismatch=%llu\n",
|
||||
(unsigned long long)stereo.pairs, (unsigned long long)stereo.shed,
|
||||
(unsigned long long)xr.empty, (unsigned long long)xr.failures,
|
||||
stereo.avg_pair_ms, stereo.last_pair_ms,
|
||||
(unsigned long long)passes.verify_checks,
|
||||
(unsigned long long)passes.verify_mismatch);
|
||||
}
|
||||
std::fflush(stdout);
|
||||
last = current;
|
||||
last_spu = audio.tap_frames[AUDIO_TAP_SPU_OUT];
|
||||
@@ -9062,6 +9220,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
{
|
||||
/* Outside every debug guard on purpose: production must be measurable. */
|
||||
extern uint64_t s_frame_count;
|
||||
openxr_ready_report_tick(s_frame_count);
|
||||
frame_report_tick(s_frame_count);
|
||||
}
|
||||
runtime_perf_frame_begin();
|
||||
@@ -20352,6 +20511,9 @@ session_reboot:
|
||||
video_res_set_applied(PSX_IR_UNSET, 1000);
|
||||
g_video_res_live_failed.store(false);
|
||||
g_video_res_live_capable.store(false);
|
||||
g_openxr_ready_execution_generation++;
|
||||
if (g_openxr_ready_execution_generation == 0)
|
||||
g_openxr_ready_execution_generation = 1;
|
||||
mod_runtime_set_mounted_disc_resolver(nullptr, nullptr);
|
||||
psx_disc_tray_reset_session();
|
||||
/* Rematch after lobby soft-return re-enters here with updated net_cfg. */
|
||||
@@ -21636,17 +21798,33 @@ session_reboot:
|
||||
psx_rewind_set_depth((uint32_t)g_rewind_depth);
|
||||
psx_rewind_set_interval((uint32_t)g_rewind_interval);
|
||||
psx_rewind_configure(memory_get_bios_checksum(), game_entry_pc);
|
||||
/* Headless / agent load: PSX_LOAD_SLOT=N stages slot load (0..11)
|
||||
/* Headless load: PSX_LOAD_SLOT=N stages slot load (0..11)
|
||||
* at the next safe block boundary after boot. */
|
||||
if (const char *ls = std::getenv("PSX_LOAD_SLOT")) {
|
||||
int slot = atoi(ls);
|
||||
if (slot >= 0 && slot < 12) {
|
||||
if (const char *ls = std::getenv("PSX_LOAD_SLOT"); ls && *ls) {
|
||||
char *end = nullptr;
|
||||
const long parsed = std::strtol(ls, &end, 10);
|
||||
if (!end || *end != '\0' || parsed < 0 || parsed >= SAVESTATE_SLOTS) {
|
||||
openxr_ready_record_load_request(
|
||||
-1, "state_load_request_invalid");
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: PSX_LOAD_SLOT=%s ignored (expected 0..%d)\n",
|
||||
ls, SAVESTATE_SLOTS - 1);
|
||||
} else {
|
||||
const int slot = static_cast<int>(parsed);
|
||||
if (psx_netplay_active()) {
|
||||
openxr_ready_record_load_request(
|
||||
slot, "state_load_unavailable_netplay");
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: PSX_LOAD_SLOT=%d ignored (use the save-state menu after netplay starts)\n",
|
||||
slot);
|
||||
} else if (savestate_request_load(slot)) {
|
||||
openxr_ready_record_load_request(slot, nullptr);
|
||||
std::fprintf(stdout, "psxrecomp: PSX_LOAD_SLOT=%d staged\n", slot);
|
||||
} else {
|
||||
openxr_ready_record_load_request(
|
||||
slot, "state_load_request_refused");
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: PSX_LOAD_SLOT=%d could not be staged\n", slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,6 +303,7 @@ static int initialize(void) {
|
||||
if(!binding.hDC || !binding.hGLRC){s_stats.stage="opengl_context";return 0;}
|
||||
XrSessionCreateInfo sci={XR_TYPE_SESSION_CREATE_INFO};sci.next=&binding;sci.systemId=s_system;
|
||||
if(!check(xrCreateSession(s_instance,&sci,&s_session),"session"))return 0;
|
||||
if(++s_stats.session_generation==0)s_stats.session_generation=1;
|
||||
if(!input_initialize())return 0;
|
||||
XrReferenceSpaceCreateInfo space={XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
||||
space.referenceSpaceType=XR_REFERENCE_SPACE_TYPE_LOCAL;space.poseInReferenceSpace.orientation.w=1;
|
||||
@@ -798,6 +799,7 @@ int psx_openxr_end(int keep,PSXOpenXRCopy copy) {
|
||||
xr_trace_us(gl_renderer_perf_ticks()-s_pose_sample_ticks));
|
||||
psx_frame_trace_xr_cadence(s_display_delta_us, complete);
|
||||
if(ok && complete) {
|
||||
s_stats.submitted_session_generation=s_stats.session_generation;
|
||||
s_stats.submitted_surfaces=quad?0:((s_surface_mask&1)!=0)+((s_surface_mask&2)!=0);
|
||||
s_stats.submitted++;s_stats.submitted_pair_id=s_pair_id;
|
||||
s_stats.submitted_guest_cycle=s_pair_cycle;s_stats.submitted_predicted_time=(uint64_t)s_time;
|
||||
|
||||
@@ -873,6 +873,15 @@ int savestate_pending(void) {
|
||||
return (s_save_pending >= 0 || s_load_pending >= 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
void savestate_get_status(PSXSavestateStatus* out) {
|
||||
if (!out) return;
|
||||
out->generation = s_status_generation;
|
||||
out->pending = s_status_pending;
|
||||
out->last_ok = s_status_last_ok;
|
||||
out->last_load = s_status_last_load;
|
||||
out->last_slot = s_status_last_slot;
|
||||
}
|
||||
|
||||
void savestate_status_json(char* buf, size_t cap) {
|
||||
if (!buf || cap == 0) return;
|
||||
snprintf(buf, cap,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#include "atomic_file_write.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
static bool read_file(const std::filesystem::path& path, std::string* out) {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in) return false;
|
||||
out->assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
||||
return bool(in);
|
||||
}
|
||||
|
||||
int main() {
|
||||
const auto nonce = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
const auto root = std::filesystem::temp_directory_path() /
|
||||
("psx-atomic-write-" + std::to_string(nonce));
|
||||
std::filesystem::create_directories(root);
|
||||
const auto target = root / "receipt.json";
|
||||
{
|
||||
std::ofstream old(target, std::ios::binary);
|
||||
old << "old";
|
||||
}
|
||||
|
||||
std::string error;
|
||||
if (!psx::atomic_write_file(target, "{\"complete\":true}\n", &error)) {
|
||||
std::fprintf(stderr, "atomic write failed: %s\n", error.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::string got;
|
||||
if (!read_file(target, &got) || got != "{\"complete\":true}\n") {
|
||||
std::fprintf(stderr, "destination was not atomically replaced\n");
|
||||
return 1;
|
||||
}
|
||||
for (const auto& entry : std::filesystem::directory_iterator(root)) {
|
||||
if (entry.path() != target) {
|
||||
std::fprintf(stderr, "temporary file leaked\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(root, ec);
|
||||
return ec ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "openxr_ready_state.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#define CHECK(v, m) do { if (!(v)) { \
|
||||
fprintf(stderr, "FAIL: %s\n", (m)); return 1; } } while (0)
|
||||
|
||||
int main(void) {
|
||||
PSXOpenXRReadyLoadState state = {0};
|
||||
PSXOpenXRReadyLoadRequest request = {0};
|
||||
PSXOpenXRReadyLoadStatus status = {0};
|
||||
|
||||
psx_openxr_ready_load_begin_execution(&state, 1, 0, -1);
|
||||
CHECK(psx_openxr_ready_load_ok(&state), "no requested load is ready");
|
||||
|
||||
psx_openxr_ready_load_begin_execution(&state, 1, 1, 2);
|
||||
CHECK(!psx_openxr_ready_load_ok(&state), "requested load waits for its request");
|
||||
|
||||
request = (PSXOpenXRReadyLoadRequest){1, 1, 10, 2, NULL};
|
||||
CHECK(psx_openxr_ready_load_observe_request(&state, &request),
|
||||
"first load request is accepted");
|
||||
CHECK(!psx_openxr_ready_load_ok(&state), "pending request is not ready");
|
||||
status = (PSXOpenXRReadyLoadStatus){10, 1, 0, 1, 2};
|
||||
CHECK(!psx_openxr_ready_load_observe_status(&state, &status),
|
||||
"pending request cannot resolve");
|
||||
status = (PSXOpenXRReadyLoadStatus){11, 0, 1, 1, 2};
|
||||
CHECK(psx_openxr_ready_load_observe_status(&state, &status),
|
||||
"completed current request resolves");
|
||||
CHECK(psx_openxr_ready_load_ok(&state), "successful load is ready");
|
||||
|
||||
/* An XR session re-init in the same execution rebinds the current request. */
|
||||
psx_openxr_ready_load_begin_execution(&state, 1, 1, 2);
|
||||
CHECK(!psx_openxr_ready_load_ok(&state), "session re-init clears readiness");
|
||||
request = (PSXOpenXRReadyLoadRequest){1, 1, 10, 2, NULL};
|
||||
CHECK(psx_openxr_ready_load_observe_request(&state, &request),
|
||||
"current execution request is rebound");
|
||||
status = (PSXOpenXRReadyLoadStatus){11, 0, 1, 1, 2};
|
||||
CHECK(psx_openxr_ready_load_observe_status(&state, &status) &&
|
||||
psx_openxr_ready_load_ok(&state),
|
||||
"current execution stays ready after XR session re-init");
|
||||
|
||||
/* A reboot must ignore both the prior status and the prior request. */
|
||||
psx_openxr_ready_load_begin_execution(&state, 2, 1, 2);
|
||||
status = (PSXOpenXRReadyLoadStatus){11, 0, 1, 1, 2};
|
||||
CHECK(!psx_openxr_ready_load_observe_status(&state, &status),
|
||||
"previous session success cannot authorize reboot");
|
||||
CHECK(!psx_openxr_ready_load_ok(&state), "reboot waits for a fresh load");
|
||||
request = (PSXOpenXRReadyLoadRequest){1, 1, 10, 2, NULL};
|
||||
CHECK(!psx_openxr_ready_load_observe_request(&state, &request),
|
||||
"stale execution request is ignored");
|
||||
|
||||
request = (PSXOpenXRReadyLoadRequest){2, 2, 11, 2, NULL};
|
||||
CHECK(psx_openxr_ready_load_observe_request(&state, &request),
|
||||
"reboot load request is accepted");
|
||||
CHECK(!psx_openxr_ready_load_ok(&state),
|
||||
"reboot pending load cannot reuse the prior success");
|
||||
status = (PSXOpenXRReadyLoadStatus){12, 0, 0, 1, 2};
|
||||
CHECK(psx_openxr_ready_load_observe_status(&state, &status),
|
||||
"reboot failed load resolves as failure");
|
||||
CHECK(psx_openxr_ready_load_failed(&state),
|
||||
"reboot failed load cannot authorize success");
|
||||
|
||||
psx_openxr_ready_load_begin_execution(&state, 3, 1, 2);
|
||||
request = (PSXOpenXRReadyLoadRequest){3, 3, 12, 2, NULL};
|
||||
CHECK(psx_openxr_ready_load_observe_request(&state, &request),
|
||||
"recovery load request is accepted");
|
||||
status = (PSXOpenXRReadyLoadStatus){13, 0, 1, 1, 2};
|
||||
CHECK(psx_openxr_ready_load_observe_status(&state, &status),
|
||||
"recovery load resolves");
|
||||
CHECK(psx_openxr_ready_load_ok(&state), "recovery session is ready");
|
||||
|
||||
psx_openxr_ready_load_begin_execution(&state, 4, 1, 3);
|
||||
request = (PSXOpenXRReadyLoadRequest){4, 4, 13, -1,
|
||||
"state_load_request_refused"};
|
||||
CHECK(psx_openxr_ready_load_observe_request(&state, &request),
|
||||
"refused load request is accepted");
|
||||
CHECK(psx_openxr_ready_load_failed(&state) &&
|
||||
state.error && state.error[0],
|
||||
"refused load reports failure in its execution");
|
||||
|
||||
puts("ALL PASS");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user