mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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 @@
|
||||
833e65ea87826617c22ddb8598694feb750f9d4f9faa432976e006af40911129
|
||||
9c92d6060348bc2478abb2370e9c9142ee9c0d1d0ec8dc05d9be98177545e6e2
|
||||
|
||||
@@ -1 +1 @@
|
||||
5fdf6a427293e5169e5f9c249d6194a25e6b9cc5a78ae3116d956c6d0feefe3e
|
||||
26c707e2203f6886cc6c73098d2072fc811b278f55521fa0e945fa30b2def17f
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
Reference in New Issue
Block a user