diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 6108e6cf..60fb00c5 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -1330,6 +1330,9 @@ GameConfig load_game_config(const fs::path& config_path_in) { std::vector netplay_required_disc_fps; std::string netplay_local_viewport; std::string netplay_local_viewport_aspect; + std::string netplay_local_viewport_renderer; + uint32_t netplay_local_viewport_state_addr = 0; + std::vector netplay_local_viewport_state_values; if (cfg.contains("netplay")) { const toml::value& np = toml::find(cfg, "netplay"); if (np.contains("require_cue")) @@ -1385,6 +1388,46 @@ GameConfig load_game_config(const fs::path& config_path_in) { "[netplay] local_viewport_aspect requires local_viewport"); } } + if (np.contains("local_viewport_renderer")) { + netplay_local_viewport_renderer = + toml::find(np, "local_viewport_renderer"); + for (char& c : netplay_local_viewport_renderer) + c = (char)std::tolower((unsigned char)c); + if (netplay_local_viewport_renderer != "native_wide" && + netplay_local_viewport_renderer != "projection") { + throw std::runtime_error(fmt::format( + "[netplay] local_viewport_renderer must be \"native_wide\" " + "or \"projection\", got '{}'", + netplay_local_viewport_renderer)); + } + if (netplay_local_viewport.empty()) { + throw std::runtime_error( + "[netplay] local_viewport_renderer requires local_viewport"); + } + } + { + const bool has_addr = np.contains("local_viewport_state_addr"); + const bool has_values = np.contains("local_viewport_state_values"); + if (has_addr != has_values) + throw std::runtime_error( + "[netplay] local_viewport_state_addr and " + "local_viewport_state_values must be set together"); + if (has_addr) { + if (netplay_local_viewport.empty()) + throw std::runtime_error( + "[netplay] local_viewport_state_addr requires local_viewport"); + netplay_local_viewport_state_addr = parse_hex( + toml::find(np, "local_viewport_state_addr"), + "netplay.local_viewport_state_addr"); + for (const auto& value : toml::find>( + np, "local_viewport_state_values")) + netplay_local_viewport_state_values.push_back(parse_hex( + value, "netplay.local_viewport_state_values")); + if (netplay_local_viewport_state_values.empty()) + throw std::runtime_error( + "[netplay] local_viewport_state_values must not be empty"); + } + } } // [recompiler] @@ -2220,6 +2263,9 @@ GameConfig load_game_config(const fs::path& config_path_in) { /*netplay_required_disc_fps*/ netplay_required_disc_fps, /*netplay_local_viewport*/ netplay_local_viewport, /*netplay_local_viewport_aspect*/ netplay_local_viewport_aspect, + /*netplay_local_viewport_renderer*/ netplay_local_viewport_renderer, + /*netplay_local_viewport_state_addr*/ netplay_local_viewport_state_addr, + /*netplay_local_viewport_state_values*/ netplay_local_viewport_state_values, /*seeds_path*/ seeds_path, /*bios_thunks_path*/ bios_thunks_path, /*bios_config_path*/ bios_config_path, diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index d69873a4..8e53322b 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -824,6 +824,20 @@ struct GameConfig { // "16:9", "21:9", or "adaptive" (initial 16:9, live-window capped 21:9). // Unset keeps netplay at the title's normal mod-cleared aspect. std::string netplay_local_viewport_aspect; + // local_viewport_renderer = "native_wide" (default) | "projection". + // native_wide renders extra columns beside the local half; it needs the + // game to submit geometry outside its own split viewport. "projection" + // instead widens the GTE projection into the half (squash around OFX) and + // stretches the half on present -- for titles whose per-viewport culling + // rejects everything outside the half (THPS2), as their single-player + // [widescreen] native_wide = false path does. + std::string netplay_local_viewport_renderer; + // local_viewport_state_addr / _values: optional guest word gating the + // local viewport. Split frames crop to this peer's half only while the + // word holds one of the values (e.g. a level running); otherwise, as on a + // pause menu drawn across both halves, every peer sees the whole frame. + uint32_t netplay_local_viewport_state_addr = 0; + std::vector netplay_local_viewport_state_values; // [recompiler] block std::filesystem::path seeds_path; // absolute path to seeds (text or json) diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 8f90b2d3..75c30d66 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -407,6 +407,10 @@ void psx_ws_note_gte_project(int nverts); * heuristic gameplay classification for native-wide presentation. */ void gpu_ws_set_gameplay_state_gate(uint32_t addr, const uint32_t *values, int nvalues); +/* [netplay] local_viewport_state_*: a split frame counts as a vertical split + * (netplay local viewport) only while this word holds one of the values. */ +void gpu_ws_set_local_viewport_state_gate(uint32_t addr, + const uint32_t *values, int nvalues); /* Native-wide HUD corner re-anchoring ([widescreen] nw_hud_corners): push * outer-third screen-space HUD primitives out to the true wide-frame corners * (they otherwise sit inset by the reveal). Runtime-only. Off by default. */ diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 590ff9d7..4be0e01c 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -3017,8 +3017,33 @@ static void split_trace_note_draw_area(void) { } } +#define WS_LOCAL_VIEWPORT_STATE_VALUES_MAX 16 +static uint32_t ws_local_viewport_state_addr = 0; +static uint32_t ws_local_viewport_state_values[WS_LOCAL_VIEWPORT_STATE_VALUES_MAX]; +static int ws_local_viewport_state_value_count = 0; +void gpu_ws_set_local_viewport_state_gate(uint32_t addr, + const uint32_t *values, int nvalues) { + if (nvalues < 0) nvalues = 0; + if (nvalues > WS_LOCAL_VIEWPORT_STATE_VALUES_MAX) + nvalues = WS_LOCAL_VIEWPORT_STATE_VALUES_MAX; + ws_local_viewport_state_addr = values && nvalues ? addr : 0; + ws_local_viewport_state_value_count = values ? nvalues : 0; + for (int i = 0; i < ws_local_viewport_state_value_count; i++) + ws_local_viewport_state_values[i] = values[i]; +} + +static int ws_local_viewport_state_ok(void) { + if (!ws_local_viewport_state_addr || ws_local_viewport_state_value_count == 0) + return 1; + const uint32_t state = psx_read_word(ws_local_viewport_state_addr); + for (int i = 0; i < ws_local_viewport_state_value_count; i++) + if (state == ws_local_viewport_state_values[i]) return 1; + return 0; +} + static int ws_vertical_split_active(void) { - return split_recent_left_age <= 8 && split_recent_right_age <= 8; + return split_recent_left_age <= 8 && split_recent_right_age <= 8 && + ws_local_viewport_state_ok(); } int gpu_last_frame_vertical_split_screen(void) { diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index f70c0911..bd5ddf68 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1435,6 +1435,9 @@ static int g_netplay_local_viewport = 0; /* 0 off, 1 vertical split */ /* Optional aspect for netplay local-view extraction. Mirrors trusted mod aspect * activation, but remains game.toml opt-in so normal netplay stays vanilla. */ static int g_netplay_local_viewport_aspect = 0; /* 0 off, 1 16:9, 2 21:9, 3 adaptive */ +/* [netplay] local_viewport_renderer = "projection": widen the projection into + * the local half instead of rendering native-wide columns beside it. */ +static int g_netplay_local_viewport_projection = 0; extern "C" int psx_mod_set_fixed_display_aspect( uint32_t numerator, uint32_t denominator) { @@ -1847,14 +1850,16 @@ static void netplay_local_viewport_projection_aspect( return; /* The normal widescreen squash assumes a 4:3 source. A split-screen peer - * source is only half the display width, so derive the equivalent aspect - * that produces source_aspect / target_aspect as the X squash factor: + * source is half the display, which spans 4:3 at whatever pixel aspect + * its mode has, so the half is a 2:3 view and the equivalent aspect that + * produces source_aspect / target_aspect as the X squash factor is * - * source = (display_w / 2) / display_h - * effective = (4:3) * target / source - */ - int64_t n = (int64_t)present_num * 8 * (int64_t)di.height; - int64_t d = (int64_t)present_den * 3 * (int64_t)di.width; + * effective = (4:3) * target / (2:3) = 2 * target + * + * (The earlier (display_w/2)/display_h source assumed square pixels and + * under-widened 512-wide splits.) */ + int64_t n = (int64_t)present_num * 2; + int64_t d = (int64_t)present_den; if (n <= 0 || d <= 0) return; int64_t gcd = aspect_gcd64(n, d); n /= gcd; @@ -1874,8 +1879,8 @@ static void refresh_widescreen_projection() { const bool wide = g_video_aspect_num * 3 != g_video_aspect_den * 4; const bool local_native_wide = - g_netplay_local_viewport == 1 && psx_netplay_active() && - gpu_last_frame_vertical_split_screen(); + g_netplay_local_viewport == 1 && !g_netplay_local_viewport_projection && + psx_netplay_active() && gpu_last_frame_vertical_split_screen(); const bool native_wide = (g_netplay_local_viewport == 1) ? local_native_wide : (g_ws_native_wide != 0); @@ -7674,6 +7679,20 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { * do NOT flush_cpu_uploads (MDEC already wrote the CPU mirror; forcing * FBO uploads every frame cut MotK intro from ~50 to ~30 FPS). */ #ifndef PSX_SDL_NO_RENDER + /* Netplay local viewport without native-wide columns (squash or 4:3): + * present this peer's half straight from the high-resolution FBO. + * The CPU crop below reads the 1x canonical frame, which throws away + * internal resolution and smears the proportion-corrected HUD. */ + if (g_gl_active && g_gl_fbo_present && !di.depth24 && + local_viewport_crop && !local_viewport_wide) { + const int half = (int)w / 2; + const int half_x = (int)di.display_x + + (local_viewport_slot == 1 ? (int)w - half : 0); + gl_renderer_present_vram(half_x, (int)di.display_y, half, (int)h, + g_video_aa ? 1 : 0, 0); + netplay_note_present(); + return ep; + } if (g_gl_active && g_gl_fbo_present && !di.depth24 && !local_viewport_crop) { if (wide_present) { @@ -13309,6 +13328,12 @@ int main(int argc, char** argv) { (gc.netplay_local_viewport_aspect == "16:9") ? 1 : (gc.netplay_local_viewport_aspect == "21:9") ? 2 : (gc.netplay_local_viewport_aspect == "adaptive") ? 3 : 0; + g_netplay_local_viewport_projection = + (gc.netplay_local_viewport_renderer == "projection") ? 1 : 0; + gpu_ws_set_local_viewport_state_gate( + gc.netplay_local_viewport_state_addr, + gc.netplay_local_viewport_state_values.data(), + (int)gc.netplay_local_viewport_state_values.size()); game_discs = gc.discs; /* Per-disc serial gate, shared by the launch-time disc check and * the launcher's disc verdict. Keyed by the image's uppercased