Add opt-in netplay local viewport width sites, fixed aspect and native-wide per-camera views

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-10-05 20:20:18 -07:00
parent 8aa3c63779
commit d247ce8b88
18 changed files with 295 additions and 15 deletions
+33
View File
@@ -318,3 +318,36 @@ Generate & rebuild / prepare flows should point at the **`.cue`**, not a lone
This document will grow as N-way rollback confirmation, SFU soak on 5P titles,
and further ICE/SFU policy land.
## Per-peer full-screen views
`[netplay].local_viewport = "vertical_split"` presents the camera belonging to
the peer's mapped controller port, including lobby seat reordering.
`local_viewport_aspect = "16:9"` fixes the presentation aspect independently
of monitor size. `local_viewport_renderer = "projection"` widens projection
inside the native split framebuffer; `native_wide` selects the separate wide
compositor. Offline rendering keeps the title's original renderer choice.
`local_viewport_aspect = "fixed"` uses the admitted mod plan's 21:9 choice or
defaults to 16:9. It disables adaptive resizing for the session. Enhanced
native-wide local views present their per-camera surface directly through GL,
including both double-buffer bands, and retain the full composite rather than
copying canonical split-divider columns into it. Trusted title plugins may opt
into forward-netplay render passes; rollback resimulation stays excluded (see
[RENDER_PASSES.md](RENDER_PASSES.md)).
Projection uses the PSX display's pixel aspect: each half of a 4:3 display
spans 2:3, so a 16:9 local view needs a 3/8 horizontal projection ratio.
The GL path presents the selected high-resolution FBO half directly.
Source-guarded HUD groups drawn in a full-display area and spanning the seam
are copied completely into both halves, preserving glyphs and shadows.
Titles may configure `local_viewport_state_addr` and
`local_viewport_state_values` to release the crop immediately during modal
menus. V82 uses its modal-menu flag at 0x8006B4F0 (running value 0). Its
`local_viewport_width_sites = ["0x8001C248", "0x8002E01C"]` identifies exact
LW sites for terrain/object frusta. The generated and dirty-RAM paths apply
the same inverse projection ratio to half-display widths while unsplit is
active; ordinary widths, pause and offline execution remain unchanged.
Regenerate title code after changing these sites. At most 16 unique aligned
main-RAM sites are allowed, and they require the projection renderer.
+1 -1
View File
@@ -1 +1 @@
833e65ea87826617c22ddb8598694feb750f9d4f9faa432976e006af40911129
9c92d6060348bc2478abb2370e9c9142ee9c0d1d0ec8dc05d9be98177545e6e2
+1 -1
View File
@@ -1 +1 @@
5fdf6a427293e5169e5f9c249d6194a25e6b9cc5a78ae3116d956c6d0feefe3e
26c707e2203f6886cc6c73098d2072fc811b278f55521fa0e945fa30b2def17f
+1
View File
@@ -152,6 +152,7 @@ struct CodeGenConfig {
// plane_nx_sites). The configured lw routes through the runtime helper
// (inverse-aspect scale while revealed, identity at 4:3).
std::set<uint32_t> ws_cull_plane_nx_sites;
std::set<uint32_t> netplay_local_viewport_width_sites;
// Per-primitive X-reject bound load sites ([widescreen.cull]
// xclip_load_sites). The configured lw routes through the runtime helper
+23
View File
@@ -1278,6 +1278,27 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in
}
// Overlay variant at the same address: leave nonmatching code unchanged.
}
// Exact LW sites that build each unsplit camera's horizontal frustum.
if (config_.netplay_local_viewport_width_sites.count(addr)) {
if (opcode == 0x23) { // lw
uint32_t rs = get_rs(instr), rt = get_rt(instr);
int16_t offset = get_imm16(instr);
std::string laddr = (offset == 0)
? reg_name(rs)
: fmt::format("{} + {}", reg_name(rs), (int)offset);
uint32_t mask = 1u << rs;
return fmt::format("{} = (uint32_t)psx_ws_local_viewport_width((int32_t)psx_cyc_load_word(cpu, {}, {}, 0x{:X}u));"
" /* unsplit camera frustum width */{}",
reg_name(rt), laddr, rt, mask, comment);
}
if (!config_.overlay_mode) {
fmt::print(stderr, "ERROR: [netplay] local_viewport_width site 0x{:08X} is not "
"lw (opcode 0x{:02X})\n", addr, opcode);
std::exit(1);
}
// Overlay variant at the same address: leave nonmatching code unchanged.
}
// Side frustum-plane normal-X load: `lw rt,off(rs)` reads the nx of a
// plane sign-tested per corner (dot = nx*px + nz*pz). The helper scales
// nx by the inverse aspect factor (4*den)/(3*num) while revealed, which
@@ -3589,6 +3610,8 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
ss << "extern void psx_ws_mmx6_bg_stage_init(void); /* ws 2D stage reveal invalidation (gpu.c) */\n";
ss << "extern int psx_ws_x_margin(void); /* widescreen cull-margin term (gpu.c) */\n";
ss << "extern int32_t psx_ws_player_x_bound(int32_t vanilla); /* typed gameplay X bound */\n";
if (!config_.netplay_local_viewport_width_sites.empty())
ss << "extern int32_t psx_ws_local_viewport_width(int32_t vanilla); /* netplay unsplit frustum width */\n";
ss << "extern int32_t psx_ws_screen_x_bound(int32_t vanilla); /* typed screen-pixel X bound */\n";
ss << "extern int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm); /* ws auto screen-x cull (gpu.c) */\n";
ss << "extern int psx_ws_cull_slti(uint32_t sx, uint32_t imm); /* ws cull signed right edge (gpu.c) */\n";
+24 -1
View File
@@ -2,6 +2,7 @@
#include "config_loader.h"
#include <unordered_set>
#include <algorithm>
#include <cctype>
#include <cstdio>
@@ -109,6 +110,8 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) {
h.words("cull_clip_edge_x_load", c.ws_cull_clip_edge_x_load_sites);
h.u32(ws_cull_clip_edge_width(c));
}
if (!c.netplay_local_viewport_width_sites.empty())
h.words("local_viewport_width", c.netplay_local_viewport_width_sites);
h.tag("flags");
h.u32(c.ws_auto_screen_x_cull ? 1u : 0u);
@@ -1441,6 +1444,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
std::string netplay_local_viewport;
std::string netplay_local_viewport_aspect;
std::string netplay_local_viewport_renderer;
std::vector<uint32_t> netplay_local_viewport_width_sites;
uint32_t netplay_local_viewport_state_addr = 0;
std::vector<uint32_t> netplay_local_viewport_state_values;
if (cfg.contains("netplay")) {
@@ -1486,10 +1490,11 @@ GameConfig load_game_config(const fs::path& config_path_in) {
if (!netplay_local_viewport_aspect.empty() &&
netplay_local_viewport_aspect != "16:9" &&
netplay_local_viewport_aspect != "21:9" &&
netplay_local_viewport_aspect != "fixed" &&
netplay_local_viewport_aspect != "adaptive") {
throw std::runtime_error(fmt::format(
"[netplay] local_viewport_aspect must be \"16:9\", "
"\"21:9\", or \"adaptive\", got '{}'",
"\"21:9\", \"fixed\", or \"adaptive\", got '{}'",
netplay_local_viewport_aspect));
}
if (!netplay_local_viewport_aspect.empty() &&
@@ -1498,6 +1503,10 @@ GameConfig load_game_config(const fs::path& config_path_in) {
"[netplay] local_viewport_aspect requires local_viewport");
}
}
if (np.contains("local_viewport_width_sites")) {
for (const auto& value : toml::find<std::vector<std::string>>(np, "local_viewport_width_sites"))
netplay_local_viewport_width_sites.push_back(parse_hex(value, "netplay.local_viewport_width_sites"));
}
if (np.contains("local_viewport_renderer")) {
netplay_local_viewport_renderer =
toml::find<std::string>(np, "local_viewport_renderer");
@@ -1515,6 +1524,19 @@ GameConfig load_game_config(const fs::path& config_path_in) {
"[netplay] local_viewport_renderer requires local_viewport");
}
}
if (!netplay_local_viewport_width_sites.empty()) {
if (netplay_local_viewport_renderer != "projection")
throw std::runtime_error("[netplay] local_viewport_width_sites requires projection renderer");
if (netplay_local_viewport_width_sites.size() > 16)
throw std::runtime_error("[netplay] local_viewport_width_sites supports at most 16 sites");
std::unordered_set<uint32_t> unique;
for (uint32_t pc : netplay_local_viewport_width_sites) {
if ((pc & 3) || (pc & 0x1fffffff) >= 0x800000)
throw std::runtime_error("[netplay] local_viewport_width_sites requires aligned main RAM addresses");
if (!unique.insert(pc & 0x1fffffff).second)
throw std::runtime_error("[netplay] duplicate local_viewport_width_sites address");
}
}
{
const bool has_addr = np.contains("local_viewport_state_addr");
const bool has_values = np.contains("local_viewport_state_values");
@@ -2437,6 +2459,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
/*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_width_sites*/ netplay_local_viewport_width_sites,
/*netplay_local_viewport_state_addr*/ netplay_local_viewport_state_addr,
/*netplay_local_viewport_state_values*/ netplay_local_viewport_state_values,
/*seeds_path*/ seeds_path,
+3
View File
@@ -903,6 +903,9 @@ struct GameConfig {
// rejects everything outside the half (THPS2), as their single-player
// [widescreen] native_wide = false path does.
std::string netplay_local_viewport_renderer;
// Exact LW sites loading the native per-camera width for world frusta.
// Projection unsplit widens that cone while retaining the half-size raster.
std::vector<uint32_t> netplay_local_viewport_width_sites;
// 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
+4
View File
@@ -234,6 +234,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
std::set<uint32_t> ws_cull_negsub; // [widescreen.cull] negsub_sites
std::set<uint32_t> ws_cull_vxrange; // [widescreen.cull] vxrange_sites
std::set<uint32_t> ws_cull_depth; // [widescreen.cull] depth_sites
std::set<uint32_t> netplay_local_viewport_width_sites;
std::set<uint32_t> ws_cull_plane_nx; // [widescreen.cull] plane_nx_sites
std::set<uint32_t> ws_cull_xclip_load; // [widescreen.cull] xclip_load_sites
std::set<uint32_t> ws_cull_nclip_keep; // [widescreen.cull] nclip_keep_sites
@@ -310,6 +311,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
ws_cull_vxrange.insert(cfg.ws_cull_vxrange_sites.begin(), cfg.ws_cull_vxrange_sites.end());
ws_cull_depth.insert(cfg.ws_cull_depth_sites.begin(), cfg.ws_cull_depth_sites.end());
ws_cull_plane_nx.insert(cfg.ws_cull_plane_nx_sites.begin(), cfg.ws_cull_plane_nx_sites.end());
netplay_local_viewport_width_sites.insert(cfg.netplay_local_viewport_width_sites.begin(), cfg.netplay_local_viewport_width_sites.end());
ws_cull_xclip_load.insert(cfg.ws_cull_xclip_load_sites.begin(), cfg.ws_cull_xclip_load_sites.end());
ws_cull_nclip_keep.insert(cfg.ws_cull_nclip_keep_sites.begin(), cfg.ws_cull_nclip_keep_sites.end());
ws_cull_nclip_exact.insert(cfg.ws_cull_nclip_exact_sites.begin(), cfg.ws_cull_nclip_exact_sites.end());
@@ -418,6 +420,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
ws_cull_vxrange.insert(wscfg.ws_cull_vxrange_sites.begin(), wscfg.ws_cull_vxrange_sites.end());
ws_cull_depth.insert(wscfg.ws_cull_depth_sites.begin(), wscfg.ws_cull_depth_sites.end());
ws_cull_plane_nx.insert(wscfg.ws_cull_plane_nx_sites.begin(), wscfg.ws_cull_plane_nx_sites.end());
netplay_local_viewport_width_sites.insert(wscfg.netplay_local_viewport_width_sites.begin(), wscfg.netplay_local_viewport_width_sites.end());
ws_cull_xclip_load.insert(wscfg.ws_cull_xclip_load_sites.begin(), wscfg.ws_cull_xclip_load_sites.end());
ws_cull_nclip_keep.insert(wscfg.ws_cull_nclip_keep_sites.begin(), wscfg.ws_cull_nclip_keep_sites.end());
ws_cull_nclip_exact.insert(wscfg.ws_cull_nclip_exact_sites.begin(), wscfg.ws_cull_nclip_exact_sites.end());
@@ -1489,6 +1492,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
codegen_config.ws_cull_vxrange_sites = ws_cull_vxrange;
codegen_config.ws_cull_depth_sites = ws_cull_depth;
codegen_config.ws_cull_plane_nx_sites = ws_cull_plane_nx;
codegen_config.netplay_local_viewport_width_sites = netplay_local_viewport_width_sites;
codegen_config.ws_cull_xclip_load_sites = ws_cull_xclip_load;
codegen_config.ws_cull_nclip_keep_sites = ws_cull_nclip_keep;
codegen_config.ws_cull_nclip_exact_sites = ws_cull_nclip_exact;
@@ -1251,6 +1251,32 @@ void mod_instruction_codegen_test(const fs::path& root) {
"unconfigured code emits no instruction callbacks");
}
void local_viewport_tests(const fs::path& root) {
const std::string prefix = "\n[netplay]\nlocal_viewport=\"vertical_split\"\nlocal_viewport_renderer=\"projection\"\n";
const auto cfg = PSXRecompV4::load_game_config(write_config(root,"unsplit",prefix +
"local_viewport_width_sites=[\"0x80010000\"]\nlocal_viewport_state_addr=\"0x80018000\"\nlocal_viewport_state_values=[\"0x0\"]\n"));
check(cfg.netplay_local_viewport_width_sites == std::vector<uint32_t>{0x80010000}, "unsplit width sites parse");
check(cfg.netplay_local_viewport_renderer == "projection" && cfg.netplay_local_viewport_state_addr == 0x80018000 &&
cfg.netplay_local_viewport_state_values == std::vector<uint32_t>{0}, "unsplit renderer and pause gate parse");
auto changed = cfg; changed.netplay_local_viewport_width_sites.clear();
check(PSXRecompV4::overlay_codegen_config_hash(cfg) != PSXRecompV4::overlay_codegen_config_hash(changed), "unsplit sites invalidate cached code");
for (auto bad : {"0x80010001", "0x1F800000", "0x80800000"}) {
auto path = write_config(root,"unsplit-bad",prefix+"local_viewport_width_sites=[\""+bad+"\"]\n");
check_throws([&]{PSXRecompV4::load_game_config(path);}, "aligned main RAM", "unsplit rejects invalid site");
}
auto dup = write_config(root,"unsplit-duplicate",prefix+"local_viewport_width_sites=[\"0x80010000\",\"0xA0010000\"]\n");
check_throws([&]{PSXRecompV4::load_game_config(dup);},"duplicate", "unsplit rejects aliased duplicates");
auto no_projection = write_config(root,"unsplit-renderer","\n[netplay]\nlocal_viewport_width_sites=[\"0x80010000\"]\n");
check_throws([&]{PSXRecompV4::load_game_config(no_projection);},"requires projection", "unsplit width requires projection");
PSXRecomp::CodeGenConfig codegen{};
codegen.netplay_local_viewport_width_sites.insert(0x80010000);
for (bool overlay : {false,true}) {
auto code=generate_first_instruction(0x8F820EDCu,{},overlay,codegen);
check(code.find("psx_ws_local_viewport_width((int32_t)psx_cyc_load_word") != std::string::npos, "unsplit keeps LW cycle accounting");
}
check(generate_first_instruction(0x8F820EDCu,{},false).find("= (uint32_t)psx_ws_local_viewport_width") == std::string::npos, "ordinary loads remain native");
}
void cfg_fallthrough_reachability_test() {
constexpr uint32_t base = 0x80010000u;
PSXRecomp::PS1Executable exe{};
@@ -1302,6 +1328,7 @@ int main() {
cfg_codegen_load_delay_test();
mod_function_completion_codegen_test();
mod_instruction_codegen_test(root);
local_viewport_tests(root);
cfg_fallthrough_reachability_test();
} catch (const std::exception& e) {
fmt::print(stderr, "FAIL unexpected exception: {}\n", e.what());
+5
View File
@@ -98,6 +98,11 @@ void gpu_get_draw_area(GpuDrawArea* out);
* areas. Presentation-only helpers use this to expand a local split-screen
* viewport without changing the underlying framebuffer or netplay hashes. */
int gpu_last_frame_vertical_split_screen(void);
int32_t psx_ws_local_viewport_width(int32_t vanilla);
void gpu_ws_set_local_viewport_width_sites(const uint32_t *sites, int count);
/* The title's local viewport uses the native-wide per-camera renderer. */
void gpu_ws_set_local_viewport_native_wide(int enabled);
int psx_ws_is_local_viewport_width_site(uint32_t pc);
void gpu_vertical_split_debug(int *active, int *left_age, int *right_age);
uint16_t gpu_vram_peek(int x, int y);
+4 -2
View File
@@ -9858,13 +9858,15 @@ static const char *write_wide_png(const char *path, int *out_w, int *out_h)
if (di.disabled || di.width == 0 || di.height == 0) return "display disabled";
int scale = gr_scale(); if (scale < 1) scale = 1;
int present_w = (int)di.width + extra;
int local_width = gpu_ws_netplay_local_viewport_width();
int base_x = local_width > 0 ? gpu_ws_netplay_local_viewport_base_x() : (int)di.display_x;
int present_w = local_width > 0 ? local_width : (int)di.width + extra;
int W = present_w * scale, H = (int)di.height * scale;
uint32_t *buf = (uint32_t *)malloc((size_t)W * H * sizeof(uint32_t));
if (!buf) return "alloc failed";
int n = gr_render_wide_display(buf, W * (int)sizeof(uint32_t),
(int)di.display_x, (int)di.display_y, (int)di.height);
base_x, (int)di.display_y, (int)di.height);
if (n <= 0) { free(buf); return "no wide surface for displayed buffer"; }
/* ARGB8888 (0xAARRGGBB) -> RGB, written in the buffer's row order (so the
+3 -1
View File
@@ -2384,7 +2384,9 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
case 0x23: { /* LW */
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
if (addr & 3) return interp_exception(cpu, 4, addr, pc); /* LoadAddressError */
if (psx_ws_is_cull_plane_nx_site(pc))
if (psx_ws_is_local_viewport_width_site(pc))
cpu->gpr[rt] = (uint32_t)psx_ws_local_viewport_width((int32_t)psx_cyc_load_word(cpu, addr, rt, 1u << rs));
else if (psx_ws_is_cull_plane_nx_site(pc))
/* Side-plane normal-X: inverse-aspect scale while revealed. */
cpu->gpr[rt] = (uint32_t)psx_ws_plane_nx(
(int32_t)psx_cyc_load_word(cpu, addr, rt, 1u << rs));
+76 -3
View File
@@ -111,6 +111,15 @@ static int ws_auto_ui_squash;
static int ws_auto_ui_in_place;
static int ws_auto_ui_dense;
static int ws_active(void);
static int ws_local_viewport_cfg = 0;
/* [netplay] local_viewport_renderer = "native_wide": split frames are always
* world frames for the per-camera wide surfaces. Projection views keep the
* title's own scene classification. */
static int ws_local_viewport_native = 0;
static int ws_vertical_split_active(void);
static int ws_local_native_split(void) {
return ws_local_viewport_cfg && ws_local_viewport_native && ws_vertical_split_active();
}
static int gp0_command_word_count(uint8_t opcode);
static uint64_t ws_auto_ui_candidate_count;
static uint64_t ws_auto_ui_transform_count;
@@ -384,6 +393,7 @@ static int ws_mod_world_scene(void) {
}
static int ws_game_mode(void) {
if (ws_mod_world_scene()) return 1;
if (ws_local_native_split()) return 1;
int state_match = ws_gameplay_state_matches();
if (state_match >= 0) return state_match;
if (ws_full_2d_mode()) return 1;
@@ -421,6 +431,7 @@ static int ws_game_mode(void) {
static WsSceneLatch ws_scene_latch;
static int ws_2d_only_scene(void) {
if (ws_mod_world_scene()) return 0;
if (ws_local_native_split()) return 0;
if (ws_full_2d_mode() || ws_gte_game_mode_cfg) return 0;
uint32_t f = (uint32_t)s_frame_count;
return ws_scene_is_2d(&ws_scene_latch, f,
@@ -504,10 +515,31 @@ static int ws_native_wide_configured(void) {
return ws_mode == 2 && ws_cfg_num * 3 > ws_cfg_den * 4;
}
static int ws_local_viewport_cfg = 0;
static int ws_local_viewport_slot = 0;
static int ws_local_viewport_draw_target(int *base_x);
static int ws_vertical_split_active(void);
#define WS_LOCAL_WIDTH_SITES_MAX 16
static uint32_t ws_local_width_sites[WS_LOCAL_WIDTH_SITES_MAX];
static int ws_local_width_count;
void gpu_ws_set_local_viewport_width_sites(const uint32_t *sites, int count) {
ws_local_width_count = count < 0 ? 0 : count > WS_LOCAL_WIDTH_SITES_MAX ? WS_LOCAL_WIDTH_SITES_MAX : count;
for (int i = 0; i < ws_local_width_count; ++i) ws_local_width_sites[i] = sites[i] & 0x1FFFFFFFu;
}
int psx_ws_is_local_viewport_width_site(uint32_t pc) {
for (int i = 0; i < ws_local_width_count; ++i)
if (ws_local_width_sites[i] == (pc & 0x1FFFFFFFu)) return 1;
return 0;
}
int32_t psx_ws_local_viewport_width(int32_t vanilla) {
GpuDisplayInfo di; gpu_get_display_info(&di);
if (!ws_local_viewport_cfg || ws_mode != 1 || !ws_vertical_split_active() ||
vanilla <= 0 || (int64_t)vanilla * 2 != (int)di.width) return vanilla;
return (int32_t)(((int64_t)vanilla * ws_xden + ws_xnum - 1) / ws_xnum);
}
void gpu_ws_set_local_viewport_native_wide(int enabled) {
ws_local_viewport_native = enabled ? 1 : 0;
}
void gpu_ws_set_netplay_local_viewport(int enabled, int slot) {
ws_local_viewport_cfg = enabled ? 1 : 0;
ws_local_viewport_slot = slot == 1 ? 1 : 0;
@@ -3172,7 +3204,9 @@ static int ws_local_viewport_draw_target(int *base_x) {
return 0;
GpuDisplayInfo di;
gpu_get_display_info(&di);
if (draw_area_intersects_rect(base, (int)di.display_y,
/* Native double buffering draws the next frame in the other Y band.
* The per-camera wide surface spans every band, just like the 1P surface. */
if (draw_area_intersects_rect(base, (int)draw_area_top,
src_w, (int)di.height)) {
if (base_x) *base_x = base;
return 1;
@@ -4638,12 +4672,34 @@ static void gp0_exec_mono_tri(void) {
}
/* Execute mono quad (GP0 0x28-0x2B) — two triangles: (0,1,2) and (2,1,3) */
/* A native split divider belongs to the combined framebuffer. Preserve it in
* canonical VRAM, but never mirror it into an expanded individual camera. */
static int ws_local_viewport_divider(int x0, int y0, int x1, int y1,
uint16_t color, int semi_trans) {
int base, src_w;
if (color || semi_trans || !ws_native_wide_active() ||
!ws_local_viewport_layout(&base, &src_w, NULL, NULL)) return 0;
GpuDisplayInfo di; gpu_get_display_info(&di);
const int seam = (int)di.display_x + (int)di.width / 2;
return x0 + draw_offset_x >= seam - 1 && x1 + draw_offset_x <= seam + 1 &&
x1 > x0 && y0 <= 0 && y1 >= (int)di.height;
}
static void gp0_exec_mono_quad(void) {
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu);
int32_t vx[4], vy[4];
for (int i = 0; i < 4; i++)
parse_vertex(gp0_cmd_buf[1 + i], &vx[i], &vy[i]);
if (vx[0] == vx[2] && vx[1] == vx[3] && vy[0] == vy[1] && vy[2] == vy[3] &&
ws_local_viewport_divider(vx[0], vy[0], vx[1], vy[2], color, semi_trans)) {
gr_wide_disable_target();
gr_set_semi_transparency(0, (int)semi_transparency);
gr_draw_flat_rect(vx[0] + draw_offset_x, vy[0] + draw_offset_y,
vx[1] - vx[0], vy[2] - vy[0], color);
ws_nw_sync_target();
return;
}
int rej_a = gpu_triangle_rejected(vx, vy, 0, 1, 2);
int rej_b = gpu_triangle_rejected(vx, vy, 2, 1, 3);
if (rej_a && rej_b) return;
@@ -5169,6 +5225,15 @@ static void gp0_exec_mono_line(void) {
parse_vertex(gp0_cmd_buf[1], &x0, &y0);
parse_vertex(gp0_cmd_buf[2], &x1, &y1);
if (psx_gpu_line_oversize(x0, y0, x1, y1)) return;
if (x0 == x1 && ws_local_viewport_divider(x0, y0 < y1 ? y0 : y1,
x0 + 1, (y0 > y1 ? y0 : y1) + 1, color, semi_trans)) {
gr_wide_disable_target();
gr_set_semi_transparency(0, (int)semi_transparency);
gr_draw_line(x0 + draw_offset_x, y0 + draw_offset_y,
x1 + draw_offset_x, y1 + draw_offset_y, color);
ws_nw_sync_target();
return;
}
int32_t vx[2] = { x0, x1 };
ws_nw_hud_shift_vertices(vx, 2);
x0 = vx[0]; x1 = vx[1];
@@ -5214,6 +5279,13 @@ static void gp0_exec_mono_rect(void) {
int h = (gp0_cmd_buf[2] >> 16) & 0xFFFFu;
if (w > 1023) w = 1023;
if (h > 511) h = 511;
if (ws_local_viewport_divider(x0, y0, x0 + w, y0 + h, color, semi_trans)) {
gr_wide_disable_target();
gr_set_semi_transparency(0, (int)semi_transparency);
gr_draw_flat_rect(x0 + draw_offset_x, y0 + draw_offset_y, w, h, color);
ws_nw_sync_target();
return;
}
ws_expand_fullscreen_rect(&x0, y0, &w, h);
/* Same auto_ui squash the textured rect path gets. Without it a flat
* -coloured HUD mark keeps its 4:3 X while the textured primitives of the
@@ -6178,7 +6250,8 @@ void gpu_ws_prepass_linked_list(uint32_t start_addr) {
* threshold, which pinned every HUD group to the 4:3 centre. Keep edge
* groups edge-anchored in those frames so the HUD adapts to the wide view.
*/
ws_auto_ui_dense = ws_ui_prepass_count >= 32u && !ws_gte_game_mode_cfg;
ws_auto_ui_dense = ws_ui_prepass_count >= 32u && !ws_gte_game_mode_cfg &&
!ws_local_native_split();
if (ws_ui_prepass_count == 0) return;
const int32_t group_origin = ws_disp_x();
+4 -1
View File
@@ -2489,7 +2489,10 @@ static int wide_fast_center_valid(void) {
/* An explicitly stretched sky differs inside the canonical viewport too.
* Keep the full composite for those scenes, including later foreground
* draws. Tags are installed before DMA, so no earlier center draws skip. */
return s_wide_fast && !gpu_ws_background_requires_full_composite();
// The native split seam occupies canonical edge columns that the expanded
// camera renders as world. Copying those columns reinstates the divider.
return s_wide_fast && gpu_ws_netplay_local_viewport_width() <= 0 &&
!gpu_ws_background_requires_full_composite();
}
static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h); /* def below */
/* True if [lo,hi] (canonical draw-x) lies strictly inside the 4:3 frame, so the
+27 -5
View File
@@ -1479,10 +1479,15 @@ static bool ensure_sdl_pixel_buf_capacity(size_t pixels) {
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 */
static int g_netplay_local_viewport_aspect = 0; /* 0 off, 1 16:9, 2 21:9, 3 adaptive, 4 fixed mod choice */
/* [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;
/* Native-wide per-camera views ([netplay] local_viewport_renderer =
* "native_wide"). Projection views keep the shared presentation path. */
static bool netplay_local_viewport_native_wide(void) {
return g_netplay_local_viewport == 1 && !g_netplay_local_viewport_projection;
}
extern "C" int psx_mod_set_fixed_display_aspect(
uint32_t numerator, uint32_t denominator) {
@@ -1948,7 +1953,10 @@ static void refresh_widescreen_projection() {
const bool local_native_wide =
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)
/* Native-wide local views follow the title's own renderer offline;
* projection local views keep their per-camera projection throughout. */
const bool native_wide = (g_netplay_local_viewport == 1 &&
(g_netplay_local_viewport_projection || psx_netplay_active()))
? local_native_wide
: (g_ws_native_wide != 0);
const int mode = wide ? (native_wide ? 2 : 1) : 0;
@@ -3271,6 +3279,12 @@ static void apply_netplay_local_viewport_aspect(bool netplay_enabled) {
(void)psx_mod_set_fixed_display_aspect(16u, 9u);
(void)psx_mod_set_adaptive_display_aspect(21u, 9u);
break;
case 4:
// The admitted mod plan selects a fixed ratio before play. Window
// resizing cannot change it; other offline choices default to 16:9.
(void)psx_mod_set_fixed_display_aspect(
g_video_aspect_num * 9 == g_video_aspect_den * 21 ? 21u : 16u, 9u);
break;
default:
break;
}
@@ -8138,7 +8152,9 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
* wide (compositor unsupported, or the surface fallback below)
* pillarboxes 4:3 like FMV/menus instead. Only squash mode (1) may
* stretch: its canonical content is pre-squashed FOR the stretch. */
const bool nw_pin = g_ws_engaged && g_ws_native_wide;
const bool nw_pin = g_ws_engaged &&
(netplay_local_viewport_native_wide() ? g_ws_projection_mode == 2
: g_ws_native_wide != 0);
/* Ring the classification now that it's final (only the software/CPU
* wide path below can still fall back — it amends this entry). A
@@ -8171,7 +8187,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
return ep;
}
if (g_gl_active && g_gl_fbo_present && !di.depth24 &&
!local_viewport_crop) {
(!local_viewport_crop || local_viewport_wide)) {
if (wide_present) {
/* GPU-direct native-wide present: blit the displayed buffer's
* wide FBO straight to the window (GPU-side, like the canonical
@@ -8180,7 +8196,9 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
* GL-only slowdown (SW's wide path is pure CPU, no GPU sync, so
* SW stayed smooth). Falls through to the CPU readout path only if
* the wide surface for this buffer doesn't exist yet. */
if (gl_renderer_present_wide_fbo((int)di.display_x, (int)di.display_y,
const int wide_base = local_viewport_wide
? gpu_ws_netplay_local_viewport_base_x() : (int)di.display_x;
if (gl_renderer_present_wide_fbo(wide_base, (int)di.display_y,
(int)h, g_video_aa ? 1 : 0)) {
netplay_note_present();
return ep;
@@ -14048,9 +14066,13 @@ int main(int argc, char** argv) {
g_netplay_local_viewport_aspect =
(gc.netplay_local_viewport_aspect == "16:9") ? 1 :
(gc.netplay_local_viewport_aspect == "21:9") ? 2 :
(gc.netplay_local_viewport_aspect == "fixed") ? 4 :
(gc.netplay_local_viewport_aspect == "adaptive") ? 3 : 0;
gpu_ws_set_local_viewport_width_sites(gc.netplay_local_viewport_width_sites.data(), (int)gc.netplay_local_viewport_width_sites.size());
g_netplay_local_viewport_projection =
(gc.netplay_local_viewport_renderer == "projection") ? 1 : 0;
gpu_ws_set_local_viewport_native_wide(
g_netplay_local_viewport == 1 && !g_netplay_local_viewport_projection);
gpu_ws_set_local_viewport_state_gate(
gc.netplay_local_viewport_state_addr,
gc.netplay_local_viewport_state_values.data(),
+2
View File
@@ -23,6 +23,8 @@ int gpu_ws_nw_flat_backdrop_enabled(void){return 0;}
int g_ws_tex_edge_pct=0;
int psx_ws_prim_is_tagged(void){return 0;}
void gpu_depth24_upload_span_reset(void){}
/* Netplay unsplit view and forward-pass opt-in: off in these fixtures. */
int gpu_ws_netplay_local_viewport_width(void){return 0;}
void frame_interpolation_schedule_reset(FrameInterpolationSchedule *p){memset(p,0,sizeof(*p));}
void frame_flip_tracker_reset(FrameFlipTracker *p){memset(p,0,sizeof(*p));p->period=1;}
static int checks,failures;
+2
View File
@@ -80,6 +80,8 @@ int g_ws_tex_edge_pct=0;
int psx_ws_prim_is_tagged(void){return 0;}
void psx_ws_dbg_gate_frame_snapshot(void){}
void gpu_depth24_upload_span_reset(void){}
/* Netplay unsplit view and forward-pass opt-in: off in these fixtures. */
int gpu_ws_netplay_local_viewport_width(void){return 0;}
/* Guest clock the renderer's stereo-pair freshness reads; test stand-ins, as
* in test_gl_readback_region.c. */
uint64_t psx_cycle_count=0;
@@ -86,7 +86,62 @@ static void expect_shared(int shared) {
}
}
static void width_sites(void) {
counter(1,0);
hres1=0; hres2=1; // 368 would not represent V82; use its 320 mode below.
hres1=1; hres2=0;
GpuDisplayInfo di; gpu_get_display_info(&di); assert(di.width==320);
uint32_t sites[]={0x8001C248,0x8002E01C};
gpu_ws_set_local_viewport_width_sites(sites,2);
assert(psx_ws_is_local_viewport_width_site(0xA001C248));
assert(!psx_ws_is_local_viewport_width_site(0x8001C24C));
for(int slot=0;slot<2;slot++) {
gpu_ws_set_netplay_local_viewport(1,slot);
assert(psx_ws_local_viewport_width(160)==427);
assert(psx_ws_local_viewport_width(320)==320);
assert(psx_ws_local_viewport_width(INT32_MAX)==INT32_MAX);
assert(psx_ws_local_viewport_width(-1)==-1);
}
test_ram[GATE/4]=1; assert(psx_ws_local_viewport_width(160)==160);
test_ram[GATE/4]=0; ws_mode=2; assert(psx_ws_local_viewport_width(160)==160);
ws_mode=1; gpu_ws_set_netplay_local_viewport(0,0); assert(psx_ws_local_viewport_width(160)==160);
}
static void native_wide_buffers(void) {
counter(1, 0);
hres1 = 1; hres2 = 0;
ws_mode = 2; ws_cfg_num = 16; ws_cfg_den = 9;
for (int slot = 0; slot < 2; ++slot) {
ws_local_viewport_slot = slot;
assert(gpu_ws_netplay_local_viewport_width() == 426);
for (int band = 0; band < 2; ++band) {
display_area_y = 240 * (1 - band);
draw_area_top = draw_offset_y = 240 * band;
draw_area_bottom = draw_area_top + 239;
draw_area_left = slot ? 161 : 0;
draw_area_right = slot ? 319 : 159;
int base = -1;
assert(ws_local_viewport_draw_target(&base));
assert(base == 160 * slot);
draw_area_left = slot ? 0 : 161;
draw_area_right = slot ? 159 : 319;
assert(!ws_local_viewport_draw_target(&base));
assert(ws_local_viewport_divider(160, 0, 161, 240, 0, 0));
assert(!ws_local_viewport_divider(160, 0, 161, 20, 0, 0));
assert(!ws_local_viewport_divider(160, 0, 161, 240, 1, 0));
assert(!ws_local_viewport_divider(160, 0, 161, 240, 0, 1));
assert(!ws_local_viewport_divider(159, 0, 163, 240, 0, 0));
}
}
ws_cfg_num = 21;
assert(gpu_ws_netplay_local_viewport_width() == 560);
ws_local_viewport_cfg = 0;
assert(!ws_local_viewport_divider(160, 0, 161, 240, 0, 0));
}
int main(void) {
native_wide_buffers();
width_sites();
counter(1, 0);
expect_shared(1);
for (unsigned slot = 0; slot < 2; slot++) {