mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Merge pull request #598 from RetroPortingToolKit/feat/host-pause
Host pause API (in-game overlay P1)
This commit is contained in:
@@ -493,9 +493,9 @@ between Play and TCP availability.
|
||||
|
||||
## Complete command index (generated)
|
||||
|
||||
**371 commands registered** — 358 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
**375 commands registered** — 362 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
|
||||
83 of 371 have prose above; **288 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
84 of 375 have prose above; **291 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
|
||||
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
|
||||
|
||||
@@ -657,8 +657,11 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `hd_textures` | ✓ | | ✓ |
|
||||
| `history` | ✓ | ✓ | ✓ |
|
||||
| `hle_dump` | ✓ | | ✓ |
|
||||
| `host_key` | ✓ | | |
|
||||
| `host_launch_timings` | ✓ | | ✓ |
|
||||
| `host_mouse` | ✓ | | |
|
||||
| `host_osd_shot` | ✓ | | |
|
||||
| `host_pause` | ✓ | | |
|
||||
| `host_profile` | ✓ | | |
|
||||
| `idle_skip` | ✓ | | |
|
||||
| `imask_trace` | ✓ | | |
|
||||
|
||||
@@ -224,6 +224,14 @@ 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)
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
add_test(NAME menu_video_snapshot_test COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_menu_video_snapshot.py
|
||||
--cxx ${CMAKE_CXX_COMPILER})
|
||||
add_test(NAME host_pause_first_present_test COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_host_pause_first_present.py
|
||||
--cxx ${CMAKE_CXX_COMPILER})
|
||||
endif()
|
||||
add_test(NAME guest_implementation_binding_test COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_guest_implementation.py"
|
||||
--cmake "${CMAKE_COMMAND}" --compiler "${CMAKE_C_COMPILER}"
|
||||
@@ -803,6 +811,17 @@ if(BUILD_TESTING)
|
||||
tests/test_pad_external_input.c src/pad_external_input.c)
|
||||
target_include_directories(pad_external_input_test PRIVATE include src)
|
||||
add_test(NAME pad_external_input_test COMMAND pad_external_input_test)
|
||||
add_executable(host_overlay_test tests/test_host_overlay.c src/host_overlay.c src/video_texture_filter.c)
|
||||
target_include_directories(host_overlay_test PRIVATE include src)
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(host_overlay_test PRIVATE Threads::Threads)
|
||||
if(WIN32)
|
||||
target_link_libraries(host_overlay_test PRIVATE shell32)
|
||||
endif()
|
||||
add_test(NAME host_overlay_test COMMAND host_overlay_test)
|
||||
add_executable(psx_menu_host_test tests/test_psx_menu_host.c src/psx_menu_host.c)
|
||||
target_include_directories(psx_menu_host_test PRIVATE include src)
|
||||
add_test(NAME psx_menu_host_test COMMAND psx_menu_host_test)
|
||||
|
||||
add_executable(gte_register_access_test
|
||||
tests/test_gte_register_access.cpp
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/* host_overlay.h -- hooks for an optional in-game menu ("overlay provider").
|
||||
*
|
||||
* Every hook is inert until something registers with it: a title with no
|
||||
* overlay provider runs exactly as before. Sections are added by the
|
||||
* docs/IN_GAME_OVERLAY.md PR stack (P1 pause, P2 draw, P3 input, ...).
|
||||
*/
|
||||
#ifndef PSX_HOST_OVERLAY_H
|
||||
#define PSX_HOST_OVERLAY_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include "video_texture_filter.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- P1: host pause --------------------------------------------------------
|
||||
* A nesting pause owned by the host (not the guest): while depth > 0 the
|
||||
* runtime holds the guest at the vblank present, keeps presenting the last
|
||||
* frame and pauses audio. Push refuses (returns 0) while the refusal probe
|
||||
* says so -- the runtime installs psx_netplay_active() there, because pausing
|
||||
* one peer would stall the session. Main thread only. */
|
||||
typedef int (*PsxHostPauseRefuseFn)(void);
|
||||
int psx_host_pause_push(const char *reason);
|
||||
void psx_host_pause_pop(void);
|
||||
int psx_host_pause_depth(void);
|
||||
const char *psx_host_pause_reason(void);
|
||||
void psx_host_pause_set_refuse_probe(PsxHostPauseRefuseFn fn);
|
||||
|
||||
/* ---- P2: overlay draw callback ---------------------------------------------
|
||||
* Called once per presented frame by the OpenGL backend after the game image
|
||||
* and the host OSD layers are composed, before present_shot capture and the
|
||||
* swap, with the drawable size in pixels. It runs on the thread that owns the
|
||||
* GL context (the render thread when it is on, else the main thread), with the
|
||||
* window framebuffer bound. The callback must leave GL state as it found it.
|
||||
* Vulkan and the SDL renderer never call it. NULL unregisters.
|
||||
*
|
||||
* Lifetime contract: the registrant owns ctx. psx_host_overlay_set_draw_cb
|
||||
* (replace or NULL) returns only after any draw call that may still be using
|
||||
* the previous fn/ctx has finished, so the registrant may free ctx and the GL
|
||||
* objects it draws with right after it returns, and must keep them alive
|
||||
* until then. Unregister before destroying those objects or the GL context
|
||||
* (e.g. on renderer shutdown). Called from inside the callback it does not
|
||||
* wait; the current call is then the last one with the old ctx. */
|
||||
typedef void (*PsxHostOverlayDrawFn)(int width, int height, void *ctx);
|
||||
void psx_host_overlay_set_draw_cb(PsxHostOverlayDrawFn fn, void *ctx);
|
||||
int psx_host_overlay_has_draw(void);
|
||||
void psx_host_overlay_draw(int width, int height); /* renderer side */
|
||||
|
||||
/* ---- P3: input sink -----------------------------------------------------
|
||||
* The sink sees every host SDL event first (main thread), in the normal event
|
||||
* drain and in the host pause loop. Returning non-zero consumes the event.
|
||||
* While ui_capture is on, local game input reads as released/centred, in
|
||||
* netplay too (the peer then simply receives "nothing pressed"), so a menu
|
||||
* open over a running session never leaks presses into the game.
|
||||
* `event` is an SDL_Event*. NULL unregisters. */
|
||||
typedef int (*PsxHostInputSinkFn)(const void *event, void *ctx);
|
||||
void psx_host_set_input_sink(PsxHostInputSinkFn fn, void *ctx);
|
||||
int psx_host_input_sink_dispatch(const void *event);
|
||||
void psx_host_set_ui_capture(int on);
|
||||
int psx_host_ui_capture_active(void);
|
||||
|
||||
/* ---- P4: live video settings ---------------------------------------------
|
||||
* The [video] keys an in-game menu may change, in settings.toml terms.
|
||||
* psx_video_apply_live() applies what can change on a running game, saves
|
||||
* every field to settings.toml, and returns PSX_VIDEO_RESTART_* bits for the
|
||||
* fields that only take effect on the next start (compared with boot). Main
|
||||
* thread only. */
|
||||
typedef struct PsxHostVideoSettings {
|
||||
int fullscreen; /* 0 windowed, 1 borderless, 2 exclusive */
|
||||
int texture_filter; /* PSX_TEXTURE_FILTER_* (the [video] enum) */
|
||||
int fmv_filter; /* VIDEO_FMV_FILTER_* (config value) */
|
||||
int scanlines; /* 0/1 */
|
||||
int scanline_strength_pct; /* 0..100 */
|
||||
int present_linear; /* [video] antialiasing (linear present) 0/1 */
|
||||
int dynamic_resolution; /* 0/1 */
|
||||
int frame_generation; /* 0/1 */
|
||||
int renderer; /* restart */
|
||||
int internal_resolution; /* restart */
|
||||
int supersampling; /* restart */
|
||||
int render_thread; /* restart */
|
||||
int present_thread; /* restart */
|
||||
} PsxHostVideoSettings;
|
||||
|
||||
/* Texture filter values: PSX_TEXTURE_FILTER_* and psx_texture_filter_from_ui
|
||||
* (video_texture_filter.h, #655), shared with settings and the handoff. */
|
||||
|
||||
#define PSX_VIDEO_RESTART_RENDERER (1u << 0)
|
||||
#define PSX_VIDEO_RESTART_RESOLUTION (1u << 1)
|
||||
#define PSX_VIDEO_RESTART_THREADS (1u << 2)
|
||||
/* Netplay is running: live changes beyond presentation (dynamic resolution,
|
||||
* frame generation, filters) were saved but not applied; they take effect in
|
||||
* the next session. */
|
||||
#define PSX_VIDEO_RESTART_NETPLAY (1u << 3)
|
||||
|
||||
#define PSX_VIDEO_LIVE_FULLSCREEN (1u << 0)
|
||||
#define PSX_VIDEO_LIVE_TEXTURE_FILTER (1u << 1)
|
||||
#define PSX_VIDEO_LIVE_FMV_FILTER (1u << 2)
|
||||
#define PSX_VIDEO_LIVE_SCANLINES (1u << 3)
|
||||
#define PSX_VIDEO_LIVE_PRESENT_LINEAR (1u << 4)
|
||||
#define PSX_VIDEO_LIVE_DYNRES (1u << 5)
|
||||
#define PSX_VIDEO_LIVE_FRAME_GEN (1u << 6)
|
||||
|
||||
/* Pure helpers (unit-tested): which live fields differ, and which restart
|
||||
* fields differ from the values the session booted with. */
|
||||
unsigned psx_host_video_live_changes(const PsxHostVideoSettings *prev,
|
||||
const PsxHostVideoSettings *next);
|
||||
unsigned psx_host_video_restart_bits(const PsxHostVideoSettings *boot,
|
||||
const PsxHostVideoSettings *next);
|
||||
/* Runtime side (main.cpp). */
|
||||
unsigned psx_video_apply_live(const PsxHostVideoSettings *next);
|
||||
void psx_video_current(PsxHostVideoSettings *out);
|
||||
|
||||
/* Replace this process with a fresh copy started with the same arguments
|
||||
* (the in-game menu's quick relaunch for restart-only settings). Returns only
|
||||
* on failure. */
|
||||
void psx_relaunch_self(int argc, char** argv);
|
||||
/* Windows command-line quoting for one argument (CommandLineToArgvW rules);
|
||||
* returns the quoted length, writing at most cap-1 chars plus NUL to out
|
||||
* (out may be NULL to measure). Portable, for tests. */
|
||||
size_t psx_win_cmdline_quote(const char* arg, char* out, size_t cap);
|
||||
/* All arguments quoted and space-joined; malloc'd (free it), NULL on OOM. */
|
||||
char* psx_win_cmdline_join(int argc, const char* const* argv);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -372,6 +372,38 @@ int psx_mod_option_value(const char* package_id, const char* feature_id,
|
||||
* The same committed-plan and buffer rules as psx_mod_option_value apply. */
|
||||
int psx_mod_current_option_value(const char* option_id,
|
||||
char* out, uint32_t out_size);
|
||||
/*
|
||||
* Live option changes (in-game overlay P5). A trusted plugin that can re-read
|
||||
* an option on a running game registers an option-changed callback under its
|
||||
* plugin id; it runs in that plugin's context (psx_mod_current_option_value
|
||||
* already answers the new value) and returns 1 if the change took effect now.
|
||||
*
|
||||
* psx_mod_set_option_live records a new value for a committed feature's
|
||||
* option for the rest of the session and notifies its plugin. Returns 1 when
|
||||
* a plugin applied it live; 0 when the feature has no live-capable plugin (or
|
||||
* is not in the plan) -- then nothing changes until the next start; -1 when
|
||||
* refused: during netplay the plan is
|
||||
* negotiated between peers and must not change. Persisting the choice stays
|
||||
* the mod provider's job (mods/state.toml).
|
||||
*/
|
||||
typedef int (*PSXModOptionChangedCallback)(const char* option_id, const char* value);
|
||||
int psx_mod_register_option_changed_plugin(const char* id,
|
||||
PSXModOptionChangedCallback callback);
|
||||
int psx_mod_set_option_live(const char* package_id, const char* feature_id,
|
||||
const char* option_id, const char* value);
|
||||
/* 1 when the committed feature's plugin registered an option-changed callback
|
||||
* (so a live set can take effect now). Read-only; any thread. */
|
||||
int psx_mod_option_live_capable(const char* package_id, const char* feature_id);
|
||||
/* Write the player's current selection (feature switches and option values,
|
||||
* as edited through the launcher mod provider) to mods/state.toml now,
|
||||
* without re-resolving the running plan. Main thread. 1 on success. */
|
||||
int psx_mod_save_selection(void);
|
||||
/* Switch a feature on/off or set one of its options in the selection (not
|
||||
* the committed plan; psx_mod_save_selection persists it). Main thread.
|
||||
* Returns 1 on success. Used by the in-game menu host. */
|
||||
int psx_mod_feature_enable(const char* package_id, const char* feature_id, int enabled);
|
||||
int psx_mod_feature_set_option(const char* package_id, const char* feature_id,
|
||||
const char* option_id, const char* value);
|
||||
/*
|
||||
* Read the committed owner-selected path for a resource declared by the
|
||||
* package feature whose trusted plugin is currently running. Returns 0 when
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/* psx_menu_host.h -- the runtime's contract with an in-game menu provider.
|
||||
*
|
||||
* psxrecomp owns this header. A frontend (recomp-ui, recomp-launcher, or any
|
||||
* other) implements PsxMenuProvider; the runtime implements PsxMenuHost. The
|
||||
* runtime keeps every policy (pause, live settings, mod changes, disc/tray,
|
||||
* netplay session start/exit) on its side, so all providers get the same
|
||||
* behaviour. Nothing here is specific to one frontend.
|
||||
*
|
||||
* Selection: no provider is registered by default, so a title's existing UI
|
||||
* (recomp-ui) and its pin are unchanged. A title that wants a provider
|
||||
* registers it with psx_menu_set_provider() before the game window opens
|
||||
* (for example from a static initializer in the title's own code).
|
||||
*
|
||||
* Threads: attach/event/is_open/set_open run on the main thread; draw runs on
|
||||
* the thread that owns the GL context (host_overlay.h P2) with the window
|
||||
* framebuffer bound. PsxMenuHost calls may come from either; the runtime
|
||||
* applies them on the main thread at the next frame. */
|
||||
#ifndef PSX_MENU_HOST_H
|
||||
#define PSX_MENU_HOST_H
|
||||
|
||||
#include "host_overlay.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PSX_MENU_ABI_VERSION 1
|
||||
|
||||
/* request_exit */
|
||||
enum {
|
||||
PSX_MENU_EXIT_QUIT = 0, /* leave the game (netplay: back to the lobby) */
|
||||
PSX_MENU_EXIT_TO_FRONTEND = 1, /* end the session, re-enter the frontend loop in-process */
|
||||
PSX_MENU_EXIT_RELAUNCH = 2 /* restart this process with its original arguments */
|
||||
};
|
||||
|
||||
/* video_apply returns PSX_VIDEO_RESTART_* bits (host_overlay.h) plus this one:
|
||||
* during a netplay match only presentation applies; the rest waits. */
|
||||
#define PSX_MENU_RESTART_AFTER_MATCH (1u << 31)
|
||||
|
||||
/* The shared launcher C ABI (recomp_launcher.h) that recomp-ui and
|
||||
* recomp-launcher both speak for the pre-game launcher. Optional: a provider
|
||||
* that uses it gets the same game info, mod catalogue and lobby objects the
|
||||
* pre-game launcher gets; one that does not ignores these entries. */
|
||||
struct RecompLauncherCGameInfo;
|
||||
struct RecompLauncherCSettings;
|
||||
|
||||
typedef struct PsxMenuHost {
|
||||
int abi_version; /* PSX_MENU_ABI_VERSION */
|
||||
/* Pause the guest and audio (host_overlay.h P1). Returns 0 when refused
|
||||
* (netplay): the menu then stays open without pausing. */
|
||||
int (*set_paused)(int paused);
|
||||
int (*netplay_active)(void);
|
||||
/* Video settings: current values, and a live apply (P4) that also saves
|
||||
* settings.toml. Returns the restart bits of the fields that wait. */
|
||||
void (*video_get)(PsxHostVideoSettings *out);
|
||||
unsigned (*video_apply)(const PsxHostVideoSettings *next);
|
||||
/* Mod feature switch (option == NULL, value "true"/"false") or option.
|
||||
* Saved to mods/state.toml; returns 1 when it applied live (P5), 0 when it
|
||||
* waits for the next start. Refused (0, unsaved) during netplay. */
|
||||
int (*mod_set)(const char *package_id, const char *feature_id,
|
||||
const char *option_id, const char *value);
|
||||
/* Reload keybinds.ini / input.ini / config.ini after the menu wrote them. */
|
||||
void (*reload_bindings)(void);
|
||||
/* PSX_MENU_EXIT_*. Applied on the main thread; the menu is closed and its
|
||||
* pause dropped before the session ends. */
|
||||
void (*request_exit)(int how);
|
||||
/* Disc/tray: the runtime's production tray policy (psx_disc_tray.h),
|
||||
* shared with its own disc menu and hotkeys; count is 0 without a
|
||||
* multi-disc roster or a policy. insert queues the lid cycle for the main
|
||||
* thread: 0 = queued, -1 = no such disc, 3 = refused (netplay). */
|
||||
int (*disc_count)(void);
|
||||
int (*disc_mounted)(void); /* 1-based, 0 = none */
|
||||
const char *(*disc_path)(int disc_1based);
|
||||
int (*disc_insert)(int disc_1based);
|
||||
/* Shared launcher ABI helpers (see above); NULL in builds without it. */
|
||||
void (*launcher_game_info)(struct RecompLauncherCGameInfo *out);
|
||||
void (*launcher_settings_get)(struct RecompLauncherCSettings *out);
|
||||
unsigned (*launcher_settings_apply)(const struct RecompLauncherCSettings *next);
|
||||
const char *assets_dir; /* the title's assets folder */
|
||||
} PsxMenuHost;
|
||||
|
||||
typedef struct PsxMenuProvider {
|
||||
int abi_version; /* PSX_MENU_ABI_VERSION */
|
||||
const char *name;
|
||||
void *ctx;
|
||||
/* Once per GL context, with it current. Returns 0 to decline. */
|
||||
int (*attach)(void *ctx, void *sdl_window, void *gl_context, const PsxMenuHost *host);
|
||||
/* Every SDL event first; 1 = consumed (the game does not see it). The
|
||||
* provider opens itself (its own key/pad binding) from here. */
|
||||
int (*event)(void *ctx, const void *sdl_event);
|
||||
int (*is_open)(void *ctx);
|
||||
void (*set_open)(void *ctx, int open);
|
||||
/* Draw over the presented frame (P2). */
|
||||
void (*draw)(void *ctx, int framebuffer_w, int framebuffer_h);
|
||||
/* Before its GL context goes away (session end, rematch), with it current;
|
||||
* attach follows for the next context. */
|
||||
void (*detach)(void *ctx);
|
||||
} PsxMenuProvider;
|
||||
|
||||
/* Register (or clear, NULL) the in-game menu provider. Main thread, before
|
||||
* the game window opens; a provider with another ABI version is refused. */
|
||||
int psx_menu_set_provider(const PsxMenuProvider *provider);
|
||||
const PsxMenuProvider *psx_menu_provider(void);
|
||||
|
||||
/* The disc/tray policy publishes itself here (PsxMenuHost disc_* entries). */
|
||||
typedef struct PsxMenuDiscOps {
|
||||
int (*count)(void);
|
||||
int (*mounted)(void);
|
||||
const char *(*path)(int disc_1based);
|
||||
int (*insert)(int disc_1based);
|
||||
} PsxMenuDiscOps;
|
||||
void psx_menu_set_disc_ops(const PsxMenuDiscOps *ops);
|
||||
const PsxMenuDiscOps *psx_menu_disc_ops(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -21,6 +21,9 @@ typedef struct PsxSdlAudioSpec {
|
||||
SDL_AudioDeviceID psx_sdl_audio_open(
|
||||
const PsxSdlAudioSpec *want, PsxSdlAudioSpec *have);
|
||||
int psx_sdl_audio_resume(SDL_AudioDeviceID device);
|
||||
/* Host pause (host_overlay.h P1): stop/start the device without dropping
|
||||
* queued samples. */
|
||||
void psx_sdl_audio_set_paused(SDL_AudioDeviceID device, int paused);
|
||||
void psx_sdl_audio_close(SDL_AudioDeviceID device);
|
||||
void psx_sdl_audio_clear(SDL_AudioDeviceID device);
|
||||
Uint32 psx_sdl_audio_queued_size(SDL_AudioDeviceID device);
|
||||
|
||||
@@ -390,6 +390,8 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_openxr.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_controller_source.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/pad_external_input.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/host_overlay.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_menu_host.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_pad_transform.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/vr_pose_math.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_vk_renderer.c
|
||||
@@ -2286,7 +2288,7 @@ function(psxrecomp_add_runtime_target target)
|
||||
# by opengl32; Phase 2b will load modern GL via SDL_GL_GetProcAddress.
|
||||
# iphlpapi: GetAdaptersAddresses in the launcher's netplay
|
||||
# local-address discovery (MSVC does not link it implicitly).
|
||||
target_link_libraries(${target} PRIVATE ws2_32 iphlpapi dbghelp comdlg32 opengl32)
|
||||
target_link_libraries(${target} PRIVATE ws2_32 iphlpapi dbghelp comdlg32 opengl32 shell32)
|
||||
# Newer mingw-w64 maps clock_gettime → clock_gettime64 in libwinpthread.
|
||||
# Link it even when netplay code prefers Win32 clocks, so any residual
|
||||
# POSIX time refs (third-party / debug tools) resolve under -static.
|
||||
|
||||
@@ -9255,6 +9255,40 @@ static void handle_debug_key(int id, const char *json)
|
||||
send_fmt("{\"id\":%d,\"ok\":%s,\"state\":\"%s\"}", id, ok ? "true" : "false", msg);
|
||||
}
|
||||
|
||||
/* host_pause on=<1|0>: push/pop a host pause (host_overlay.h P1). */
|
||||
#include "host_overlay.h"
|
||||
static void handle_host_pause(int id, const char *json)
|
||||
{
|
||||
int on = json_get_int(json, "on", -1);
|
||||
int ok = 1;
|
||||
if (on == 1) ok = psx_host_pause_push("debug");
|
||||
else if (on == 0) psx_host_pause_pop();
|
||||
send_fmt("{\"id\":%d,\"ok\":%s,\"depth\":%d}", id, ok ? "true" : "false",
|
||||
psx_host_pause_depth());
|
||||
}
|
||||
|
||||
/* host_key name=<SDL scancode name> down=<1|0> (both when omitted). */
|
||||
extern int psx_debug_push_host_key(const char *name, int down);
|
||||
static void handle_host_key(int id, const char *json)
|
||||
{
|
||||
char name[64] = "";
|
||||
json_get_str(json, "name", name, sizeof name);
|
||||
int down = json_get_int(json, "down", -1);
|
||||
int ok = down < 0 ? (psx_debug_push_host_key(name, 1) && psx_debug_push_host_key(name, 0))
|
||||
: psx_debug_push_host_key(name, down);
|
||||
send_fmt("{\"id\":%d,\"ok\":%s}", id, ok ? "true" : "false");
|
||||
}
|
||||
|
||||
/* host_mouse x=<pt> y=<pt> [wheel=<steps>]: click (or scroll) at a window point. */
|
||||
extern int psx_debug_push_host_mouse(float x, float y, int wheel);
|
||||
static void handle_host_mouse(int id, const char *json)
|
||||
{
|
||||
int ok = psx_debug_push_host_mouse((float)json_get_int(json, "x", 0),
|
||||
(float)json_get_int(json, "y", 0),
|
||||
json_get_int(json, "wheel", 0));
|
||||
send_fmt("{\"id\":%d,\"ok\":%s}", id, ok ? "true" : "false");
|
||||
}
|
||||
|
||||
static void handle_ws_nw(int id, const char *json)
|
||||
{
|
||||
int on = json_get_int(json, "on", -1);
|
||||
@@ -15916,6 +15950,9 @@ static const CmdEntry s_commands[] = {
|
||||
{ "window_size", handle_window_size },
|
||||
{ "ws_nw", handle_ws_nw },
|
||||
{ "debug_key", handle_debug_key },
|
||||
{ "host_pause", handle_host_pause },
|
||||
{ "host_key", handle_host_key },
|
||||
{ "host_mouse", handle_host_mouse },
|
||||
{ "scanline", handle_scanline },
|
||||
{ "ws_backdrop_ring", handle_ws_backdrop_ring },
|
||||
{ "ws_ui_groups", handle_ws_ui_groups },
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
* returns 0 and the runtime falls back to the pure software renderer — no
|
||||
* half-GL hybrid. */
|
||||
|
||||
#include "host_overlay.h"
|
||||
#include "gpu.h"
|
||||
#include "gpu_render.h"
|
||||
#include "gl_scale_limits.h"
|
||||
@@ -9680,6 +9681,9 @@ static void gl_swap_with_osd(void) {
|
||||
}
|
||||
if (ov_image(3, &px, &ow, &oh) && px)
|
||||
gl_draw_osd_image(px, ow, oh, ww, wh, 0, 0, ww, wh);
|
||||
/* In-game overlay provider (host_overlay.h P2), over everything
|
||||
* the host composes; no-op unless one registered. */
|
||||
psx_host_overlay_draw(ww, wh);
|
||||
}
|
||||
}
|
||||
if (!rth_replaying()) host_osd_present_done(); /* replay: done at record */
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
/* host_overlay.c -- see host_overlay.h. */
|
||||
#include "host_overlay.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sched.h>
|
||||
#endif
|
||||
|
||||
/* ---- P1: host pause ------------------------------------------------------ */
|
||||
#define PSX_HOST_PAUSE_MAX 8
|
||||
static int s_pause_depth = 0;
|
||||
static const char *s_pause_reason[PSX_HOST_PAUSE_MAX];
|
||||
static PsxHostPauseRefuseFn s_pause_refuse = NULL;
|
||||
|
||||
int psx_host_pause_push(const char *reason) {
|
||||
if (s_pause_refuse && s_pause_refuse()) return 0;
|
||||
if (s_pause_depth >= PSX_HOST_PAUSE_MAX) return 0;
|
||||
s_pause_reason[s_pause_depth++] = reason ? reason : "host";
|
||||
return 1;
|
||||
}
|
||||
|
||||
void psx_host_pause_pop(void) {
|
||||
if (s_pause_depth > 0) s_pause_reason[--s_pause_depth] = NULL;
|
||||
}
|
||||
|
||||
int psx_host_pause_depth(void) { return s_pause_depth; }
|
||||
|
||||
const char *psx_host_pause_reason(void) {
|
||||
return s_pause_depth > 0 ? s_pause_reason[s_pause_depth - 1] : NULL;
|
||||
}
|
||||
|
||||
void psx_host_pause_set_refuse_probe(PsxHostPauseRefuseFn fn) { s_pause_refuse = fn; }
|
||||
|
||||
/* ---- P2: overlay draw callback -------------------------------------------- */
|
||||
/* Registered on the main thread, read on the render / present thread. The
|
||||
* function and its context are published together as one immutable record
|
||||
* behind an atomic pointer, so a reader never pairs a live function with a
|
||||
* stale context.
|
||||
*
|
||||
* Lifetime: every call runs inside an in-flight count. Replacing or removing
|
||||
* the callback swaps the record and then waits until no call that could have
|
||||
* read the old record is still running, so when psx_host_overlay_set_draw_cb
|
||||
* returns, the previous fn will not run again and its ctx (and the GL objects
|
||||
* it draws with) may be freed by the caller. The old record is freed there
|
||||
* too. A change made from inside the callback itself does not wait (it would
|
||||
* wait for itself); that caller's own frame is the only one still using the
|
||||
* old ctx and it returns before the next frame reads the new record. */
|
||||
typedef struct {
|
||||
PsxHostOverlayDrawFn fn;
|
||||
void *ctx;
|
||||
} PsxHostOverlayDraw;
|
||||
|
||||
static _Atomic(PsxHostOverlayDraw *) s_draw = NULL;
|
||||
static atomic_int s_draw_inflight = 0;
|
||||
#if defined(_MSC_VER)
|
||||
static __declspec(thread) int s_in_draw = 0;
|
||||
#else
|
||||
static _Thread_local int s_in_draw = 0;
|
||||
#endif
|
||||
|
||||
static void draw_yield(void) {
|
||||
#if defined(_WIN32)
|
||||
Sleep(0);
|
||||
#else
|
||||
sched_yield();
|
||||
#endif
|
||||
}
|
||||
|
||||
void psx_host_overlay_set_draw_cb(PsxHostOverlayDrawFn fn, void *ctx) {
|
||||
PsxHostOverlayDraw *rec = NULL, *old;
|
||||
if (fn) {
|
||||
rec = (PsxHostOverlayDraw *)malloc(sizeof(*rec));
|
||||
if (!rec) return;
|
||||
rec->fn = fn;
|
||||
rec->ctx = ctx;
|
||||
}
|
||||
old = atomic_exchange(&s_draw, rec); /* seq_cst: ordered with the readers' count */
|
||||
if (s_in_draw) return; /* from inside the callback: see above; old stays (rare) */
|
||||
while (atomic_load(&s_draw_inflight) > 0) draw_yield();
|
||||
free(old);
|
||||
}
|
||||
|
||||
int psx_host_overlay_has_draw(void) {
|
||||
return atomic_load_explicit(&s_draw, memory_order_acquire) != NULL;
|
||||
}
|
||||
|
||||
void psx_host_overlay_draw(int width, int height) {
|
||||
PsxHostOverlayDraw *rec;
|
||||
if (width <= 0 || height <= 0) return;
|
||||
atomic_fetch_add(&s_draw_inflight, 1); /* seq_cst, before reading the record */
|
||||
rec = atomic_load(&s_draw);
|
||||
if (rec && rec->fn) {
|
||||
s_in_draw = 1;
|
||||
rec->fn(width, height, rec->ctx);
|
||||
s_in_draw = 0;
|
||||
}
|
||||
atomic_fetch_sub(&s_draw_inflight, 1);
|
||||
}
|
||||
|
||||
/* ---- P3: input sink -------------------------------------------------------- */
|
||||
static PsxHostInputSinkFn s_sink_fn = NULL;
|
||||
static void *s_sink_ctx = NULL;
|
||||
static volatile int s_ui_capture = 0;
|
||||
|
||||
void psx_host_set_input_sink(PsxHostInputSinkFn fn, void *ctx) {
|
||||
s_sink_fn = fn;
|
||||
s_sink_ctx = ctx;
|
||||
}
|
||||
|
||||
int psx_host_input_sink_dispatch(const void *event) {
|
||||
return (s_sink_fn && event) ? (s_sink_fn(event, s_sink_ctx) != 0) : 0;
|
||||
}
|
||||
|
||||
void psx_host_set_ui_capture(int on) { s_ui_capture = on ? 1 : 0; }
|
||||
int psx_host_ui_capture_active(void) { return s_ui_capture; }
|
||||
|
||||
/* ---- P4: live video settings ------------------------------------------------ */
|
||||
unsigned psx_host_video_live_changes(const PsxHostVideoSettings *a,
|
||||
const PsxHostVideoSettings *b) {
|
||||
unsigned m = 0;
|
||||
if (!a || !b) return 0;
|
||||
if (a->fullscreen != b->fullscreen) m |= PSX_VIDEO_LIVE_FULLSCREEN;
|
||||
if (a->texture_filter != b->texture_filter) m |= PSX_VIDEO_LIVE_TEXTURE_FILTER;
|
||||
if (a->fmv_filter != b->fmv_filter) m |= PSX_VIDEO_LIVE_FMV_FILTER;
|
||||
if (a->scanlines != b->scanlines || a->scanline_strength_pct != b->scanline_strength_pct)
|
||||
m |= PSX_VIDEO_LIVE_SCANLINES;
|
||||
if (a->present_linear != b->present_linear) m |= PSX_VIDEO_LIVE_PRESENT_LINEAR;
|
||||
if (a->dynamic_resolution != b->dynamic_resolution) m |= PSX_VIDEO_LIVE_DYNRES;
|
||||
if (a->frame_generation != b->frame_generation) m |= PSX_VIDEO_LIVE_FRAME_GEN;
|
||||
return m;
|
||||
}
|
||||
|
||||
unsigned psx_host_video_restart_bits(const PsxHostVideoSettings *boot,
|
||||
const PsxHostVideoSettings *next) {
|
||||
unsigned m = 0;
|
||||
if (!boot || !next) return 0;
|
||||
if (boot->renderer != next->renderer) m |= PSX_VIDEO_RESTART_RENDERER;
|
||||
if (boot->internal_resolution != next->internal_resolution ||
|
||||
boot->supersampling != next->supersampling)
|
||||
m |= PSX_VIDEO_RESTART_RESOLUTION;
|
||||
if (boot->render_thread != next->render_thread ||
|
||||
boot->present_thread != next->present_thread)
|
||||
m |= PSX_VIDEO_RESTART_THREADS;
|
||||
return m;
|
||||
}
|
||||
|
||||
/* ---- seamless relaunch ----------------------------------------------------- */
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Windows command-line quoting (the rules CommandLineToArgvW and the MSVC CRT
|
||||
* parse with): an argument is quoted when empty or holding a space, tab or
|
||||
* quote; inside quotes, backslashes are literal except before a quote, so a
|
||||
* run of n backslashes before a quote becomes 2n+1 plus the quote, and a run
|
||||
* before the closing quote becomes 2n. */
|
||||
size_t psx_win_cmdline_quote(const char* arg, char* out, size_t cap) {
|
||||
size_t n = 0, i, bs;
|
||||
const char* p;
|
||||
#define PUT(c) do { if (out && n + 1 < cap) out[n] = (c); ++n; } while (0)
|
||||
if (!arg) arg = "";
|
||||
if (*arg && !strpbrk(arg, " \t\n\v\"")) {
|
||||
for (p = arg; *p; ++p) PUT(*p);
|
||||
} else {
|
||||
PUT('"');
|
||||
for (p = arg;; ++p) {
|
||||
bs = 0;
|
||||
while (*p == '\\') { ++bs; ++p; }
|
||||
if (!*p) { for (i = 0; i < bs * 2; ++i) PUT('\\'); break; }
|
||||
if (*p == '"') { for (i = 0; i < bs * 2 + 1; ++i) PUT('\\'); PUT('"'); continue; }
|
||||
for (i = 0; i < bs; ++i) PUT('\\');
|
||||
PUT(*p);
|
||||
}
|
||||
PUT('"');
|
||||
}
|
||||
#undef PUT
|
||||
if (out && cap) out[n < cap ? n : cap - 1] = '\0';
|
||||
return n;
|
||||
}
|
||||
|
||||
char* psx_win_cmdline_join(int argc, const char* const* argv) {
|
||||
size_t total = 1;
|
||||
int i;
|
||||
char* out;
|
||||
char* w;
|
||||
for (i = 0; i < argc; ++i) total += psx_win_cmdline_quote(argv[i], NULL, 0) + 1;
|
||||
out = (char*)malloc(total);
|
||||
if (!out) return NULL;
|
||||
w = out;
|
||||
for (i = 0; i < argc; ++i) {
|
||||
if (i) *w++ = ' ';
|
||||
w += psx_win_cmdline_quote(argv[i], w, total - (size_t)(w - out));
|
||||
}
|
||||
*w = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <stdio.h>
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
/* Re-run this exe with the original arguments. The arguments come from
|
||||
* GetCommandLineW (full Unicode, no ANSI code-page loss, any count or length),
|
||||
* are re-quoted by the rules above, and the new process is started before
|
||||
* this one exits. argc/argv are only a fallback. */
|
||||
static wchar_t* utf8_to_wide(const char* s) {
|
||||
int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0);
|
||||
wchar_t* w = n > 0 ? (wchar_t*)malloc(sizeof(wchar_t) * (size_t)n) : NULL;
|
||||
if (w && MultiByteToWideChar(CP_UTF8, 0, s, -1, w, n) <= 0) { free(w); w = NULL; }
|
||||
return w;
|
||||
}
|
||||
static char* wide_to_utf8(const wchar_t* s) {
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, s, -1, NULL, 0, NULL, NULL);
|
||||
char* u = n > 0 ? (char*)malloc((size_t)n) : NULL;
|
||||
if (u && WideCharToMultiByte(CP_UTF8, 0, s, -1, u, n, NULL, NULL) <= 0) { free(u); u = NULL; }
|
||||
return u;
|
||||
}
|
||||
void psx_relaunch_self(int argc, char** argv) {
|
||||
int wargc = 0, i, ok = 1;
|
||||
wchar_t** wargv = CommandLineToArgvW(GetCommandLineW(), &wargc);
|
||||
const char** args = NULL;
|
||||
char* line = NULL;
|
||||
wchar_t* wline = NULL;
|
||||
wchar_t* exe = NULL;
|
||||
DWORD cap = MAX_PATH, got = 0;
|
||||
STARTUPINFOW si;
|
||||
PROCESS_INFORMATION pi;
|
||||
if (wargv && wargc > 0) {
|
||||
args = (const char**)calloc((size_t)wargc, sizeof(*args));
|
||||
for (i = 0; args && i < wargc; ++i)
|
||||
if (!(args[i] = wide_to_utf8(wargv[i]))) ok = 0;
|
||||
if (args && ok) line = psx_win_cmdline_join(wargc, args);
|
||||
for (i = 0; args && i < wargc; ++i) free((void*)args[i]);
|
||||
free((void*)args);
|
||||
LocalFree(wargv);
|
||||
} else if (argv) {
|
||||
line = psx_win_cmdline_join(argc, (const char* const*)argv);
|
||||
}
|
||||
if (!line) return;
|
||||
wline = utf8_to_wide(line);
|
||||
free(line);
|
||||
for (;;) { /* long paths: grow until the module path fits */
|
||||
free(exe);
|
||||
exe = (wchar_t*)malloc(sizeof(wchar_t) * cap);
|
||||
if (!exe) break;
|
||||
got = GetModuleFileNameW(NULL, exe, cap);
|
||||
if (got && got < cap) break;
|
||||
if (!got || cap >= 32768) { free(exe); exe = NULL; break; }
|
||||
cap *= 2;
|
||||
}
|
||||
if (wline && exe) {
|
||||
memset(&si, 0, sizeof(si));
|
||||
si.cb = sizeof(si);
|
||||
memset(&pi, 0, sizeof(pi));
|
||||
fflush(NULL);
|
||||
if (CreateProcessW(exe, wline, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
free(wline);
|
||||
free(exe);
|
||||
_exit(0);
|
||||
}
|
||||
}
|
||||
free(wline);
|
||||
free(exe);
|
||||
}
|
||||
#else
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
#if defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#endif
|
||||
void psx_relaunch_self(int argc, char** argv) {
|
||||
char path[4096];
|
||||
(void)argc;
|
||||
if (!argv || !argv[0]) return;
|
||||
path[0] = '\0';
|
||||
#if defined(__APPLE__)
|
||||
{
|
||||
uint32_t cap = sizeof(path);
|
||||
if (_NSGetExecutablePath(path, &cap) != 0) path[0] = '\0';
|
||||
}
|
||||
#elif defined(__linux__)
|
||||
{
|
||||
ssize_t n = readlink("/proc/self/exe", path, sizeof(path) - 1);
|
||||
path[n > 0 ? n : 0] = '\0';
|
||||
}
|
||||
#endif
|
||||
/* Interval timers and the signal mask survive exec: a still-armed
|
||||
* profiling/watchdog timer would kill the new image (default SIGPROF)
|
||||
* before it reaches main. */
|
||||
{
|
||||
struct itimerval off;
|
||||
sigset_t none;
|
||||
memset(&off, 0, sizeof(off));
|
||||
setitimer(ITIMER_REAL, &off, NULL);
|
||||
setitimer(ITIMER_VIRTUAL, &off, NULL);
|
||||
setitimer(ITIMER_PROF, &off, NULL);
|
||||
sigemptyset(&none);
|
||||
sigprocmask(SIG_SETMASK, &none, NULL);
|
||||
}
|
||||
fflush(NULL);
|
||||
execv(path[0] ? path : argv[0], argv);
|
||||
}
|
||||
#endif
|
||||
+654
-8
@@ -125,7 +125,9 @@ extern "C" void psx_event_step_conservative_env_init(void);
|
||||
#include "sbi_setup.h"
|
||||
#include "disc_path.h"
|
||||
#include "iso_reader.h" /* text-image guard: extract the boot EXE from the disc */
|
||||
#include "psx_keybinds.h" /* configurable keyboard->DualShock keybinds (keybinds.ini) */
|
||||
#include "psx_keybinds.h"
|
||||
#include "host_overlay.h"
|
||||
#include "psx_menu_host.h" /* optional in-game menu hooks (P1 pause, ...) */ /* configurable keyboard->DualShock keybinds (keybinds.ini) */
|
||||
#include "psx_window_icon.h"
|
||||
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
@@ -3471,6 +3473,11 @@ static std::filesystem::path default_memcard_dir(const char* argv0) {
|
||||
|
||||
static void close_controller(void);
|
||||
static void shutdown_runtime(void);
|
||||
/* The in-game menu's lobby started a match while a local game ran: the game
|
||||
* session ends but the pending launch (and its seats/signals) must survive
|
||||
* into the launcher loop that starts it. */
|
||||
static int g_ovl_launch_to_start = 0;
|
||||
static void recomp_overlay_detach(void); /* in-game menu host, below */
|
||||
|
||||
/* Set when this process started netplay from the lobby room — soft-exit returns
|
||||
* there instead of killing the process. */
|
||||
@@ -4003,6 +4010,11 @@ static void teardown_game_session_keep_lobby(void) {
|
||||
g_vk_active = false;
|
||||
}
|
||||
if (g_gl_active) {
|
||||
/* The in-game menu's GL objects belong to this context: stop the
|
||||
* render thread (the context comes back here), let the provider free
|
||||
* them, then shut the renderer down. The next session attaches again. */
|
||||
gl_renderer_render_thread_stop();
|
||||
recomp_overlay_detach();
|
||||
gl_renderer_shutdown();
|
||||
g_gl_active = false;
|
||||
}
|
||||
@@ -4019,13 +4031,15 @@ static void teardown_game_session_keep_lobby(void) {
|
||||
s_netplay_sim_native_scale = 0;
|
||||
gl_renderer_set_cpu_auth_dual(0);
|
||||
sw_set_faithful_authority(0);
|
||||
psx_lobby_set_ready(0);
|
||||
psx_lobby_clear_launch_pending();
|
||||
if (!g_ovl_launch_to_start) {
|
||||
psx_lobby_set_ready(0);
|
||||
psx_lobby_clear_launch_pending();
|
||||
#if defined(PSX_HAS_LOBBY_CLIENT)
|
||||
/* Drop prior-match ICE SDP/candidates; ignore new ones until next launch. */
|
||||
psx_lobby_clear_signals();
|
||||
psx_lobby_set_ice_signal_accept(0);
|
||||
/* Drop prior-match ICE SDP/candidates; ignore new ones until next launch. */
|
||||
psx_lobby_clear_signals();
|
||||
psx_lobby_set_ice_signal_accept(0);
|
||||
#endif
|
||||
}
|
||||
psx_clear_return_to_lobby();
|
||||
}
|
||||
|
||||
@@ -6177,6 +6191,8 @@ static int capture_host_layer_pad(PsxNetPad* out, uint16_t word,
|
||||
static int capture_pad_slot_view(int s, PsxNetPad* out, bool guarded,
|
||||
bool host_view) {
|
||||
if (!out) return 0;
|
||||
/* An open overlay menu owns the controls: the game sees released pads. */
|
||||
if (psx_host_ui_capture_active()) guarded = true;
|
||||
out->buttons = 0xFFFFu;
|
||||
out->lx = out->ly = out->rx = out->ry = 0x80u;
|
||||
out->analog = 0;
|
||||
@@ -6277,6 +6293,12 @@ static int capture_pad_slot_view(int s, PsxNetPad* out, bool guarded,
|
||||
|
||||
out->buttons = btn;
|
||||
out->lx = st[0]; out->ly = st[1]; out->rx = st[2]; out->ry = st[3];
|
||||
if (psx_host_ui_capture_active()) {
|
||||
/* Overlay open during netplay: publish "nothing pressed", which every
|
||||
* peer simulates identically. */
|
||||
out->buttons = 0xFFFFu;
|
||||
out->lx = out->ly = out->rx = out->ry = 0x80u;
|
||||
}
|
||||
out->analog = (uint8_t)frame_type;
|
||||
out->connected = 1;
|
||||
return 1;
|
||||
@@ -6895,6 +6917,10 @@ static void netplay_barrier_admit(int override) {
|
||||
if (!g_headless) {
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
/* The in-game overlay sees events first here too: the netplay
|
||||
* admit wait polls every frame, and an Esc meant for the menu
|
||||
* must not end the match. */
|
||||
if (psx_host_input_sink_dispatch(&ev)) continue;
|
||||
if (ev.type == SDL_QUIT) {
|
||||
netplay_soft_exit("sdl_window_close");
|
||||
if (psx_return_to_lobby_requested()) goto done;
|
||||
@@ -7915,7 +7941,10 @@ static void rewind_pause_present(void) {
|
||||
#ifndef PSX_SDL_NO_RENDER
|
||||
if (g_gl_active) {
|
||||
gl_renderer_set_interpolation_suspended(1);
|
||||
(void)gl_renderer_present_hold_last();
|
||||
/* A menu can pause before the first guest present. Give its draw
|
||||
* callback a blank host frame when no held frame exists, or input
|
||||
* handled during draw (including Resume) can never release pause. */
|
||||
if (!gl_renderer_present_hold_last()) gl_renderer_present_blank();
|
||||
} else if (g_vk_active) {
|
||||
vk_renderer_present_blank();
|
||||
} else if (sdl_renderer) {
|
||||
@@ -7988,6 +8017,113 @@ static void rewind_host_pause_loop(void) {
|
||||
savestate_input_guard_arm();
|
||||
}
|
||||
|
||||
/* Host pause (host_overlay.h, P1). Same freeze as the save-state menu: the
|
||||
* guest is held at the vblank present, the last frame keeps presenting and
|
||||
* audio is paused, until every psx_host_pause_push() has been popped. Nothing
|
||||
* pushes unless an overlay provider is registered, so titles without one never
|
||||
* enter this loop. */
|
||||
static void host_pause_service(void); /* in-game overlay glue (P6), below */
|
||||
static bool host_pause_event(const SDL_Event& ev); /* P3: input sink */
|
||||
static bool host_pause_event(const SDL_Event& ev) {
|
||||
return psx_host_input_sink_dispatch(&ev) != 0;
|
||||
}
|
||||
|
||||
static void host_pause_loop(void) {
|
||||
psx_local_mouse_begin(sdl_window, false);
|
||||
psx_local_mouse_reset();
|
||||
freeze_heartbeat_set_paused(1);
|
||||
psx_sdl_audio_set_paused(sdl_audio_device, 1);
|
||||
while (psx_host_pause_depth() > 0) {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
debug_server_poll();
|
||||
#endif
|
||||
SDL_Event ev;
|
||||
while (!g_headless && SDL_PollEvent(&ev)) {
|
||||
(void)psx_local_mouse_event(ev);
|
||||
if (host_pause_event(ev)) continue;
|
||||
if (ev.type == SDL_QUIT) {
|
||||
psx_crash_trace_set_exit_origin("sdl_window_close");
|
||||
shutdown_runtime();
|
||||
std::exit(0);
|
||||
} else if (ev.type == SDL_CONTROLLERDEVICEADDED) {
|
||||
refresh_player_devices();
|
||||
} else if (ev.type == SDL_CONTROLLERDEVICEREMOVED) {
|
||||
close_controller();
|
||||
refresh_player_devices();
|
||||
}
|
||||
}
|
||||
host_pause_service();
|
||||
if (!g_headless) rewind_pause_present();
|
||||
starvation_watchdog_heartbeat();
|
||||
SDL_Delay(g_headless ? 1 : 8);
|
||||
}
|
||||
psx_sdl_audio_set_paused(sdl_audio_device, 0);
|
||||
freeze_heartbeat_set_paused(0);
|
||||
/* Resume the way a savestate load does: the pacer's deadline is now in
|
||||
* the past, and without a re-anchor it pays the pause back as up to ~12
|
||||
* unpaced frames (a visible catch-up burst). Re-anchor presentation and
|
||||
* the FPS baseline, resync guest-cycle audio budgeting, and hold dynamic
|
||||
* resolution so the pause's wall time is not read as an overrun. */
|
||||
present_session_reset();
|
||||
g_audio_cycle_resync = 1;
|
||||
dynres_note_savestate_loaded();
|
||||
/* Swallow the still-held resume press so it doesn't reach the game. */
|
||||
savestate_input_guard_arm();
|
||||
}
|
||||
|
||||
/* Debug: inject a host key / gamepad button as an SDL event, so scripted
|
||||
* runs can drive host UI (the overlay) through the real event path. */
|
||||
extern "C" int psx_debug_push_host_key(const char *name, int down) {
|
||||
const SDL_Scancode sc = SDL_GetScancodeFromName(name ? name : "");
|
||||
if (sc == SDL_SCANCODE_UNKNOWN) return 0;
|
||||
SDL_Event ev;
|
||||
SDL_zero(ev);
|
||||
#if defined(PSX_SDL3)
|
||||
ev.type = down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP;
|
||||
ev.key.scancode = sc;
|
||||
ev.key.key = SDL_GetKeyFromScancode(sc, SDL_KMOD_NONE, false);
|
||||
ev.key.down = down != 0;
|
||||
ev.key.windowID = sdl_window ? SDL_GetWindowID(sdl_window) : 0;
|
||||
#else
|
||||
ev.type = down ? SDL_KEYDOWN : SDL_KEYUP;
|
||||
ev.key.keysym.scancode = sc;
|
||||
ev.key.keysym.sym = SDL_GetKeyFromScancode(sc);
|
||||
ev.key.state = down ? SDL_PRESSED : SDL_RELEASED;
|
||||
#endif
|
||||
return SDL_PushEvent(&ev) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* Debug: inject a mouse click (x, y in window points) or wheel step. */
|
||||
extern "C" int psx_debug_push_host_mouse(float x, float y, int wheel) {
|
||||
SDL_Event ev;
|
||||
const Uint32 win = sdl_window ? SDL_GetWindowID(sdl_window) : 0;
|
||||
#if defined(PSX_SDL3)
|
||||
SDL_zero(ev);
|
||||
ev.type = SDL_EVENT_MOUSE_MOTION;
|
||||
ev.motion.windowID = win; ev.motion.x = x; ev.motion.y = y;
|
||||
if (!SDL_PushEvent(&ev)) return 0;
|
||||
if (wheel) {
|
||||
SDL_zero(ev);
|
||||
ev.type = SDL_EVENT_MOUSE_WHEEL;
|
||||
ev.wheel.windowID = win; ev.wheel.y = (float)wheel; ev.wheel.mouse_x = x; ev.wheel.mouse_y = y;
|
||||
return SDL_PushEvent(&ev) ? 1 : 0;
|
||||
}
|
||||
for (int down = 1; down >= 0; --down) {
|
||||
SDL_zero(ev);
|
||||
ev.type = down ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP;
|
||||
ev.button.windowID = win; ev.button.button = SDL_BUTTON_LEFT;
|
||||
ev.button.down = down != 0; ev.button.clicks = 1; ev.button.x = x; ev.button.y = y;
|
||||
if (!SDL_PushEvent(&ev)) return 0;
|
||||
}
|
||||
return 1;
|
||||
#else
|
||||
(void)ev; (void)win; (void)x; (void)y; (void)wheel;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int host_pause_refuse_netplay(void) { return psx_netplay_active() ? 1 : 0; }
|
||||
|
||||
/* Freeze guest in vblank present while the save-state slot menu is open. */
|
||||
static void savestate_menu_host_pause_loop(void) {
|
||||
psx_local_mouse_begin(sdl_window, false);
|
||||
@@ -8131,6 +8267,8 @@ static bool drain_host_events() {
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
if (psx_local_mouse_event(ev)) continue;
|
||||
/* In-game overlay provider sees events first (host_overlay.h P3). */
|
||||
if (psx_host_input_sink_dispatch(&ev)) continue;
|
||||
if (ev.type == SDL_QUIT) {
|
||||
if (psx_netplay_active()) {
|
||||
netplay_soft_exit("sdl_window_close");
|
||||
@@ -8277,6 +8415,7 @@ static bool local_mouse_live(int override_word) {
|
||||
g.savestate_menu_open = savestate_menu_open;
|
||||
g.rewind_open = psx_rewind_is_open();
|
||||
g.input_guard = savestate_input_guard_active();
|
||||
g.ui_capture = psx_host_ui_capture_active();
|
||||
return pad_ext_live(&g) != 0;
|
||||
}
|
||||
|
||||
@@ -8444,6 +8583,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
/* Pump SDL events to prevent window freeze. */
|
||||
psx_local_mouse_begin(sdl_window, local_mouse_live(override));
|
||||
if (!drain_host_events()) return ep;
|
||||
host_pause_service();
|
||||
savestate_menu_poll_toggle_buttons();
|
||||
rewind_poll_toggle_buttons();
|
||||
fast_forward_toggle_poll_buttons();
|
||||
@@ -8453,6 +8593,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
savestate_menu_host_pause_loop();
|
||||
if (psx_rewind_is_open())
|
||||
rewind_host_pause_loop();
|
||||
if (psx_host_pause_depth() > 0)
|
||||
host_pause_loop();
|
||||
} else if (host_pad_layer(nullptr, nullptr, nullptr, nullptr)) {
|
||||
/* Headless with the debug host layer: the host shortcuts that need no
|
||||
* window (Rewind capture/open, fast-forward latch) poll as they would
|
||||
@@ -8463,6 +8605,11 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
if (psx_rewind_is_open())
|
||||
rewind_host_pause_loop();
|
||||
}
|
||||
/* Headless: a host pause (debug host_pause, scripted overlay tests)
|
||||
* holds the guest exactly as it does windowed; the loop polls the debug
|
||||
* server and needs no window. */
|
||||
if (g_headless && psx_host_pause_depth() > 0)
|
||||
host_pause_loop();
|
||||
|
||||
/* Sample each player's device and feed the matching SIO pad slot.
|
||||
* Debug server input override (when active) drives port 1 only. With
|
||||
@@ -8836,9 +8983,14 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
if (psx_local_mouse_installed()) {
|
||||
psx_local_mouse_begin(sdl_window, local_mouse_live(override));
|
||||
if (!drain_host_events()) return ep;
|
||||
/* The overlay may have opened during the pacer wait (its
|
||||
* Start+Select chord): settle ui_capture before the pad
|
||||
* sample below, or the chord reaches the guest for a frame. */
|
||||
host_pause_service();
|
||||
// A hotkey acquired during pacing may open a host menu.
|
||||
if (savestate_menu_open) savestate_menu_host_pause_loop();
|
||||
if (psx_rewind_is_open()) rewind_host_pause_loop();
|
||||
if (psx_host_pause_depth() > 0) host_pause_loop();
|
||||
psx_local_mouse_begin(sdl_window, local_mouse_live(override));
|
||||
} else {
|
||||
SDL_PumpEvents(); // retain native timing when no policy exists
|
||||
@@ -9547,6 +9699,92 @@ static void dynres_setup(void) {
|
||||
floor_s * g_video_ref_lines, ceiling * g_video_ref_lines);
|
||||
}
|
||||
|
||||
/* ---- Live video apply (host_overlay.h P4) ------------------------------- */
|
||||
static std::filesystem::path g_overlay_settings_path; /* settings.toml */
|
||||
static PsxHostVideoSettings g_video_boot; /* as the session booted */
|
||||
static bool g_video_boot_valid = false;
|
||||
|
||||
extern "C" void psx_video_current(PsxHostVideoSettings *v) {
|
||||
if (!v) return;
|
||||
std::memset(v, 0, sizeof *v);
|
||||
v->fullscreen = s_window_fullscreen.mode;
|
||||
v->texture_filter = g_video_texfilter;
|
||||
v->fmv_filter = g_video_fmv_filter;
|
||||
v->scanlines = g_video_scanlines ? 1 : 0;
|
||||
v->scanline_strength_pct = (int)(g_video_scanline_strength * 100.0f + 0.5f);
|
||||
v->present_linear = g_video_aa ? 1 : 0;
|
||||
v->dynamic_resolution = g_video_dynres ? 1 : 0;
|
||||
v->frame_generation = g_frame_generation ? 1 : 0;
|
||||
v->renderer = g_video_renderer;
|
||||
v->internal_resolution = g_video_internal_res;
|
||||
v->supersampling = g_video_scale;
|
||||
v->render_thread = g_render_thread ? 1 : 0;
|
||||
v->present_thread = g_present_thread ? 1 : 0;
|
||||
if (!g_video_boot_valid) { g_video_boot = *v; g_video_boot_valid = true; }
|
||||
}
|
||||
|
||||
extern "C" unsigned psx_video_apply_live(const PsxHostVideoSettings *next) {
|
||||
if (!next) return 0;
|
||||
PsxHostVideoSettings cur;
|
||||
psx_video_current(&cur);
|
||||
unsigned m = psx_host_video_live_changes(&cur, next);
|
||||
unsigned deferred = 0;
|
||||
/* Netplay: dynamic resolution, frame generation and the filters change
|
||||
* how a peer renders mid-match; only the presentation of the frame we
|
||||
* already show may change. The rest is saved for the next session. */
|
||||
if (psx_netplay_active()) {
|
||||
const unsigned present_only = PSX_VIDEO_LIVE_FULLSCREEN |
|
||||
PSX_VIDEO_LIVE_SCANLINES |
|
||||
PSX_VIDEO_LIVE_PRESENT_LINEAR;
|
||||
if (m & ~present_only) deferred = PSX_VIDEO_RESTART_NETPLAY;
|
||||
m &= present_only;
|
||||
}
|
||||
if (m & PSX_VIDEO_LIVE_FULLSCREEN)
|
||||
(void)psx_window_fullscreen_set(sdl_window, &s_window_fullscreen, next->fullscreen);
|
||||
if (m & PSX_VIDEO_LIVE_TEXTURE_FILTER) {
|
||||
g_video_texfilter = psx_texture_filter_from_ui(next->texture_filter);
|
||||
if (g_mod_texfilter < 0) gr_set_texture_filter(g_video_texfilter);
|
||||
}
|
||||
if (m & PSX_VIDEO_LIVE_FMV_FILTER) {
|
||||
g_video_fmv_filter = next->fmv_filter;
|
||||
if (g_gl_active) gl_renderer_set_fmv_filter(g_video_fmv_filter);
|
||||
}
|
||||
if (m & PSX_VIDEO_LIVE_SCANLINES)
|
||||
psx_video_set_scanlines(next->scanlines, next->scanline_strength_pct / 100.0f);
|
||||
if (m & PSX_VIDEO_LIVE_PRESENT_LINEAR) g_video_aa = next->present_linear != 0;
|
||||
if (m & PSX_VIDEO_LIVE_DYNRES) {
|
||||
g_video_dynres = next->dynamic_resolution ? 1 : 0;
|
||||
if (!g_video_dynres) g_dynres.active = false;
|
||||
else { dynres_setup(); if (!g_dynres.active && gl_renderer_dynamic_resolution_ceiling() >= 2) g_dynres.active = true; }
|
||||
}
|
||||
if ((m & PSX_VIDEO_LIVE_FRAME_GEN) && g_gl_active) {
|
||||
g_frame_generation = next->frame_generation ? 1 : 0;
|
||||
gl_renderer_set_frame_generation(g_frame_generation);
|
||||
}
|
||||
/* Persist everything the menu shows, live or not, like the launcher. */
|
||||
if (!g_overlay_settings_path.empty()) {
|
||||
try {
|
||||
PSXRecompV4::UserSettings us = PSXRecompV4::load_user_settings(g_overlay_settings_path);
|
||||
us.fullscreen = next->fullscreen; us.has_fullscreen = true;
|
||||
us.texture_filter = next->texture_filter; us.has_texture_filter = true;
|
||||
us.fmv_filter = next->fmv_filter; us.has_fmv_filter = true;
|
||||
us.scanlines = next->scanlines != 0; us.has_scanlines = true;
|
||||
us.scanline_strength = next->scanline_strength_pct / 100.0; us.has_scanline_strength = true;
|
||||
us.antialiasing = next->present_linear != 0; us.has_antialiasing = true;
|
||||
us.dynamic_resolution = next->dynamic_resolution != 0; us.has_dynamic_resolution = true;
|
||||
us.frame_generation = next->frame_generation != 0; us.has_frame_generation = true;
|
||||
us.renderer = next->renderer; us.has_renderer = true;
|
||||
us.supersampling = next->supersampling; us.has_supersampling = true;
|
||||
us.render_thread = next->render_thread != 0; us.has_render_thread = true;
|
||||
us.present_thread = next->present_thread != 0; us.has_present_thread = true;
|
||||
us.internal_resolution = next->internal_resolution; us.has_internal_resolution = true;
|
||||
(void)PSXRecompV4::save_user_settings(g_overlay_settings_path, us);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
return psx_host_video_restart_bits(&g_video_boot, next) | deferred;
|
||||
}
|
||||
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
/* ---- Live rendering A/B keys (debug-tools builds) --------------------------
|
||||
* PSX_DEBUG_TOGGLES=0 turns them off. Each key flips one rendering feature
|
||||
@@ -15843,7 +16081,397 @@ static uint32_t gcs_gate_read_ram(uint32_t phys, uint32_t size) {
|
||||
return v;
|
||||
}
|
||||
|
||||
/* ---- In-game menu host (psx_menu_host.h) -----------------------------------
|
||||
* The runtime side of the provider-neutral in-game menu contract. A provider
|
||||
* (registered by the title with psx_menu_set_provider; none by default) draws
|
||||
* over the game (P2), takes input first (P3), and asks the host for pause (P1;
|
||||
* refused, so non-pausing, in netplay), live video (P4), mod changes (P5),
|
||||
* disc/tray actions, and session exits. Requests from the GL thread are posted
|
||||
* here and applied on the main thread in host_pause_service(). The same
|
||||
* policies apply to every provider. PSX_OVERLAY=0 turns the menu off. */
|
||||
#include <mutex>
|
||||
#if defined(__has_include)
|
||||
#if __has_include("psx_jogcon_ffb_sdl.h")
|
||||
#include "psx_jogcon_ffb_sdl.h" /* psxrecomp#609: wheel FFB player setting */
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* main()'s arguments, for the in-game menu's seamless relaunch. */
|
||||
static int g_main_argc = 0;
|
||||
static char** g_main_argv = nullptr;
|
||||
|
||||
namespace {
|
||||
std::mutex g_ovl_mu;
|
||||
const PsxMenuProvider* g_ovl = nullptr; /* attached provider */
|
||||
bool g_ovl_on = false;
|
||||
std::atomic<int> g_ovl_pause_wanted{0};
|
||||
bool g_ovl_pause_held = false;
|
||||
std::atomic<int> g_ovl_reload_binds{0};
|
||||
std::atomic<int> g_ovl_exit{0}; /* PSX_MENU_EXIT_* + 1; 0 = none */
|
||||
bool g_ovl_video_pending = false;
|
||||
PsxHostVideoSettings g_ovl_video_post{};
|
||||
PsxHostVideoSettings g_ovl_video_current{}, g_ovl_video_boot{};
|
||||
bool g_ovl_netplay = false;
|
||||
|
||||
/* Only the main thread reads live runtime state. Provider draw callbacks
|
||||
* consume a coherent snapshot under the same mutex as their posted edits. */
|
||||
void mh_refresh_snapshot() {
|
||||
PsxHostVideoSettings current;
|
||||
psx_video_current(¤t);
|
||||
const bool netplay = psx_netplay_active() != 0;
|
||||
std::lock_guard<std::mutex> lk(g_ovl_mu);
|
||||
g_ovl_video_current = current;
|
||||
g_ovl_video_boot = g_video_boot;
|
||||
g_ovl_netplay = netplay;
|
||||
}
|
||||
int mh_netplay_active(void) {
|
||||
std::lock_guard<std::mutex> lk(g_ovl_mu);
|
||||
return g_ovl_netplay ? 1 : 0;
|
||||
}
|
||||
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
bool g_ovl_ls_pending = false;
|
||||
RecompLauncherCSettings g_ovl_ls_post{};
|
||||
#endif
|
||||
std::string g_ovl_argv0, g_ovl_name, g_ovl_keybinds, g_ovl_config, g_ovl_assets;
|
||||
|
||||
bool ovl_open(void) { return g_ovl && g_ovl->is_open && g_ovl->is_open(g_ovl->ctx); }
|
||||
|
||||
int mh_set_paused(int paused) {
|
||||
if (paused && mh_netplay_active()) return 0;
|
||||
g_ovl_pause_wanted = paused ? 1 : 0;
|
||||
return 1;
|
||||
}
|
||||
void mh_video_get(PsxHostVideoSettings* out) {
|
||||
if (!out) return;
|
||||
std::lock_guard<std::mutex> lk(g_ovl_mu);
|
||||
*out = g_ovl_video_current;
|
||||
}
|
||||
unsigned mh_restart_bits(const PsxHostVideoSettings& next) {
|
||||
std::lock_guard<std::mutex> lk(g_ovl_mu);
|
||||
const PsxHostVideoSettings now = g_ovl_video_current;
|
||||
unsigned out = psx_host_video_restart_bits(&g_ovl_video_boot, &next);
|
||||
/* In a netplay match only presentation applies live (psx_video_apply_live);
|
||||
* the rest waits for the next session. */
|
||||
if (g_ovl_netplay &&
|
||||
(psx_host_video_live_changes(&now, &next) &
|
||||
~(PSX_VIDEO_LIVE_FULLSCREEN | PSX_VIDEO_LIVE_SCANLINES | PSX_VIDEO_LIVE_PRESENT_LINEAR)))
|
||||
out |= PSX_MENU_RESTART_AFTER_MATCH;
|
||||
return out;
|
||||
}
|
||||
unsigned mh_video_apply(const PsxHostVideoSettings* next) {
|
||||
if (!next) return 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_ovl_mu);
|
||||
g_ovl_video_post = *next;
|
||||
g_ovl_video_pending = true;
|
||||
}
|
||||
return mh_restart_bits(*next);
|
||||
}
|
||||
struct OvlModSet { std::string p, f, o, v; };
|
||||
std::vector<OvlModSet> g_ovl_mod_posts;
|
||||
/* The provider never touches the mod catalogue itself (it may run on the GL
|
||||
* thread): every change is posted here, recorded in the catalogue and saved
|
||||
* to mods/state.toml on the main thread, and handed to a live-capable plugin.
|
||||
* option == NULL is the feature switch (value "true"/"false"): restart. */
|
||||
int mh_mod_set(const char* p, const char* f, const char* o, const char* v) {
|
||||
if (!p || !f || !v) return 0;
|
||||
if (psx_netplay_active()) return 0; /* the plan is negotiated with peers */
|
||||
std::lock_guard<std::mutex> lk(g_ovl_mu);
|
||||
g_ovl_mod_posts.push_back({p, f, o ? o : "", v});
|
||||
return o ? psx_mod_option_live_capable(p, f) : 0;
|
||||
}
|
||||
void mh_reload_bindings(void) { g_ovl_reload_binds = 1; }
|
||||
void mh_request_exit(int how) {
|
||||
g_ovl_exit = (how < PSX_MENU_EXIT_QUIT || how > PSX_MENU_EXIT_RELAUNCH ? PSX_MENU_EXIT_QUIT : how) + 1;
|
||||
}
|
||||
/* Disc/tray: the runtime's production tray policy (psx_disc_tray.h, #652)
|
||||
* when the build has it -- the same lid cycle, netplay refusal, persistence
|
||||
* and event ring as the runtime's own disc menu and hotkeys -- else whatever
|
||||
* published itself through psx_menu_set_disc_ops. */
|
||||
#if defined(__has_include)
|
||||
#if __has_include("psx_disc_tray.h")
|
||||
#include "psx_disc_tray.h"
|
||||
#define PSX_MENU_HAS_DISC_TRAY 1
|
||||
#endif
|
||||
#endif
|
||||
#if defined(PSX_MENU_HAS_DISC_TRAY)
|
||||
int mh_disc_count(void) { return psx_disc_tray_disc_count(); }
|
||||
int mh_disc_mounted(void) { return psx_disc_tray_mounted_disc(); }
|
||||
const char* mh_disc_path(int n) { return psx_disc_tray_disc_path(n); }
|
||||
int disc_cycle_now(int n) { return psx_disc_tray_cycle(n, PSX_DISC_TRAY_ORIGIN_MENU); }
|
||||
#else
|
||||
int mh_disc_count(void) { const PsxMenuDiscOps* d = psx_menu_disc_ops(); return d && d->count ? d->count() : 0; }
|
||||
int mh_disc_mounted(void) { const PsxMenuDiscOps* d = psx_menu_disc_ops(); return d && d->mounted ? d->mounted() : 0; }
|
||||
const char* mh_disc_path(int n) { const PsxMenuDiscOps* d = psx_menu_disc_ops(); return d && d->path ? d->path(n) : nullptr; }
|
||||
int disc_cycle_now(int n) { const PsxMenuDiscOps* d = psx_menu_disc_ops(); return d && d->insert ? d->insert(n) : -1; }
|
||||
#endif
|
||||
/* The provider may ask from its draw (GL) thread; the lid cycle runs on the
|
||||
* main/emulation thread at the next host_pause_service. Returns 0 when
|
||||
* queued, -1 when there is no disc to change to, or the policy's netplay
|
||||
* refusal up front. */
|
||||
std::atomic<int> g_ovl_disc_req{0};
|
||||
int mh_disc_insert(int n) {
|
||||
if (n < 1 || n > mh_disc_count()) return -1;
|
||||
if (psx_netplay_active()) return 3; /* PSX_DISC_TRAY_ERR_NETPLAY */
|
||||
g_ovl_disc_req = n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
/* Shared launcher ABI helpers: the same translations the pre-game launcher
|
||||
* handoff uses (texture filter, FMV filter), so the two cannot drift. */
|
||||
const char* const kOvlRenderers[] = {"Software", "OpenGL", "Vulkan"};
|
||||
PsxHostVideoSettings ovl_video_from(const RecompLauncherCSettings& ls) {
|
||||
PsxHostVideoSettings v;
|
||||
mh_video_get(&v); /* keeps fields the menu does not show */
|
||||
v.fullscreen = ls.fullscreen ? (v.fullscreen ? v.fullscreen : 1) : 0;
|
||||
v.texture_filter = psx_texture_filter_from_ui(ls.texture_filter);
|
||||
v.fmv_filter = launcher_fmv_filter_to_cfg(ls.fmv_filter);
|
||||
v.scanlines = ls.scanlines ? 1 : 0;
|
||||
v.scanline_strength_pct = ls.scanline_strength_pct;
|
||||
v.present_linear = ls.antialiasing ? 1 : 0;
|
||||
v.dynamic_resolution = ls.dynamic_resolution ? 1 : 0;
|
||||
v.frame_generation = ls.frame_generation ? 1 : 0;
|
||||
v.renderer = ls.renderer;
|
||||
v.render_thread = ls.render_thread ? 1 : 0;
|
||||
v.present_thread = ls.present_thread ? 1 : 0;
|
||||
return v;
|
||||
}
|
||||
void mh_launcher_settings_get(RecompLauncherCSettings* out) {
|
||||
if (!out) return;
|
||||
PsxHostVideoSettings v;
|
||||
mh_video_get(&v);
|
||||
RecompLauncherCSettings& ls = *out;
|
||||
ls.fullscreen = v.fullscreen;
|
||||
ls.texture_filter = v.texture_filter;
|
||||
ls.fmv_filter = cfg_fmv_filter_to_launcher(v.fmv_filter);
|
||||
ls.scanlines = v.scanlines;
|
||||
ls.scanline_strength_pct = v.scanline_strength_pct;
|
||||
ls.antialiasing = v.present_linear;
|
||||
ls.dynamic_resolution = v.dynamic_resolution;
|
||||
ls.frame_generation = v.frame_generation;
|
||||
ls.renderer = v.renderer;
|
||||
ls.render_thread = v.render_thread;
|
||||
ls.present_thread = v.present_thread;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_JOGCON_FORCE_FEEDBACK) && \
|
||||
defined(PSX_HAS_JOGCON_FFB_PLAYER_SETTING)
|
||||
ls.jogcon_force_feedback = psx_jogcon_ffb_player_enabled();
|
||||
#endif
|
||||
for (int i = 0; i < PSX_MAX_PLAYERS && i < RECOMP_LAUNCHER_MAX_PLAYERS; ++i) {
|
||||
ls.player_src[i] = g_players[i].kind;
|
||||
ls.pad_mode[i] = PSXRecompV4::PAD_MODE_ANALOG;
|
||||
}
|
||||
}
|
||||
unsigned mh_launcher_settings_apply(const RecompLauncherCSettings* s) {
|
||||
if (!s) return 0;
|
||||
const PsxHostVideoSettings next = ovl_video_from(*s);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_ovl_mu);
|
||||
g_ovl_ls_post = *s;
|
||||
g_ovl_ls_pending = true;
|
||||
}
|
||||
return mh_restart_bits(next);
|
||||
}
|
||||
void mh_launcher_game_info(RecompLauncherCGameInfo* out) {
|
||||
if (!out) return;
|
||||
RecompLauncherCGameInfo& gi = *out;
|
||||
launcher_profile_apply("psx", &gi);
|
||||
gi.name = g_ovl_name.c_str();
|
||||
gi.keybinds_path = g_ovl_keybinds.c_str();
|
||||
gi.config_path = g_ovl_config.c_str();
|
||||
gi.num_players = PSX_MAX_PLAYERS < 4 ? PSX_MAX_PLAYERS : 4;
|
||||
gi.renderer_labels = kOvlRenderers;
|
||||
gi.num_renderers = 2;
|
||||
gi.has_supersampling = 0; /* restart-only; set before boot */
|
||||
gi.has_dynamic_resolution = 1;
|
||||
gi.has_render_pipeline = 1;
|
||||
gi.has_vsync = 0;
|
||||
gi.has_rewind_depth = 0;
|
||||
gi.mods = PSXRecompV4::mod_runtime_launcher_provider();
|
||||
#if defined(PSX_HAS_RECOMP_NET) && defined(PSX_HAS_LOBBY_CLIENT)
|
||||
/* The same lobby the pre-game launcher drives, so the menu can host/join. */
|
||||
gi.netplay_supported = g_lnch_netplay_available ? 1 : 0;
|
||||
gi.netplay = g_lnch_netplay_available ? &g_lnch_netplay_callbacks : nullptr;
|
||||
#endif
|
||||
}
|
||||
#endif /* RECOMP_LAUNCHER */
|
||||
|
||||
PsxMenuHost g_menu_host = {
|
||||
PSX_MENU_ABI_VERSION, mh_set_paused, mh_netplay_active, mh_video_get, mh_video_apply,
|
||||
mh_mod_set, mh_reload_bindings, mh_request_exit,
|
||||
mh_disc_count, mh_disc_mounted, mh_disc_path, mh_disc_insert,
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
reinterpret_cast<void (*)(struct RecompLauncherCGameInfo*)>(mh_launcher_game_info),
|
||||
reinterpret_cast<void (*)(struct RecompLauncherCSettings*)>(mh_launcher_settings_get),
|
||||
reinterpret_cast<unsigned (*)(const struct RecompLauncherCSettings*)>(mh_launcher_settings_apply),
|
||||
#else
|
||||
nullptr, nullptr, nullptr,
|
||||
#endif
|
||||
nullptr,
|
||||
};
|
||||
|
||||
int ovl_sink(const void* ev, void*) { return g_ovl && g_ovl->event ? g_ovl->event(g_ovl->ctx, ev) : 0; }
|
||||
void ovl_draw(int w, int h, void*) { if (g_ovl && g_ovl->draw) g_ovl->draw(g_ovl->ctx, w, h); }
|
||||
} // namespace
|
||||
|
||||
/* Main thread, once per GL context (windowed OpenGL only). */
|
||||
static void recomp_overlay_setup(const char* argv0, const std::string& title) {
|
||||
const char* e = std::getenv("PSX_OVERLAY");
|
||||
const PsxMenuProvider* p = psx_menu_provider();
|
||||
if (!p || g_ovl_on || !g_gl_active || g_headless || (e && *e == '0')) return;
|
||||
const std::filesystem::path dir = exe_dir_from_argv(argv0);
|
||||
g_ovl_name = title;
|
||||
g_ovl_argv0 = argv0 ? argv0 : "";
|
||||
g_ovl_keybinds = (dir / "keybinds.ini").string();
|
||||
g_ovl_config = (dir / "config.ini").string();
|
||||
g_ovl_assets = (dir / "assets").string();
|
||||
g_menu_host.assets_dir = g_ovl_assets.c_str();
|
||||
mh_refresh_snapshot();
|
||||
if (!p->attach || !p->attach(p->ctx, sdl_window, SDL_GL_GetCurrentContext(), &g_menu_host)) return;
|
||||
g_ovl = p;
|
||||
psx_host_set_input_sink(ovl_sink, nullptr);
|
||||
psx_host_overlay_set_draw_cb(ovl_draw, nullptr);
|
||||
g_ovl_on = true;
|
||||
std::fprintf(stdout, "psxrecomp: in-game menu ready (%s)\n", p->name ? p->name : "provider");
|
||||
}
|
||||
|
||||
/* Before the GL context goes away (session teardown), with it current: the
|
||||
* draw hook is removed first (it waits for an in-flight draw), then the
|
||||
* provider frees its GL objects. setup attaches again on the next context. */
|
||||
static void recomp_overlay_detach(void) {
|
||||
if (!g_ovl_on) return;
|
||||
psx_host_overlay_set_draw_cb(nullptr, nullptr);
|
||||
psx_host_set_input_sink(nullptr, nullptr);
|
||||
psx_host_set_ui_capture(0);
|
||||
if (g_ovl_pause_held) { psx_host_pause_pop(); g_ovl_pause_held = false; }
|
||||
g_ovl_pause_wanted = 0;
|
||||
if (g_ovl->detach) g_ovl->detach(g_ovl->ctx);
|
||||
g_ovl = nullptr;
|
||||
g_ovl_on = false;
|
||||
}
|
||||
|
||||
static void host_pause_service(void) {
|
||||
if (!g_ovl_on) return;
|
||||
mh_refresh_snapshot();
|
||||
psx_host_set_ui_capture(ovl_open());
|
||||
const bool want = g_ovl_pause_wanted != 0 && ovl_open();
|
||||
if (want && !g_ovl_pause_held) g_ovl_pause_held = psx_host_pause_push("overlay") != 0;
|
||||
else if (!want && g_ovl_pause_held) { psx_host_pause_pop(); g_ovl_pause_held = false; }
|
||||
PsxHostVideoSettings video{};
|
||||
bool have_video = false;
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
RecompLauncherCSettings post{};
|
||||
bool have_ls = false;
|
||||
#endif
|
||||
std::vector<OvlModSet> mods;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_ovl_mu);
|
||||
if (g_ovl_video_pending) { video = g_ovl_video_post; have_video = true; g_ovl_video_pending = false; }
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
if (g_ovl_ls_pending) { post = g_ovl_ls_post; have_ls = true; g_ovl_ls_pending = false; }
|
||||
#endif
|
||||
mods.swap(g_ovl_mod_posts);
|
||||
}
|
||||
if (have_video) (void)psx_video_apply_live(&video);
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
if (have_ls) {
|
||||
const PsxHostVideoSettings v = ovl_video_from(post);
|
||||
(void)psx_video_apply_live(&v);
|
||||
#if defined(RECOMP_LAUNCHER_HAS_JOGCON_FORCE_FEEDBACK) && \
|
||||
defined(PSX_HAS_JOGCON_FFB_PLAYER_SETTING)
|
||||
/* Controls > Wheel force feedback (experimental): live, and saved to
|
||||
* settings.toml [controller] jogcon_force_feedback when it changed. */
|
||||
if ((post.jogcon_force_feedback != 0) != (psx_jogcon_ffb_player_enabled() != 0)) {
|
||||
psx_jogcon_ffb_set_player_enabled(post.jogcon_force_feedback);
|
||||
const auto settings_path =
|
||||
exe_dir_from_argv(g_ovl_argv0.c_str()) / "settings.toml";
|
||||
PSXRecompV4::UserSettings us = PSXRecompV4::load_user_settings(settings_path);
|
||||
us.jogcon_force_feedback = post.jogcon_force_feedback != 0;
|
||||
us.has_jogcon_force_feedback = true;
|
||||
(void)PSXRecompV4::save_user_settings(settings_path, us);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
if (!mods.empty()) {
|
||||
for (const OvlModSet& m : mods) {
|
||||
if (m.o.empty()) {
|
||||
(void)psx_mod_feature_enable(m.p.c_str(), m.f.c_str(), m.v == "true");
|
||||
} else {
|
||||
(void)psx_mod_feature_set_option(m.p.c_str(), m.f.c_str(), m.o.c_str(), m.v.c_str());
|
||||
(void)psx_mod_set_option_live(m.p.c_str(), m.f.c_str(), m.o.c_str(), m.v.c_str());
|
||||
}
|
||||
}
|
||||
(void)psx_mod_save_selection();
|
||||
}
|
||||
if (const int disc = g_ovl_disc_req.exchange(0)) {
|
||||
const int rc = disc_cycle_now(disc);
|
||||
std::fprintf(stdout, "psxrecomp: in-game menu disc %d -> %d\n", disc, rc);
|
||||
}
|
||||
if (g_ovl_reload_binds.exchange(0)) {
|
||||
psx_keybinds_init(g_ovl_argv0.c_str());
|
||||
load_input_config(g_ovl_argv0.c_str());
|
||||
}
|
||||
#if defined(RECOMP_LAUNCHER) && defined(PSX_HAS_RECOMP_NET) && defined(PSX_HAS_LOBBY_CLIENT)
|
||||
/* The frontend loop is not running during a game: pump the lobby here,
|
||||
* for every provider. Once a launch is pending, stop pumping: the pending
|
||||
* check freed the lobby's UDP port for the match and a pump would bind it
|
||||
* again; the session ends and the frontend loop starts the match. */
|
||||
if (g_lnch_netplay_available && !psx_netplay_active() && !g_ovl_launch_to_start) {
|
||||
const RecompLauncherCNetplayCallbacks* np = &g_lnch_netplay_callbacks;
|
||||
if (np->launch_pending && np->launch_pending(np->ctx)) {
|
||||
if (!g_ovl_exit.load()) g_ovl_exit = PSX_MENU_EXIT_TO_FRONTEND + 1;
|
||||
} else if (np->pump) {
|
||||
np->pump(np->ctx);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
mh_refresh_snapshot();
|
||||
if (const int code = g_ovl_exit.exchange(0)) {
|
||||
const int how = code - 1;
|
||||
if (how == PSX_MENU_EXIT_TO_FRONTEND) {
|
||||
/* The menu does not come along into the next session: close it
|
||||
* and drop its pause, or the match would start paused. */
|
||||
g_ovl_pause_wanted = 0;
|
||||
if (g_ovl_pause_held) { psx_host_pause_pop(); g_ovl_pause_held = false; }
|
||||
if (g_ovl->set_open) g_ovl->set_open(g_ovl->ctx, 0);
|
||||
if (psx_netplay_active()) {
|
||||
netplay_soft_exit("menu_to_frontend");
|
||||
} else {
|
||||
/* End this session and re-enter the frontend loop in-process
|
||||
* (the netplay soft-return path); it then starts whatever the
|
||||
* menu asked for, e.g. the lobby's match. */
|
||||
std::fprintf(stdout, "psxrecomp: in-game menu -> frontend loop\n");
|
||||
g_netplay_from_lobby = 1;
|
||||
g_ovl_launch_to_start = 1;
|
||||
psx_request_return_to_lobby();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (how == PSX_MENU_EXIT_RELAUNCH) {
|
||||
std::fprintf(stdout, "psxrecomp: in-game menu -> relaunch for restart-only settings\n");
|
||||
std::fflush(stdout);
|
||||
psx_crash_trace_set_exit_origin("menu_relaunch");
|
||||
shutdown_runtime();
|
||||
std::fprintf(stderr, "psxrecomp: relaunching\n");
|
||||
psx_relaunch_self(g_main_argc, g_main_argv);
|
||||
std::fprintf(stderr, "psxrecomp: relaunch failed (%s)\n", std::strerror(errno));
|
||||
std::exit(0); /* relaunch failed: leave rather than run torn down */
|
||||
}
|
||||
if (psx_netplay_active()) {
|
||||
netplay_soft_exit("menu_quit");
|
||||
} else {
|
||||
psx_crash_trace_set_exit_origin("menu_quit");
|
||||
shutdown_runtime();
|
||||
std::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_main_argc = argc;
|
||||
g_main_argv = argv;
|
||||
/* Force line-buffered output so messages appear even if killed. */
|
||||
std::setvbuf(stdout, nullptr, _IOLBF, BUFSIZ);
|
||||
std::setvbuf(stderr, nullptr, _IOLBF, BUFSIZ);
|
||||
@@ -15854,6 +16482,7 @@ int main(int argc, char** argv) {
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
launcher_boot_timing_mark("host:main_enter");
|
||||
#endif
|
||||
psx_host_pause_set_refuse_probe(host_pause_refuse_netplay);
|
||||
|
||||
/* --audit-mod-plugins <manifest-root>...: build-time check, no emulation.
|
||||
* Every trusted implementation this executable registered must be named
|
||||
@@ -15998,6 +16627,13 @@ int main(int argc, char** argv) {
|
||||
force_launcher = true;
|
||||
} else if (std::strcmp(argv[i], "--launcher") == 0) {
|
||||
force_launcher = true;
|
||||
/* Launchers that boot into the game (pages in the in-game menu)
|
||||
* show their pre-game window again when asked for explicitly. */
|
||||
#if defined(_WIN32)
|
||||
_putenv_s("RECOMP_LAUNCHER_PREGAME", "1");
|
||||
#else
|
||||
setenv("RECOMP_LAUNCHER_PREGAME", "1", 1);
|
||||
#endif
|
||||
} else if (std::strcmp(argv[i], "--no-launcher") == 0) {
|
||||
force_no_launcher = true;
|
||||
} else if (std::strcmp(argv[i], "--headless-opengl") == 0) {
|
||||
@@ -16053,6 +16689,12 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Test hook: a hidden game window that still runs the launcher (and its
|
||||
* in-game menu), unlike --hidden-window. */
|
||||
if (const char *e = std::getenv("PSX_HIDDEN_WINDOW")) {
|
||||
if (*e && *e != '0') g_hidden_window = 1;
|
||||
}
|
||||
|
||||
std::string default_game_config_storage;
|
||||
if (!game_config_path) {
|
||||
std::filesystem::path default_game_config =
|
||||
@@ -16753,6 +17395,7 @@ int main(int argc, char** argv) {
|
||||
exe_dir_from_argv(argv[0]) / "settings.toml";
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
g_lnch_settings_path = settings_path;
|
||||
g_overlay_settings_path = settings_path;
|
||||
#endif
|
||||
const PSXRecompV4::UserSettings us =
|
||||
PSXRecompV4::load_user_settings(settings_path);
|
||||
@@ -19309,6 +19952,8 @@ session_reboot:
|
||||
gl_renderer_set_post_shader(g_video_post_shader.c_str());
|
||||
g_gl_active = (gl_renderer_init_context(sdl_window) != 0);
|
||||
dynres_setup();
|
||||
psx_video_current(&g_video_boot); /* boot snapshot for restart-only rows */
|
||||
recomp_overlay_setup(argv[0], s_picker_game_name);
|
||||
if (dbg_live_scale && g_gl_active) {
|
||||
if (!dynres_requested()) g_dynres.active = false;
|
||||
const int ceil_s = gl_renderer_dynamic_resolution_ceiling();
|
||||
@@ -20068,7 +20713,8 @@ soft_return_lobby:
|
||||
teardown_game_session_keep_lobby();
|
||||
PSXRecompV4::mod_runtime_end_netplay();
|
||||
#if defined(RECOMP_LAUNCHER) && defined(PSX_HAS_LOBBY_CLIENT)
|
||||
ae_np_prepare_lobby_rematch();
|
||||
if (!g_ovl_launch_to_start) ae_np_prepare_lobby_rematch();
|
||||
g_ovl_launch_to_start = 0;
|
||||
/* Say why the match ended on the launcher's status line; the
|
||||
* room and browser views both show last_error. Cleared when the player
|
||||
* ended the match themselves. */
|
||||
|
||||
+160
-40
@@ -927,6 +927,53 @@ bool materialize_derived_disc(RuntimeMods& s, const ModResolution& plan,
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
int mutate(Callback callback) {
|
||||
state().ticket_ready = false;
|
||||
++state().preparation_revision;
|
||||
std::string error;
|
||||
if (!callback(error)) {
|
||||
set_error(error);
|
||||
return 0;
|
||||
}
|
||||
if (!state().disc_path.empty())
|
||||
state().validation = state().manager.resolve(
|
||||
state().game_id, state().exe_sha256, state().disc_sha256, true);
|
||||
else
|
||||
state().validation = {};
|
||||
state().error.clear();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Feature switch / option edits shared by the launcher ABI provider and the
|
||||
* in-game menu host (psx_mod_feature_enable / psx_mod_feature_set_option). */
|
||||
int feature_enable_impl(const char* package_id, const char* feature_id, int enabled) {
|
||||
if (!package_id || !feature_id) return 0;
|
||||
return mutate([&](std::string& error) {
|
||||
const ModFeature* feature =
|
||||
state().manager.selected_feature(package_id, feature_id);
|
||||
if (feature && feature->legacy)
|
||||
return state().manager.set_enabled(
|
||||
package_id, enabled != 0, &error);
|
||||
return state().manager.set_feature_enabled(
|
||||
package_id, feature_id, enabled != 0, &error);
|
||||
});
|
||||
}
|
||||
|
||||
int feature_set_option_impl(const char* package_id, const char* feature_id,
|
||||
const char* option_id, const char* value) {
|
||||
if (!package_id || !feature_id || !option_id || !value) return 0;
|
||||
return mutate([&](std::string& error) {
|
||||
const ModFeature* feature =
|
||||
state().manager.selected_feature(package_id, feature_id);
|
||||
if (feature && feature->legacy)
|
||||
return state().manager.set_option(
|
||||
package_id, option_id, value, &error);
|
||||
return state().manager.set_feature_option(
|
||||
package_id, feature_id, option_id, value, &error);
|
||||
});
|
||||
}
|
||||
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
void copy_text(char* out, size_t capacity, const std::string& value) {
|
||||
if (!out || capacity == 0) return;
|
||||
@@ -1166,32 +1213,14 @@ int provider_feature_choice_get(void*, const char* package_id,
|
||||
|
||||
int provider_feature_enable(void*, const char* package_id,
|
||||
const char* feature_id, int enabled) {
|
||||
if (!package_id || !feature_id) return 0;
|
||||
return mutate([&](std::string& error) {
|
||||
const ModFeature* feature =
|
||||
state().manager.selected_feature(package_id, feature_id);
|
||||
if (feature && feature->legacy)
|
||||
return state().manager.set_enabled(
|
||||
package_id, enabled != 0, &error);
|
||||
return state().manager.set_feature_enabled(
|
||||
package_id, feature_id, enabled != 0, &error);
|
||||
});
|
||||
return feature_enable_impl(package_id, feature_id, enabled);
|
||||
}
|
||||
|
||||
int provider_feature_set_option(void*, const char* package_id,
|
||||
const char* feature_id,
|
||||
const char* option_id,
|
||||
const char* value) {
|
||||
if (!package_id || !feature_id || !option_id || !value) return 0;
|
||||
return mutate([&](std::string& error) {
|
||||
const ModFeature* feature =
|
||||
state().manager.selected_feature(package_id, feature_id);
|
||||
if (feature && feature->legacy)
|
||||
return state().manager.set_option(
|
||||
package_id, option_id, value, &error);
|
||||
return state().manager.set_feature_option(
|
||||
package_id, feature_id, option_id, value, &error);
|
||||
});
|
||||
return feature_set_option_impl(package_id, feature_id, option_id, value);
|
||||
}
|
||||
|
||||
int provider_feature_resource_count(void*, const char* package_id,
|
||||
@@ -1317,23 +1346,6 @@ int provider_version_get(void*, const char* package_id, int index,
|
||||
return 1;
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
int mutate(Callback callback) {
|
||||
state().ticket_ready = false;
|
||||
++state().preparation_revision;
|
||||
std::string error;
|
||||
if (!callback(error)) {
|
||||
set_error(error);
|
||||
return 0;
|
||||
}
|
||||
if (!state().disc_path.empty())
|
||||
state().validation = state().manager.resolve(
|
||||
state().game_id, state().exe_sha256, state().disc_sha256, true);
|
||||
else
|
||||
state().validation = {};
|
||||
state().error.clear();
|
||||
return 1;
|
||||
}
|
||||
|
||||
int provider_install(void*, const char* path) {
|
||||
if (!path) return 0;
|
||||
@@ -1462,6 +1474,12 @@ bool mod_runtime_netplay_content_negotiation() {
|
||||
return netplay_content_negotiation_enabled;
|
||||
}
|
||||
|
||||
/* Live option overrides (psx_mod_set_option_live) belong to one session's
|
||||
* plan: initialising the runtime, committing a plan or switching to the
|
||||
* netplay plan drops them, so a new plan's committed values are what the
|
||||
* API answers (defined with the overrides below). */
|
||||
void reset_live_option_overrides();
|
||||
|
||||
bool mod_runtime_initialize(const std::filesystem::path& root,
|
||||
const std::string& game_id,
|
||||
uint32_t game_entry_pc,
|
||||
@@ -1480,6 +1498,7 @@ bool mod_runtime_initialize(const std::filesystem::path& root,
|
||||
s.audio_tracks.clear();
|
||||
s.extent_start = 0;
|
||||
s.plan = {};
|
||||
reset_live_option_overrides(); /* a new plan ends the last session's live values */
|
||||
s.validation = {};
|
||||
s.raw_disc_index.clear();
|
||||
s.user_disc_index.clear();
|
||||
@@ -1586,6 +1605,7 @@ bool mod_runtime_clear_for_netplay(std::string* error) {
|
||||
s.audio_tracks.clear();
|
||||
s.extent_start = 0;
|
||||
s.plan = {};
|
||||
reset_live_option_overrides(); /* a new plan ends the last session's live values */
|
||||
s.validation = {};
|
||||
s.raw_disc_index.clear();
|
||||
s.user_disc_index.clear();
|
||||
@@ -1740,6 +1760,7 @@ bool mod_runtime_commit_netplay(const std::filesystem::path& disc_path,
|
||||
if (renderer) plan.plugins.push_back({renderer, "", ""});
|
||||
s.disc_path = disc_path;
|
||||
s.plan = std::move(plan);
|
||||
reset_live_option_overrides(); /* a new plan ends the last session's live values */
|
||||
mod_runtime_set_session_plan_fp({});
|
||||
psx_mod_netplay_set_active(1);
|
||||
std::fprintf(stdout, "psxrecomp: title netplay profile %s (%s)\n",
|
||||
@@ -2067,6 +2088,7 @@ static bool install_plan(RuntimeMods& s, ModResolution plan,
|
||||
s.audio_tracks.clear();
|
||||
s.extent_start = 0;
|
||||
s.plan = std::move(plan);
|
||||
reset_live_option_overrides(); /* a new plan ends the last session's live values */
|
||||
{
|
||||
HostLaunchTimingScope index_timing(HOST_LAUNCH_BUILD_DISC_INDEX);
|
||||
build_disc_index(s);
|
||||
@@ -2824,6 +2846,92 @@ extern "C" int psx_mod_game_started(void) {
|
||||
return fntrace_is_game_started();
|
||||
}
|
||||
|
||||
/* ---- Live option overrides (in-game overlay P5) ------------------------- */
|
||||
namespace {
|
||||
std::map<std::string, std::string>& live_option_values() {
|
||||
static std::map<std::string, std::string> m;
|
||||
return m;
|
||||
}
|
||||
std::map<std::string, PSXModOptionChangedCallback>& option_changed_plugins() {
|
||||
static std::map<std::string, PSXModOptionChangedCallback> m;
|
||||
return m;
|
||||
}
|
||||
std::string live_option_key(const char* p, const char* f, const char* o) {
|
||||
return std::string(p) + '\x1f' + f + '\x1f' + o;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" int psx_mod_register_option_changed_plugin(const char* id,
|
||||
PSXModOptionChangedCallback cb) {
|
||||
if (!id || !*id || !cb) return 0;
|
||||
option_changed_plugins()[id] = cb;
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_option_live_capable(const char* package_id, const char* feature_id) {
|
||||
using namespace PSXRecompV4;
|
||||
if (!package_id || !feature_id) return 0;
|
||||
RuntimeMods& s = state();
|
||||
if (!s.initialized || !s.plan.ok) return 0;
|
||||
for (const ModResolution::Plugin& plugin : s.plan.plugins)
|
||||
if (plugin.package_id == package_id && plugin.feature_id == feature_id &&
|
||||
option_changed_plugins().count(plugin.id))
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_save_selection(void) {
|
||||
using namespace PSXRecompV4;
|
||||
RuntimeMods& s = state();
|
||||
if (!s.initialized) return 0;
|
||||
std::string error;
|
||||
if (!s.manager.save_state(&error)) {
|
||||
std::fprintf(stderr, "psxrecomp: mods: could not save state.toml: %s\n", error.c_str());
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
namespace PSXRecompV4 {
|
||||
void reset_live_option_overrides() { live_option_values().clear(); }
|
||||
} // namespace PSXRecompV4
|
||||
|
||||
extern "C" int psx_mod_set_option_live(const char* package_id, const char* feature_id,
|
||||
const char* option_id, const char* value) {
|
||||
using namespace PSXRecompV4;
|
||||
if (!package_id || !feature_id || !option_id || !value) return 0;
|
||||
if (psx_netplay_active()) return -1;
|
||||
RuntimeMods& s = state();
|
||||
if (!s.initialized || !s.plan.ok) return 0;
|
||||
bool in_plan = false;
|
||||
for (const ModResolution::Plugin& plugin : s.plan.plugins)
|
||||
if (plugin.package_id == package_id && plugin.feature_id == feature_id) in_plan = true;
|
||||
if (!in_plan || !psx_mod_option_live_capable(package_id, feature_id)) return 0;
|
||||
/* The value is visible to the callbacks while they run (a plugin may
|
||||
* re-read it through psx_mod_current_option_value), but it is kept only
|
||||
* if one of them accepts. Refused by all = nothing changes this session:
|
||||
* the previous value (live or committed) is restored. */
|
||||
auto& values = live_option_values();
|
||||
const std::string key = live_option_key(package_id, feature_id, option_id);
|
||||
auto prev_it = values.find(key);
|
||||
const bool had_prev = prev_it != values.end();
|
||||
const std::string prev = had_prev ? prev_it->second : std::string();
|
||||
values[key] = value;
|
||||
int live = 0;
|
||||
for (const ModResolution::Plugin& plugin : s.plan.plugins) {
|
||||
if (plugin.package_id != package_id || plugin.feature_id != feature_id) continue;
|
||||
auto it = option_changed_plugins().find(plugin.id);
|
||||
if (it == option_changed_plugins().end()) continue;
|
||||
PluginCallbackScope scope(s, &plugin);
|
||||
if (it->second(option_id, value)) live = 1;
|
||||
}
|
||||
if (!live) {
|
||||
if (had_prev) values[key] = prev;
|
||||
else values.erase(key);
|
||||
}
|
||||
return live;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_option_value(const char* package_id,
|
||||
const char* feature_id,
|
||||
const char* option_id,
|
||||
@@ -2839,9 +2947,11 @@ extern "C" int psx_mod_option_value(const char* package_id,
|
||||
* treat an empty string as a value. */
|
||||
if (!s.initialized || !s.plan.ok) return 0;
|
||||
/* Read the committed plan's selection, which includes features its
|
||||
* [[requirement]]s activated, not live launcher state. */
|
||||
const std::string value = s.manager.feature_option_value(
|
||||
s.plan, package_id, feature_id, option_id);
|
||||
* [[requirement]]s activated, not live launcher state -- unless the
|
||||
* in-game menu changed it this session (psx_mod_set_option_live). */
|
||||
auto live = live_option_values().find(live_option_key(package_id, feature_id, option_id));
|
||||
const std::string value = live != live_option_values().end() ? live->second :
|
||||
s.manager.feature_option_value(s.plan, package_id, feature_id, option_id);
|
||||
if (value.empty()) return 0;
|
||||
if (value.size() + 1 > (size_t)out_size) return 0;
|
||||
std::memcpy(out, value.c_str(), value.size() + 1);
|
||||
@@ -3419,3 +3529,13 @@ extern "C" void mod_runtime_patch_disc_sector(uint32_t lba, int raw_sector,
|
||||
if (!PSXRecompV4::state().disc_enabled) return;
|
||||
patch_committed_disc_sector(lba, raw_sector, bytes, size);
|
||||
}
|
||||
|
||||
/* In-game menu host (psx_menu_host.h): the same feature edits as the launcher
|
||||
* ABI provider, without its types. Main thread. */
|
||||
extern "C" int psx_mod_feature_enable(const char* package_id, const char* feature_id, int enabled) {
|
||||
return PSXRecompV4::feature_enable_impl(package_id, feature_id, enabled);
|
||||
}
|
||||
extern "C" int psx_mod_feature_set_option(const char* package_id, const char* feature_id,
|
||||
const char* option_id, const char* value) {
|
||||
return PSXRecompV4::feature_set_option_impl(package_id, feature_id, option_id, value);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ int pad_ext_live(const PadExtGate *g) {
|
||||
return g && !g->injected_input && !g->headless && !g->netplay_active &&
|
||||
!g->netplay_resim && !g->selfcheck_locked && !g->selfcheck_resim &&
|
||||
!g->render_pass && !g->savestate_menu_open && !g->rewind_open &&
|
||||
!g->input_guard;
|
||||
!g->input_guard && !g->ui_capture;
|
||||
}
|
||||
|
||||
/* Stage 2b: run the title transform over the resolved pad and pack its
|
||||
|
||||
@@ -85,6 +85,7 @@ typedef struct PadExtGate {
|
||||
int savestate_menu_open;
|
||||
int rewind_open;
|
||||
int input_guard; /* savestate input guard armed */
|
||||
int ui_capture; /* host overlay owns input (host_overlay.h P3) */
|
||||
} PadExtGate;
|
||||
int pad_ext_live(const PadExtGate *g);
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/* psx_menu_host.c -- provider and disc-op registration (psx_menu_host.h). */
|
||||
#include "psx_menu_host.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
static const PsxMenuProvider *s_provider = NULL;
|
||||
static const PsxMenuDiscOps *s_disc = NULL;
|
||||
|
||||
int psx_menu_set_provider(const PsxMenuProvider *provider) {
|
||||
if (provider && provider->abi_version != PSX_MENU_ABI_VERSION) return 0;
|
||||
s_provider = provider;
|
||||
return 1;
|
||||
}
|
||||
|
||||
const PsxMenuProvider *psx_menu_provider(void) { return s_provider; }
|
||||
|
||||
void psx_menu_set_disc_ops(const PsxMenuDiscOps *ops) { s_disc = ops; }
|
||||
const PsxMenuDiscOps *psx_menu_disc_ops(void) { return s_disc; }
|
||||
@@ -67,6 +67,13 @@ extern "C" int psx_sdl_audio_resume(SDL_AudioDeviceID)
|
||||
: -1;
|
||||
}
|
||||
|
||||
extern "C" void psx_sdl_audio_set_paused(SDL_AudioDeviceID, int paused)
|
||||
{
|
||||
if (!s_audio_stream) return;
|
||||
if (paused) (void)SDL_PauseAudioStreamDevice(s_audio_stream);
|
||||
else (void)SDL_ResumeAudioStreamDevice(s_audio_stream);
|
||||
}
|
||||
|
||||
extern "C" void psx_sdl_audio_close(SDL_AudioDeviceID)
|
||||
{
|
||||
if (s_audio_stream) SDL_DestroyAudioStream(s_audio_stream);
|
||||
@@ -140,6 +147,11 @@ extern "C" int psx_sdl_audio_resume(SDL_AudioDeviceID device)
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" void psx_sdl_audio_set_paused(SDL_AudioDeviceID device, int paused)
|
||||
{
|
||||
SDL_PauseAudioDevice(device, paused ? 1 : 0);
|
||||
}
|
||||
|
||||
extern "C" void psx_sdl_audio_close(SDL_AudioDeviceID device)
|
||||
{
|
||||
SDL_CloseAudioDevice(device);
|
||||
|
||||
@@ -95,6 +95,9 @@ def main():
|
||||
if WINDOWS:
|
||||
sections.append("-flto")
|
||||
objs = []
|
||||
# The renderer calls the in-game overlay hook (host_overlay.c) at present.
|
||||
if (src / "host_overlay.c").exists():
|
||||
sources.append(("hov", src / "host_overlay.c"))
|
||||
for name, path in sources:
|
||||
o = dest / (name + ".o")
|
||||
r = run([args.cc, "-std=gnu11", "-O1", "-DPSX_SDL3=1", "-DPSX_NO_DEBUG_TOOLS=1",
|
||||
|
||||
@@ -77,6 +77,9 @@ def main():
|
||||
sources.append(("pth", framework / "runtime/src/present_thread.c"))
|
||||
if (framework / "runtime/src/frame_gen.c").exists():
|
||||
sources.append(("fg", framework / "runtime/src/frame_gen.c"))
|
||||
# The renderer calls the in-game overlay hook (host_overlay.c) at present.
|
||||
if (framework / "runtime/src" / "host_overlay.c").exists():
|
||||
sources.append(("hov", framework / "runtime/src" / "host_overlay.c"))
|
||||
for name, source in sources:
|
||||
if run([compiler / "gcc.exe", "-std=c11", "-O2", "-flto", "-DPSX_SDL3=1",
|
||||
"-DPSX_NO_DEBUG_TOOLS=1", *includes, "-c", source,
|
||||
|
||||
@@ -94,6 +94,9 @@ def main():
|
||||
if WINDOWS:
|
||||
sections.append("-flto")
|
||||
objs = []
|
||||
# The renderer calls the in-game overlay hook (host_overlay.c) at present.
|
||||
if (src / "host_overlay.c").exists():
|
||||
sources.append(("hov", src / "host_overlay.c"))
|
||||
for name, path in sources:
|
||||
o = dest / (name + ".o")
|
||||
r = run([args.cc, "-std=gnu11", "-O1", "-DPSX_SDL3=1", "-DPSX_NO_DEBUG_TOOLS=1",
|
||||
|
||||
@@ -154,6 +154,9 @@ def main():
|
||||
if passes:
|
||||
sections.append("-DPSX_TEST_RENDER_PASSES=1")
|
||||
objs = []
|
||||
# The renderer calls the in-game overlay hook (host_overlay.c) at present.
|
||||
if (framework / "runtime/src" / "host_overlay.c").exists():
|
||||
sources.append(("hov", framework / "runtime/src" / "host_overlay.c"))
|
||||
for name, src in sources:
|
||||
o = dest / (name + ".o")
|
||||
r = run([args.cc, "-std=gnu11", "-O1", "-DPSX_SDL3=1", "-DPSX_NO_DEBUG_TOOLS=1",
|
||||
|
||||
@@ -52,6 +52,9 @@ def main():
|
||||
if (fw/'runtime/src/frame_gen.c').exists():
|
||||
sources.append(('fg',fw/'runtime/src/frame_gen.c'))
|
||||
objects=[]
|
||||
# The renderer calls the in-game overlay hook (host_overlay.c) at present.
|
||||
if (fw/'runtime/src' / "host_overlay.c").exists():
|
||||
sources.append(("hov", fw/'runtime/src' / "host_overlay.c"))
|
||||
for name,source in sources:
|
||||
obj=out/(name+'.o'); run([args.cc,*flags,'-c',source,'-o',obj]); objects.append(obj)
|
||||
if args.hd_pack:
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/* test_host_overlay.c -- unit tests for host_overlay.c hooks. */
|
||||
#include "host_overlay.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
#endif
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(c) do { if (!(c)) { fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #c); g_fail++; } } while (0)
|
||||
|
||||
static int refuse_on = 0;
|
||||
static int refuse(void) { return refuse_on; }
|
||||
|
||||
static void test_pause(void) {
|
||||
CHECK(psx_host_pause_depth() == 0 && psx_host_pause_reason() == NULL);
|
||||
CHECK(psx_host_pause_push("overlay"));
|
||||
CHECK(psx_host_pause_push("other"));
|
||||
CHECK(psx_host_pause_depth() == 2 && strcmp(psx_host_pause_reason(), "other") == 0);
|
||||
psx_host_pause_pop();
|
||||
CHECK(strcmp(psx_host_pause_reason(), "overlay") == 0);
|
||||
psx_host_pause_pop();
|
||||
psx_host_pause_pop(); /* extra pop is harmless */
|
||||
CHECK(psx_host_pause_depth() == 0);
|
||||
psx_host_pause_set_refuse_probe(refuse);
|
||||
refuse_on = 1;
|
||||
CHECK(!psx_host_pause_push("netplay") && psx_host_pause_depth() == 0);
|
||||
refuse_on = 0;
|
||||
CHECK(psx_host_pause_push(NULL) && strcmp(psx_host_pause_reason(), "host") == 0);
|
||||
psx_host_pause_pop();
|
||||
for (int i = 0; i < 8; i++) CHECK(psx_host_pause_push("n"));
|
||||
CHECK(!psx_host_pause_push("overflow"));
|
||||
for (int i = 0; i < 8; i++) psx_host_pause_pop();
|
||||
}
|
||||
|
||||
static int draws = 0, last_w = 0, last_h = 0;
|
||||
static void *last_ctx = NULL;
|
||||
static void on_draw(int w, int h, void *ctx) { draws++; last_w = w; last_h = h; last_ctx = ctx; }
|
||||
|
||||
static void test_draw(void) {
|
||||
int tag = 7;
|
||||
CHECK(!psx_host_overlay_has_draw());
|
||||
psx_host_overlay_draw(640, 480); /* no callback: nothing happens */
|
||||
psx_host_overlay_set_draw_cb(on_draw, &tag);
|
||||
CHECK(psx_host_overlay_has_draw());
|
||||
psx_host_overlay_draw(1280, 800);
|
||||
CHECK(draws == 1 && last_w == 1280 && last_h == 800 && last_ctx == &tag);
|
||||
psx_host_overlay_draw(0, 800); /* minimised window: skipped */
|
||||
CHECK(draws == 1);
|
||||
psx_host_overlay_set_draw_cb(NULL, NULL);
|
||||
psx_host_overlay_draw(1280, 800);
|
||||
CHECK(draws == 1 && !psx_host_overlay_has_draw());
|
||||
}
|
||||
|
||||
/* Lifetime: once set_draw_cb(NULL) returns, the old callback never runs
|
||||
* again, so its context can be freed. A reader thread draws continuously;
|
||||
* the main thread unregisters and immediately marks the context dead. */
|
||||
#if !defined(_WIN32) || defined(__MINGW32__)
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
typedef struct { atomic_int alive; atomic_int calls; atomic_int used_dead; } LifeCtx;
|
||||
static atomic_int g_stop;
|
||||
static void life_draw(int w, int h, void* p) {
|
||||
LifeCtx* c = (LifeCtx*)p;
|
||||
(void)w; (void)h;
|
||||
for (volatile int i = 0; i < 2000; ++i) {} /* a frame's worth of work */
|
||||
if (!atomic_load(&c->alive)) atomic_fetch_add(&c->used_dead, 1);
|
||||
atomic_fetch_add(&c->calls, 1);
|
||||
}
|
||||
static void* life_reader(void* arg) {
|
||||
(void)arg;
|
||||
while (!atomic_load(&g_stop)) psx_host_overlay_draw(64, 64);
|
||||
return NULL;
|
||||
}
|
||||
static void test_draw_lifetime(void) {
|
||||
pthread_t t;
|
||||
atomic_store(&g_stop, 0);
|
||||
pthread_create(&t, NULL, life_reader, NULL);
|
||||
int dead_uses = 0, total = 0;
|
||||
for (int round = 0; round < 300; ++round) {
|
||||
LifeCtx* c = (LifeCtx*)calloc(1, sizeof(*c));
|
||||
atomic_store(&c->alive, 1);
|
||||
psx_host_overlay_set_draw_cb(life_draw, c);
|
||||
while (atomic_load(&c->calls) < 2) {}
|
||||
psx_host_overlay_set_draw_cb(NULL, NULL); /* must wait for the reader */
|
||||
atomic_store(&c->alive, 0); /* the owner tears down */
|
||||
for (volatile int i = 0; i < 20000; ++i) {}
|
||||
dead_uses += atomic_load(&c->used_dead);
|
||||
total += atomic_load(&c->calls);
|
||||
free(c);
|
||||
}
|
||||
atomic_store(&g_stop, 1);
|
||||
pthread_join(t, NULL);
|
||||
CHECK(total > 0);
|
||||
CHECK(dead_uses == 0);
|
||||
}
|
||||
#else
|
||||
static void test_draw_lifetime(void) {}
|
||||
#endif
|
||||
|
||||
/* Changing the callback from inside it must not deadlock. */
|
||||
static void self_unregister(int w, int h, void* p) {
|
||||
(void)w; (void)h;
|
||||
++*(int*)p;
|
||||
psx_host_overlay_set_draw_cb(NULL, NULL);
|
||||
}
|
||||
static void test_draw_reentrant(void) {
|
||||
int n = 0;
|
||||
psx_host_overlay_set_draw_cb(self_unregister, &n);
|
||||
psx_host_overlay_draw(64, 64);
|
||||
psx_host_overlay_draw(64, 64);
|
||||
CHECK(n == 1 && !psx_host_overlay_has_draw());
|
||||
}
|
||||
|
||||
static int sunk = 0;
|
||||
static int sink(const void *ev, void *ctx) { (void)ctx; sunk++; return *(const int *)ev == 1; }
|
||||
|
||||
static void test_sink(void) {
|
||||
int a = 1, b = 2;
|
||||
CHECK(!psx_host_input_sink_dispatch(&a)); /* none registered */
|
||||
psx_host_set_input_sink(sink, NULL);
|
||||
CHECK(psx_host_input_sink_dispatch(&a) && !psx_host_input_sink_dispatch(&b) && sunk == 2);
|
||||
CHECK(!psx_host_input_sink_dispatch(NULL) && sunk == 2);
|
||||
psx_host_set_input_sink(NULL, NULL);
|
||||
CHECK(!psx_host_input_sink_dispatch(&a));
|
||||
CHECK(!psx_host_ui_capture_active());
|
||||
psx_host_set_ui_capture(5);
|
||||
CHECK(psx_host_ui_capture_active() == 1);
|
||||
psx_host_set_ui_capture(0);
|
||||
CHECK(!psx_host_ui_capture_active());
|
||||
}
|
||||
|
||||
static void test_video(void) {
|
||||
PsxHostVideoSettings a, b;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.fmv_filter = 2; a.renderer = 1; a.render_thread = 1; a.scanline_strength_pct = 50;
|
||||
b = a;
|
||||
CHECK(psx_host_video_live_changes(&a, &b) == 0 && psx_host_video_restart_bits(&a, &b) == 0);
|
||||
b.scanline_strength_pct = 60; b.present_linear = 1; b.fullscreen = 1;
|
||||
CHECK(psx_host_video_live_changes(&a, &b) ==
|
||||
(PSX_VIDEO_LIVE_SCANLINES | PSX_VIDEO_LIVE_PRESENT_LINEAR | PSX_VIDEO_LIVE_FULLSCREEN));
|
||||
CHECK(psx_host_video_restart_bits(&a, &b) == 0);
|
||||
b.internal_resolution = 4; b.present_thread = 1;
|
||||
CHECK(psx_host_video_restart_bits(&a, &b) == (PSX_VIDEO_RESTART_RESOLUTION | PSX_VIDEO_RESTART_THREADS));
|
||||
b.renderer = 0;
|
||||
CHECK(psx_host_video_restart_bits(&a, &b) & PSX_VIDEO_RESTART_RENDERER);
|
||||
CHECK(psx_host_video_live_changes(NULL, &b) == 0 && psx_host_video_restart_bits(&a, NULL) == 0);
|
||||
}
|
||||
|
||||
/* One texture-filter enum for settings, handoff and menu (xBR included). */
|
||||
static void test_texture_filter(void) {
|
||||
CHECK(psx_texture_filter_from_ui(0) == PSX_TEXTURE_FILTER_NEAREST);
|
||||
CHECK(psx_texture_filter_from_ui(1) == PSX_TEXTURE_FILTER_BILINEAR);
|
||||
CHECK(psx_texture_filter_from_ui(3) == PSX_TEXTURE_FILTER_XBR);
|
||||
CHECK(psx_texture_filter_from_ui(9) == PSX_TEXTURE_FILTER_NEAREST);
|
||||
CHECK(psx_texture_filter_from_ui(-1) == PSX_TEXTURE_FILTER_NEAREST);
|
||||
}
|
||||
|
||||
/* Windows relaunch quoting: a parser for the CommandLineToArgvW rules (the
|
||||
* real one too, on Windows) must give every argument back unchanged. */
|
||||
static int parse_cmdline(const char* line, char out[][256], int max) {
|
||||
int n = 0;
|
||||
const char* p = line;
|
||||
while (*p) {
|
||||
while (*p == ' ' || *p == '\t') ++p;
|
||||
if (!*p || n >= max) break;
|
||||
char* o = out[n];
|
||||
size_t len = 0;
|
||||
int inq = 0;
|
||||
for (;;) {
|
||||
size_t bs = 0;
|
||||
while (*p == '\\') { ++bs; ++p; }
|
||||
if (*p == '"') {
|
||||
for (size_t i = 0; i < bs / 2; ++i) o[len++] = '\\';
|
||||
if (bs & 1) { o[len++] = '"'; ++p; continue; }
|
||||
++p;
|
||||
if (inq && *p == '"') { o[len++] = '"'; ++p; continue; }
|
||||
inq = !inq;
|
||||
continue;
|
||||
}
|
||||
for (size_t i = 0; i < bs; ++i) o[len++] = '\\';
|
||||
if (!*p || (!inq && (*p == ' ' || *p == '\t'))) break;
|
||||
o[len++] = *p++;
|
||||
}
|
||||
o[len] = '\0';
|
||||
++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void test_cmdline_quote(void) {
|
||||
const char* args[] = {
|
||||
"C:\\Games\\R4 Recomp\\r4-runtime.exe",
|
||||
"--disc",
|
||||
"D:\\Discs\\R4 - Ridge Racer Type 4 (USA).cue",
|
||||
"--memcard-dir",
|
||||
"C:\\Users\\Me\\Saved Games\\R4\\", /* trailing separator */
|
||||
"C:\\path\\with \"quote\".bin", /* embedded quote */
|
||||
"C:\\a\\b\\\\\"c", /* backslashes before a quote */
|
||||
"", /* empty argument */
|
||||
"plain",
|
||||
"tab\there",
|
||||
"trailing\\\\", /* backslashes, no spaces */
|
||||
};
|
||||
const int n = (int)(sizeof(args) / sizeof(args[0]));
|
||||
char* line = psx_win_cmdline_join(n, args);
|
||||
char got[16][256];
|
||||
CHECK(line != NULL);
|
||||
CHECK(parse_cmdline(line, got, 16) == n);
|
||||
for (int i = 0; i < n; ++i) CHECK(strcmp(got[i], args[i]) == 0);
|
||||
/* No quoting when not needed; measuring matches writing. */
|
||||
char small[8];
|
||||
CHECK(psx_win_cmdline_quote("abc", small, sizeof small) == 3 && strcmp(small, "abc") == 0);
|
||||
CHECK(psx_win_cmdline_quote("a b", NULL, 0) == 5);
|
||||
CHECK(psx_win_cmdline_quote("", NULL, 0) == 2);
|
||||
#if defined(_WIN32)
|
||||
{
|
||||
int wn = 0;
|
||||
int len = MultiByteToWideChar(CP_UTF8, 0, line, -1, NULL, 0);
|
||||
wchar_t* wl = (wchar_t*)malloc(sizeof(wchar_t) * (size_t)len);
|
||||
MultiByteToWideChar(CP_UTF8, 0, line, -1, wl, len);
|
||||
wchar_t** wv = CommandLineToArgvW(wl, &wn);
|
||||
CHECK(wv && wn == n);
|
||||
for (int i = 1; wv && i < n && i < wn; ++i) { /* argv[0] has its own rules; no quotes in exe paths */
|
||||
char u[512];
|
||||
WideCharToMultiByte(CP_UTF8, 0, wv[i], -1, u, sizeof u, NULL, NULL);
|
||||
CHECK(strcmp(u, args[i]) == 0);
|
||||
}
|
||||
LocalFree(wv);
|
||||
free(wl);
|
||||
}
|
||||
#endif
|
||||
free(line);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_draw_lifetime();
|
||||
test_draw_reentrant();
|
||||
test_cmdline_quote();
|
||||
test_video();
|
||||
test_texture_filter();
|
||||
test_sink();
|
||||
test_draw();
|
||||
test_pause();
|
||||
printf("host_overlay_test: %s\n", g_fail ? "FAIL" : "ok");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute the production host-pause presenter before and after a held frame."""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
from test_netplay_load_apply_flow import function_body
|
||||
|
||||
PRELUDE = r"""
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
int g_gl_active, g_vk_active, s_sw_hold_valid;
|
||||
void *sdl_renderer, *sdl_texture;
|
||||
int s_sw_hold_src, s_sw_hold_dst;
|
||||
static int held, holds, blanks, vk_blanks, ticks, suspended;
|
||||
uint32_t SDL_GetTicks() { return 7; }
|
||||
void psx_rewind_present_tick(uint32_t t) { ticks += t == 7; }
|
||||
void gl_renderer_set_interpolation_suspended(int x) { suspended = x; }
|
||||
int gl_renderer_present_hold_last() { ++holds; return held; }
|
||||
void gl_renderer_present_blank() { ++blanks; }
|
||||
void vk_renderer_present_blank() { ++vk_blanks; }
|
||||
void SDL_SetRenderDrawColor(void*,int,int,int,int) {}
|
||||
void SDL_RenderClear(void*) {}
|
||||
void SDL_RenderCopy(void*,void*,void*,void*) {}
|
||||
void host_osd_draw_sdl(void*) {}
|
||||
void SDL_RenderPresent(void*) {}
|
||||
"""
|
||||
CHECKS = r"""
|
||||
int main() {
|
||||
g_gl_active = 1;
|
||||
held = 0; rewind_pause_present();
|
||||
if (holds != 1 || blanks != 1 || ticks != 1 || !suspended) {
|
||||
fprintf(stderr, "No held frame: menu draw must still get a blank present\n"); return 1;
|
||||
}
|
||||
held = 1; rewind_pause_present();
|
||||
if (holds != 2 || blanks != 1 || ticks != 2) return 2;
|
||||
g_gl_active = 0; g_vk_active = 1; rewind_pause_present();
|
||||
if (holds != 2 || blanks != 1 || vk_blanks != 1) return 3;
|
||||
puts("host pause: first-frame fallback and existing held-frame path PASS");
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
|
||||
def main():
|
||||
a=argparse.ArgumentParser();a.add_argument('--cxx',required=True);args=a.parse_args()
|
||||
source=(Path(__file__).resolve().parents[1]/'src/main.cpp').read_text(encoding='utf-8')
|
||||
body=function_body(source,'static void rewind_pause_present(void)')
|
||||
fixed='static void rewind_pause_present(void) {'+body+'}'
|
||||
old=fixed.replace('if (!gl_renderer_present_hold_last()) gl_renderer_present_blank();',
|
||||
'(void)gl_renderer_present_hold_last();')
|
||||
assert old != fixed
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
p=Path(folder)
|
||||
for label,code in [('fixed',fixed),('before',old)]:
|
||||
cpp=p/(label+'.cpp');exe=p/(label+'.exe')
|
||||
cpp.write_text(PRELUDE+code+CHECKS,encoding='utf-8')
|
||||
subprocess.run([args.cxx,'-std=c++17',str(cpp),'-o',str(exe)],check=True)
|
||||
r=subprocess.run([str(exe)],capture_output=True,text=True)
|
||||
if label=='fixed':
|
||||
assert r.returncode==0,r.stdout+r.stderr
|
||||
print(r.stdout.strip())
|
||||
else:
|
||||
assert r.returncode==1,'Regression must reject the old missing fallback'
|
||||
print('pre-fix presenter: regression rejects missing first-frame draw')
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Execute production menu getters while the main thread publishes settings."""
|
||||
import argparse, subprocess, tempfile
|
||||
from pathlib import Path
|
||||
from test_netplay_load_apply_flow import function_body
|
||||
|
||||
ap=argparse.ArgumentParser()
|
||||
ap.add_argument('--cxx',required=True)
|
||||
args=ap.parse_args()
|
||||
runtime=Path(__file__).resolve().parents[1]
|
||||
source=(runtime/'src/main.cpp').read_text(encoding='utf-8')
|
||||
fixture=r'''
|
||||
#include "host_overlay.h"
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
static std::mutex g_ovl_mu;
|
||||
static PsxHostVideoSettings g_ovl_video_current{},g_ovl_video_boot{},g_video_boot{}, live{};
|
||||
static bool g_ovl_netplay=false;
|
||||
static std::atomic<int> netplay{0},wrong_thread_reads{0};
|
||||
static std::thread::id main_id;
|
||||
static bool g_ovl_video_pending=false;
|
||||
static PsxHostVideoSettings g_ovl_video_post{};
|
||||
#define PSX_MENU_RESTART_AFTER_MATCH (1u<<31)
|
||||
static int psx_netplay_active() { return netplay.load(); }
|
||||
extern "C" void psx_video_current(PsxHostVideoSettings *out) {
|
||||
if (std::this_thread::get_id()!=main_id) { ++wrong_thread_reads; return; }
|
||||
*out=live;
|
||||
}
|
||||
extern "C" unsigned psx_host_video_restart_bits(const PsxHostVideoSettings *b,const PsxHostVideoSettings *n) {
|
||||
return b->renderer!=n->renderer ? PSX_VIDEO_RESTART_RENDERER : 0;
|
||||
}
|
||||
extern "C" unsigned psx_host_video_live_changes(const PsxHostVideoSettings *b,const PsxHostVideoSettings *n) {
|
||||
return b->texture_filter!=n->texture_filter ? PSX_VIDEO_LIVE_TEXTURE_FILTER : 0;
|
||||
}
|
||||
'''
|
||||
for signature in ['void mh_refresh_snapshot()', 'void mh_video_get(PsxHostVideoSettings* out)',
|
||||
'unsigned mh_restart_bits(const PsxHostVideoSettings& next)',
|
||||
'unsigned mh_video_apply(const PsxHostVideoSettings* next)']:
|
||||
fixture += signature+' {'+function_body(source,signature)+'}\n'
|
||||
fixture += r'''
|
||||
static PsxHostVideoSettings values(int v) { return {v,v,v,v,v,v,v,v,v,v,v,v,v}; }
|
||||
int main() {
|
||||
main_id=std::this_thread::get_id();
|
||||
live=values(1); g_video_boot=values(1); mh_refresh_snapshot();
|
||||
std::atomic<int> reads{0};
|
||||
std::thread draw([&] {
|
||||
for (int i=0;i<50000;++i) {
|
||||
PsxHostVideoSettings got{};
|
||||
mh_video_get(&got);
|
||||
assert(got.fullscreen==got.texture_filter && got.texture_filter==got.renderer &&
|
||||
got.renderer==got.frame_generation && got.frame_generation==got.supersampling &&
|
||||
got.supersampling==got.render_thread && got.render_thread==got.present_thread);
|
||||
const auto next=values(2);
|
||||
const unsigned restart=mh_video_apply(&next);
|
||||
assert(restart & PSX_VIDEO_RESTART_RENDERER);
|
||||
++reads;
|
||||
}
|
||||
});
|
||||
for (int i=0;i<50000;++i) { live=values(1+i%8); netplay=i%2; mh_refresh_snapshot(); }
|
||||
draw.join();
|
||||
assert(reads==50000 && wrong_thread_reads==0 && g_ovl_video_pending);
|
||||
puts("menu video snapshot: coherent draw-thread reads and main-thread ownership PASS");
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='psx-menu-snapshot-') as d:
|
||||
d=Path(d); cpp=d/'snapshot.cpp'; exe=d/'snapshot.exe'
|
||||
cpp.write_text(fixture,encoding='utf-8')
|
||||
subprocess.run([args.cxx,'-std=c++17','-O2','-pthread','-I'+str(runtime/'include'),str(cpp),'-o',str(exe)],check=True)
|
||||
subprocess.run([str(exe)],check=True)
|
||||
@@ -318,6 +318,19 @@ int iso_read_cdda_sector(void*,uint32_t,uint8_t*,int);
|
||||
}
|
||||
|
||||
static int big_ram_activations;
|
||||
static int option_changed_hits = 0;
|
||||
static std::string option_changed_value;
|
||||
static int option_changed_refuse = 0; /* 1 = the plugin answers "next start" */
|
||||
static int test_option_changed(const char* option_id, const char* value) {
|
||||
char cur[32];
|
||||
option_changed_hits++;
|
||||
/* Runs in the plugin's context: current_option_value answers the new value. */
|
||||
if (std::string(option_id) == "size" &&
|
||||
psx_mod_current_option_value("size", cur, sizeof(cur)) == 1)
|
||||
option_changed_value = cur;
|
||||
(void)value;
|
||||
return option_changed_refuse ? 0 : 1;
|
||||
}
|
||||
static void test_big_ram_activation(void) {
|
||||
big_ram_activations++;
|
||||
}
|
||||
@@ -1082,6 +1095,53 @@ int main(int argc, char** argv) {
|
||||
sizeof(value)) == 1 &&
|
||||
std::string(value) == "eight",
|
||||
"an implicit feature's plugin must read its option values");
|
||||
/* In-game overlay P5: a live option change reaches the plugin and
|
||||
* every later read, and is refused while netplay owns the plan. */
|
||||
check(psx_mod_set_option_live("runtime.ram", "big-ram", "size", "four") == 0,
|
||||
"without an option-changed callback a live set waits for restart");
|
||||
check(psx_mod_option_value("runtime.ram", "big-ram", "size", value, sizeof(value)) == 1 &&
|
||||
std::string(value) == "eight",
|
||||
"a restart-only change must not reach the running plugin");
|
||||
check(!psx_mod_option_live_capable("runtime.ram", "big-ram"),
|
||||
"no callback yet: not live-capable");
|
||||
check(psx_mod_register_option_changed_plugin("runtime.big-ram", test_option_changed),
|
||||
"option-changed hook must register");
|
||||
check(psx_mod_option_live_capable("runtime.ram", "big-ram"),
|
||||
"a registered callback makes the feature live-capable");
|
||||
check(psx_mod_set_option_live("runtime.ram", "big-ram", "size", "four") == 1 &&
|
||||
option_changed_hits == 1 && option_changed_value == "four",
|
||||
"the plugin must see the change in its own context");
|
||||
check(psx_mod_option_value("runtime.ram", "big-ram", "size", value, sizeof(value)) == 1 &&
|
||||
std::string(value) == "four",
|
||||
"a live override must answer later reads");
|
||||
/* A callback that refuses ("nothing changes until next start") must
|
||||
* leave the value the session runs with untouched. */
|
||||
option_changed_refuse = 1;
|
||||
check(psx_mod_set_option_live("runtime.ram", "big-ram", "size", "two") == 0 &&
|
||||
option_changed_hits == 2,
|
||||
"a refused live set reports refused after asking the plugin");
|
||||
check(psx_mod_option_value("runtime.ram", "big-ram", "size", value, sizeof(value)) == 1 &&
|
||||
std::string(value) == "four",
|
||||
"a refused live set must not change the value later reads see");
|
||||
option_changed_refuse = 0;
|
||||
/* A new session (initialise + commit) answers its own committed
|
||||
* plan, not the previous session's live override. */
|
||||
check(PSXRecompV4::mod_runtime_initialize(
|
||||
req_root, "SLUS-REQ", 0x80002000, {}, &error),
|
||||
error.c_str());
|
||||
check(PSXRecompV4::mod_runtime_commit(stock_path, &error),
|
||||
"the second session must commit");
|
||||
mod_runtime_activate_plugins();
|
||||
check(psx_mod_option_value("runtime.ram", "big-ram", "size", value, sizeof(value)) == 1 &&
|
||||
std::string(value) == "eight",
|
||||
"a live override must not outlive its session's plan");
|
||||
check(psx_mod_set_option_live("runtime.ram", "no-such-feature", "size", "x") == 0,
|
||||
"features outside the plan are not live-settable");
|
||||
check(psx_mod_save_selection() == 1, "saving the selection mid-session must succeed");
|
||||
test_netplay_active = 1;
|
||||
check(psx_mod_set_option_live("runtime.ram", "big-ram", "size", "four") == -1,
|
||||
"netplay must refuse live option changes");
|
||||
test_netplay_active = 0;
|
||||
/* commit rewrites state.toml in canonical form; what matters is that
|
||||
* the player's choice survives and the derived one is never added. */
|
||||
const std::string after = read_state();
|
||||
|
||||
@@ -280,7 +280,7 @@ int main(void) {
|
||||
int *f[] = { &g.injected_input, &g.headless, &g.netplay_active,
|
||||
&g.netplay_resim, &g.selfcheck_locked, &g.selfcheck_resim,
|
||||
&g.render_pass, &g.savestate_menu_open, &g.rewind_open,
|
||||
&g.input_guard };
|
||||
&g.input_guard, &g.ui_capture };
|
||||
for (unsigned i = 0; i < sizeof f / sizeof f[0]; i++) {
|
||||
*f[i] = 1; CHECK(pad_ext_live(&g) == 0); *f[i] = 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/* psx_menu_host_test: provider and disc-op registration (psx_menu_host.h). */
|
||||
#include "psx_menu_host.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
static int fails = 0;
|
||||
#define CHECK(c) do { if (!(c)) { fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #c); ++fails; } } while (0)
|
||||
|
||||
static int attach(void* c, void* w, void* g, const PsxMenuHost* h) { (void)c; (void)w; (void)g; return h != NULL; }
|
||||
static int count3(void) { return 3; }
|
||||
static int insert_ok(int d) { return d >= 1 && d <= 3 ? 0 : 2; }
|
||||
|
||||
int main(void) {
|
||||
/* Default: no provider, so a title's own UI is unchanged. */
|
||||
CHECK(psx_menu_provider() == NULL);
|
||||
PsxMenuProvider p = {PSX_MENU_ABI_VERSION, "test", NULL, attach, NULL, NULL, NULL, NULL, NULL};
|
||||
CHECK(psx_menu_set_provider(&p) == 1 && psx_menu_provider() == &p);
|
||||
/* Another ABI version is refused and leaves the registration alone. */
|
||||
PsxMenuProvider old = p;
|
||||
old.abi_version = PSX_MENU_ABI_VERSION + 1;
|
||||
CHECK(psx_menu_set_provider(&old) == 0 && psx_menu_provider() == &p);
|
||||
CHECK(psx_menu_set_provider(NULL) == 1 && psx_menu_provider() == NULL);
|
||||
/* Disc ops: none until the tray policy publishes them. */
|
||||
CHECK(psx_menu_disc_ops() == NULL);
|
||||
PsxMenuDiscOps d = {count3, NULL, NULL, insert_ok};
|
||||
psx_menu_set_disc_ops(&d);
|
||||
CHECK(psx_menu_disc_ops() == &d && psx_menu_disc_ops()->count() == 3 &&
|
||||
psx_menu_disc_ops()->insert(2) == 0 && psx_menu_disc_ops()->insert(9) != 0);
|
||||
psx_menu_set_disc_ops(NULL);
|
||||
CHECK(psx_menu_disc_ops() == NULL);
|
||||
if (!fails) printf("psx_menu_host_test: ok\n");
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user