mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
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:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user