netplay local viewport: projection renderer, hi-res half present, state gate

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-10-01 16:02:08 -07:00
parent 881c94a869
commit 2163035e9a
5 changed files with 124 additions and 10 deletions
+46
View File
@@ -1330,6 +1330,9 @@ GameConfig load_game_config(const fs::path& config_path_in) {
std::vector<std::string> 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<uint32_t> 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<std::string>(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<std::string>(np, "local_viewport_state_addr"),
"netplay.local_viewport_state_addr");
for (const auto& value : toml::find<std::vector<std::string>>(
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,
+14
View File
@@ -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<uint32_t> netplay_local_viewport_state_values;
// [recompiler] block
std::filesystem::path seeds_path; // absolute path to seeds (text or json)
+4
View File
@@ -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. */
+26 -1
View File
@@ -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) {
+34 -9
View File
@@ -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