mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge pull request #147 from mstan/codex/netplay-local-viewport
Add opt-in netplay local viewport extraction
This commit is contained in:
@@ -146,6 +146,14 @@ struct CodeGenConfig {
|
||||
// (INT32_MAX while revealed, vanilla at 4:3); empty by default.
|
||||
std::set<uint32_t> ws_cull_xclip_load_sites;
|
||||
|
||||
// Exact `bltz MAC0, reject`-style NCLIP/backface rejects to suppress while
|
||||
// widescreen reveals extra world. 4:3 keeps the original branch predicate.
|
||||
std::set<uint32_t> ws_cull_nclip_keep_sites;
|
||||
|
||||
// Exact branch PCs whose reject target is suppressed while widescreen
|
||||
// reveals extra world. 4:3 keeps the original branch predicate.
|
||||
std::set<uint32_t> ws_cull_branch_keep_sites;
|
||||
|
||||
// Exact, full-word-guarded comparison sites whose result is forced only
|
||||
// while widescreen reveals extra world. 4:3 evaluates the original compare.
|
||||
std::vector<PSXRecompV4::WidescreenCullKeepSite> ws_cull_keep_sites;
|
||||
|
||||
@@ -738,6 +738,12 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
|
||||
uint32_t opcode = (instr >> 26) & 0x3F;
|
||||
uint32_t rs = get_rs(instr);
|
||||
uint32_t rt = get_rt(instr);
|
||||
auto keep_branch_if_wide = [&](std::string cond) {
|
||||
if (!config_.ws_cull_branch_keep_sites.count(addr))
|
||||
return cond;
|
||||
return fmt::format("psx_ws_x_margin() > 0 ? 0 : ({}) /* ws branch keep */",
|
||||
cond);
|
||||
};
|
||||
|
||||
// REGIMM branches. R3000A hardware decodes EVERY rt value here, not just
|
||||
// the four assembler mnemonics: the branch sense is rt bit 0 (0 = bltz,
|
||||
@@ -759,9 +765,14 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
|
||||
(ws_cull_bltz_pcs_.count(addr) || config_.ws_cull_bltz_sites.count(addr)))
|
||||
return fmt::format("psx_ws_cull_bltz({}) /* ws cull (left edge) */",
|
||||
reg_name(rs));
|
||||
return fmt::format("(int32_t){} < 0", reg_name(rs));
|
||||
if (regimm_op == 0x00 && config_.ws_cull_nclip_keep_sites.count(addr))
|
||||
return fmt::format("psx_ws_x_margin() > 0 ? 0 : ((int32_t){} < 0) /* ws nclip keep */",
|
||||
reg_name(rs));
|
||||
return keep_branch_if_wide(
|
||||
fmt::format("(int32_t){} < 0", reg_name(rs)));
|
||||
} else { // bgez family (incl. bgezal + undefined mirrors)
|
||||
return fmt::format("(int32_t){} >= 0", reg_name(rs));
|
||||
return keep_branch_if_wide(
|
||||
fmt::format("(int32_t){} >= 0", reg_name(rs)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,22 +780,26 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
|
||||
switch (opcode) {
|
||||
case 0x04: // beq
|
||||
case 0x14: // beql
|
||||
return fmt::format("{} == {}", reg_name(rs), reg_name(rt));
|
||||
return keep_branch_if_wide(
|
||||
fmt::format("{} == {}", reg_name(rs), reg_name(rt)));
|
||||
|
||||
case 0x05: // bne
|
||||
case 0x15: // bnel
|
||||
return fmt::format("{} != {}", reg_name(rs), reg_name(rt));
|
||||
return keep_branch_if_wide(
|
||||
fmt::format("{} != {}", reg_name(rs), reg_name(rt)));
|
||||
|
||||
case 0x06: // blez
|
||||
case 0x16: // blezl
|
||||
return fmt::format("(int32_t){} <= 0", reg_name(rs));
|
||||
return keep_branch_if_wide(
|
||||
fmt::format("(int32_t){} <= 0", reg_name(rs)));
|
||||
|
||||
case 0x07: // bgtz
|
||||
case 0x17: // bgtzl
|
||||
return fmt::format("(int32_t){} > 0", reg_name(rs));
|
||||
return keep_branch_if_wide(
|
||||
fmt::format("(int32_t){} > 0", reg_name(rs)));
|
||||
}
|
||||
|
||||
return "0 /* unknown branch condition: defaults to not-taken */";
|
||||
return keep_branch_if_wide("0 /* unknown branch condition: defaults to not-taken */");
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) {
|
||||
|
||||
@@ -72,6 +72,8 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) {
|
||||
h.words("cull_depth", c.ws_cull_depth_sites);
|
||||
h.words("cull_plane_nx", c.ws_cull_plane_nx_sites);
|
||||
h.words("cull_xclip_load", c.ws_cull_xclip_load_sites);
|
||||
h.words("cull_nclip_keep", c.ws_cull_nclip_keep_sites);
|
||||
h.words("cull_branch_keep", c.ws_cull_branch_keep_sites);
|
||||
h.words("cull_w_imms", c.ws_cull_w_imms);
|
||||
h.words("cull_h_imms", c.ws_cull_h_imms);
|
||||
h.words("backdrop_x", c.ws_backdrop_x_sites);
|
||||
@@ -327,10 +329,10 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!cfg.contains("runtime")) return rt;
|
||||
const toml::value& runtime = toml::find(cfg, "runtime");
|
||||
if (runtime.contains("language")) // [runtime].language convenience alias
|
||||
rt.language = toml::find<std::string>(runtime, "language");
|
||||
if (cfg.contains("runtime")) {
|
||||
const toml::value& runtime = toml::find(cfg, "runtime");
|
||||
if (runtime.contains("language")) // [runtime].language convenience alias
|
||||
rt.language = toml::find<std::string>(runtime, "language");
|
||||
|
||||
if (runtime.contains("debug_port")) {
|
||||
const auto port = toml::find<int64_t>(runtime, "debug_port");
|
||||
@@ -502,9 +504,10 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
if (runtime.contains("overlay_backend")) {
|
||||
rt.overlay_backend = toml::find<std::string>(runtime, "overlay_backend");
|
||||
}
|
||||
if (runtime.contains("overlay_native_block")) {
|
||||
for (const auto& a : toml::find<std::vector<std::string>>(runtime, "overlay_native_block")) {
|
||||
rt.overlay_native_block.push_back(parse_hex(a, "runtime.overlay_native_block"));
|
||||
if (runtime.contains("overlay_native_block")) {
|
||||
for (const auto& a : toml::find<std::vector<std::string>>(runtime, "overlay_native_block")) {
|
||||
rt.overlay_native_block.push_back(parse_hex(a, "runtime.overlay_native_block"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,6 +712,16 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
toml::find<std::string>(ct, "p2_mode"), PAD_MODE_ANALOG);
|
||||
rt.has_default_mode = true;
|
||||
}
|
||||
if (ct.contains("p1_device")) {
|
||||
rt.default_p1_device = toml::find<std::string>(ct, "p1_device");
|
||||
if (!rt.default_p1_device.empty())
|
||||
rt.has_default_p1_device = true;
|
||||
}
|
||||
if (ct.contains("p2_device")) {
|
||||
rt.default_p2_device = toml::find<std::string>(ct, "p2_device");
|
||||
if (!rt.default_p2_device.empty())
|
||||
rt.has_default_p2_device = true;
|
||||
}
|
||||
if (ct.contains("lock_mode")) {
|
||||
rt.controller_lock_mode = toml::find<bool>(ct, "lock_mode");
|
||||
}
|
||||
@@ -1187,6 +1200,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
bool has_netplay_required_leadout = false;
|
||||
uint32_t netplay_required_leadout_lba = 0;
|
||||
std::string netplay_required_disc_fp;
|
||||
std::string netplay_local_viewport;
|
||||
std::string netplay_local_viewport_aspect;
|
||||
if (cfg.contains("netplay")) {
|
||||
const toml::value& np = toml::find(cfg, "netplay");
|
||||
if (np.contains("require_cue"))
|
||||
@@ -1203,6 +1218,37 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
for (char& c : netplay_required_disc_fp)
|
||||
c = (char)std::tolower((unsigned char)c);
|
||||
}
|
||||
if (np.contains("local_viewport")) {
|
||||
netplay_local_viewport = toml::find<std::string>(np, "local_viewport");
|
||||
for (char& c : netplay_local_viewport)
|
||||
c = (char)std::tolower((unsigned char)c);
|
||||
if (!netplay_local_viewport.empty() &&
|
||||
netplay_local_viewport != "vertical_split") {
|
||||
throw std::runtime_error(fmt::format(
|
||||
"[netplay] local_viewport must be \"vertical_split\", got '{}'",
|
||||
netplay_local_viewport));
|
||||
}
|
||||
}
|
||||
if (np.contains("local_viewport_aspect")) {
|
||||
netplay_local_viewport_aspect =
|
||||
toml::find<std::string>(np, "local_viewport_aspect");
|
||||
for (char& c : netplay_local_viewport_aspect)
|
||||
c = (char)std::tolower((unsigned char)c);
|
||||
if (!netplay_local_viewport_aspect.empty() &&
|
||||
netplay_local_viewport_aspect != "16:9" &&
|
||||
netplay_local_viewport_aspect != "21:9" &&
|
||||
netplay_local_viewport_aspect != "adaptive") {
|
||||
throw std::runtime_error(fmt::format(
|
||||
"[netplay] local_viewport_aspect must be \"16:9\", "
|
||||
"\"21:9\", or \"adaptive\", got '{}'",
|
||||
netplay_local_viewport_aspect));
|
||||
}
|
||||
if (!netplay_local_viewport_aspect.empty() &&
|
||||
netplay_local_viewport.empty()) {
|
||||
throw std::runtime_error(
|
||||
"[netplay] local_viewport_aspect requires local_viewport");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [recompiler]
|
||||
@@ -1314,6 +1360,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
bool ws_auto_ui_squash = false;
|
||||
bool ws_full_2d = false;
|
||||
bool ws_gte_game_mode = false;
|
||||
bool ws_precise_nclip = false;
|
||||
uint32_t ws_gameplay_state_addr = 0;
|
||||
std::vector<uint32_t> ws_gameplay_state_values;
|
||||
bool ws_native_wide = true;
|
||||
@@ -1461,6 +1508,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
ws_full_2d = toml::find<bool>(ws, "full_2d");
|
||||
if (ws.contains("gte_game_mode"))
|
||||
ws_gte_game_mode = toml::find<bool>(ws, "gte_game_mode");
|
||||
if (ws.contains("precise_nclip"))
|
||||
ws_precise_nclip = toml::find<bool>(ws, "precise_nclip");
|
||||
const bool has_gameplay_state_addr = ws.contains("gameplay_state_addr");
|
||||
const bool has_gameplay_state_values = ws.contains("gameplay_state_values");
|
||||
if (has_gameplay_state_addr != has_gameplay_state_values)
|
||||
@@ -1563,6 +1612,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
std::vector<uint32_t> ws_cull_depth_sites;
|
||||
std::vector<uint32_t> ws_cull_plane_nx_sites;
|
||||
std::vector<uint32_t> ws_cull_xclip_load_sites;
|
||||
std::vector<uint32_t> ws_cull_nclip_keep_sites;
|
||||
std::vector<uint32_t> ws_cull_branch_keep_sites;
|
||||
std::vector<WidescreenCullKeepSite> ws_cull_keep_sites;
|
||||
std::vector<WidescreenAngleSite> ws_cull_angle_sites;
|
||||
WidescreenAspectConeConfig ws_aspect_cone;
|
||||
@@ -1596,6 +1647,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
load_sites("depth_sites", ws_cull_depth_sites);
|
||||
load_sites("plane_nx_sites", ws_cull_plane_nx_sites);
|
||||
load_sites("xclip_load_sites", ws_cull_xclip_load_sites);
|
||||
load_sites("nclip_keep_sites", ws_cull_nclip_keep_sites);
|
||||
load_sites("branch_keep_sites", ws_cull_branch_keep_sites);
|
||||
if (cull.contains("keep")) {
|
||||
std::set<uint32_t> seen;
|
||||
for (const auto& item : toml::find<toml::array>(cull, "keep")) {
|
||||
@@ -1982,6 +2035,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
/*has_netplay_required_leadout*/ has_netplay_required_leadout,
|
||||
/*netplay_required_leadout_lba*/ netplay_required_leadout_lba,
|
||||
/*netplay_required_disc_fp*/ netplay_required_disc_fp,
|
||||
/*netplay_local_viewport*/ netplay_local_viewport,
|
||||
/*netplay_local_viewport_aspect*/ netplay_local_viewport_aspect,
|
||||
/*seeds_path*/ seeds_path,
|
||||
/*bios_thunks_path*/ bios_thunks_path,
|
||||
/*bios_config_path*/ bios_config_path,
|
||||
@@ -2020,6 +2075,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
/*ws_cull_depth_sites*/ ws_cull_depth_sites,
|
||||
/*ws_cull_plane_nx_sites*/ ws_cull_plane_nx_sites,
|
||||
/*ws_cull_xclip_load_sites*/ ws_cull_xclip_load_sites,
|
||||
/*ws_cull_nclip_keep_sites*/ ws_cull_nclip_keep_sites,
|
||||
/*ws_cull_branch_keep_sites*/ ws_cull_branch_keep_sites,
|
||||
/*ws_cull_keep_sites*/ ws_cull_keep_sites,
|
||||
/*ws_cull_angle_sites*/ ws_cull_angle_sites,
|
||||
/*ws_aspect_cone*/ ws_aspect_cone,
|
||||
@@ -2034,6 +2091,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
/*ws_auto_backdrop_preload*/ ws_auto_backdrop_preload,
|
||||
/*ws_full_2d*/ ws_full_2d,
|
||||
/*ws_gte_game_mode*/ ws_gte_game_mode,
|
||||
/*ws_precise_nclip*/ ws_precise_nclip,
|
||||
/*ws_gameplay_state_addr*/ ws_gameplay_state_addr,
|
||||
/*ws_gameplay_state_values*/ ws_gameplay_state_values,
|
||||
/*ws_native_wide*/ ws_native_wide,
|
||||
|
||||
@@ -515,6 +515,15 @@ struct RuntimeConfig {
|
||||
int default_p1_mode = PAD_MODE_ANALOG;
|
||||
int default_p2_mode = PAD_MODE_ANALOG;
|
||||
|
||||
// p1_device / p2_device: optional per-game default input sources for
|
||||
// fresh settings and launcher-less boots. Same vocabulary as settings.toml:
|
||||
// "none", "keyboard", "auto"/"gamepad"/"controller", or an SDL GUID.
|
||||
// Per-install settings.toml still overrides these defaults.
|
||||
bool has_default_p1_device = false;
|
||||
bool has_default_p2_device = false;
|
||||
std::string default_p1_device;
|
||||
std::string default_p2_device;
|
||||
|
||||
// lock_mode: when true the launcher HIDES the whole pad-mode selector
|
||||
// (Hybrid | Analog | D-Pad) and forces every port to default_p1_mode. For a
|
||||
// game that supports exactly one pad type — e.g. Tomba 2, whose driver only
|
||||
@@ -694,6 +703,15 @@ struct GameConfig {
|
||||
bool has_netplay_required_leadout = false;
|
||||
uint32_t netplay_required_leadout_lba = 0;
|
||||
std::string netplay_required_disc_fp; // lowercase hex SHA-256
|
||||
// local_viewport = "vertical_split": while real netplay is active, crop
|
||||
// presentation to this peer's left/right split-screen half. This is a
|
||||
// presentation-only helper for titles that still render native split-screen
|
||||
// in netplay; unset keeps every peer seeing the full framebuffer.
|
||||
std::string netplay_local_viewport;
|
||||
// Optional display aspect to use with local_viewport. Accepted values:
|
||||
// "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;
|
||||
|
||||
// [recompiler] block
|
||||
std::filesystem::path seeds_path; // absolute path to seeds (text or json)
|
||||
@@ -830,6 +848,14 @@ struct GameConfig {
|
||||
// scissor clips the overflow and wrapped off-left coords pass); the
|
||||
// vanilla loaded value at 4:3. Empty by default; regen required.
|
||||
std::vector<uint32_t> ws_cull_xclip_load_sites;
|
||||
// Exact `bltz MAC0, reject`-style NCLIP/backface rejects that are forced
|
||||
// not-taken only while widescreen reveals extra world. This is deliberately
|
||||
// separate from bltz_sites, whose helper adjusts screen-X edge thresholds.
|
||||
std::vector<uint32_t> ws_cull_nclip_keep_sites;
|
||||
// Exact branch PCs whose reject path is forced not-taken only while
|
||||
// widescreen reveals extra world. Use only after screenshot-validated
|
||||
// evidence that the target is a visibility reject.
|
||||
std::vector<uint32_t> ws_cull_branch_keep_sites;
|
||||
// Exact comparison sites whose result is forced only while widescreen
|
||||
// reveals extra world. Used for proven object/model participation gates
|
||||
// where maximal overdraw is preferable to range guessing. Each entry is
|
||||
@@ -921,6 +947,14 @@ struct GameConfig {
|
||||
// (native-wide engages); genuine full-2D screens (save/options) still
|
||||
// pillarbox 4:3. Runtime-only — no regen required. Off by default.
|
||||
bool ws_gte_game_mode = false;
|
||||
|
||||
// [widescreen] precise_nclip — use the runtime's unsaturated GTE projection
|
||||
// provenance for NCLIP/backface tests while classic adaptive widescreen is
|
||||
// active. This is for 3D titles whose wide side geometry otherwise hits the
|
||||
// PS1 SXY +/-1024 clamp and then disappears from game-side visibility tests.
|
||||
// Runtime-only; off by default.
|
||||
bool ws_precise_nclip = false;
|
||||
|
||||
// Optional authoritative game-state gate for titles whose menus also
|
||||
// render enough 3D geometry to fool gte_game_mode. When configured,
|
||||
// native-wide is active only while the guest word matches one listed
|
||||
|
||||
@@ -209,6 +209,8 @@ int main(int argc, char** argv) {
|
||||
std::set<uint32_t> ws_cull_depth; // [widescreen.cull] depth_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
|
||||
std::set<uint32_t> ws_cull_branch_keep; // [widescreen.cull] branch_keep_sites
|
||||
std::vector<PSXRecompV4::WidescreenCullKeepSite> ws_cull_keep;
|
||||
std::vector<PSXRecompV4::WidescreenAngleSite> ws_cull_angle;
|
||||
PSXRecompV4::WidescreenAspectConeConfig ws_aspect_cone;
|
||||
@@ -270,6 +272,8 @@ int main(int argc, char** argv) {
|
||||
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());
|
||||
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_branch_keep.insert(cfg.ws_cull_branch_keep_sites.begin(), cfg.ws_cull_branch_keep_sites.end());
|
||||
ws_cull_keep = cfg.ws_cull_keep_sites;
|
||||
ws_cull_angle = cfg.ws_cull_angle_sites;
|
||||
ws_aspect_cone = cfg.ws_aspect_cone;
|
||||
@@ -363,6 +367,8 @@ int main(int argc, char** argv) {
|
||||
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());
|
||||
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_branch_keep.insert(wscfg.ws_cull_branch_keep_sites.begin(), wscfg.ws_cull_branch_keep_sites.end());
|
||||
if (ws_cull_keep.empty()) ws_cull_keep = wscfg.ws_cull_keep_sites;
|
||||
if (ws_cull_angle.empty()) ws_cull_angle = wscfg.ws_cull_angle_sites;
|
||||
if (ws_aspect_cone.sites.empty())
|
||||
@@ -1225,6 +1231,8 @@ int main(int argc, char** argv) {
|
||||
codegen_config.ws_cull_depth_sites = ws_cull_depth;
|
||||
codegen_config.ws_cull_plane_nx_sites = ws_cull_plane_nx;
|
||||
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_branch_keep_sites = ws_cull_branch_keep;
|
||||
codegen_config.ws_cull_keep_sites = ws_cull_keep;
|
||||
codegen_config.ws_cull_angle_sites = ws_cull_angle;
|
||||
codegen_config.ws_aspect_cone = ws_aspect_cone;
|
||||
|
||||
@@ -142,6 +142,36 @@ buffer_ms = 60
|
||||
check(audio_config.runtime.audio_buffer_ms == 60,
|
||||
"parser preserves per-game audio buffer target");
|
||||
|
||||
const auto controller_defaults = write_config(root, "controller-defaults", R"toml(
|
||||
[controller]
|
||||
p1_device = "keyboard"
|
||||
p2_device = "gamepad"
|
||||
p2_mode = "digital"
|
||||
)toml");
|
||||
const auto controller_defaults_config =
|
||||
PSXRecompV4::load_game_config(controller_defaults);
|
||||
check(controller_defaults_config.runtime.has_default_p1_device &&
|
||||
controller_defaults_config.runtime.default_p1_device == "keyboard" &&
|
||||
controller_defaults_config.runtime.has_default_p2_device &&
|
||||
controller_defaults_config.runtime.default_p2_device == "gamepad" &&
|
||||
controller_defaults_config.runtime.default_p2_mode ==
|
||||
PSXRecompV4::PAD_MODE_DIGITAL,
|
||||
"parser preserves per-game controller device defaults");
|
||||
|
||||
const auto netplay_viewport = write_config(root, "netplay-viewport", R"toml(
|
||||
[runtime]
|
||||
|
||||
[netplay]
|
||||
local_viewport = "vertical_split"
|
||||
local_viewport_aspect = "adaptive"
|
||||
)toml");
|
||||
const auto netplay_viewport_config =
|
||||
PSXRecompV4::load_game_config(netplay_viewport);
|
||||
check(netplay_viewport_config.netplay_local_viewport == "vertical_split",
|
||||
"parser preserves netplay local viewport mode");
|
||||
check(netplay_viewport_config.netplay_local_viewport_aspect == "adaptive",
|
||||
"parser preserves netplay local viewport aspect");
|
||||
|
||||
const auto bad_audio_buffer = write_config(root, "bad-audio-buffer", R"toml(
|
||||
[runtime]
|
||||
|
||||
@@ -206,6 +236,15 @@ negsub_sites = ["0x80012340"]
|
||||
std::vector<uint32_t>{0x80012340u},
|
||||
"parser preserves negsub cull sites");
|
||||
|
||||
const auto branch_keep = write_config(root, "branch-keep", R"toml(
|
||||
[widescreen.cull]
|
||||
branch_keep_sites = ["0x80012340"]
|
||||
)toml");
|
||||
const auto branch_keep_config = PSXRecompV4::load_game_config(branch_keep);
|
||||
check(branch_keep_config.ws_cull_branch_keep_sites ==
|
||||
std::vector<uint32_t>{0x80012340u},
|
||||
"parser preserves branch keep sites");
|
||||
|
||||
const auto vxrange = write_config(root, "vxrange", R"toml(
|
||||
[widescreen.cull]
|
||||
vxrange_sites = ["0x80012340"]
|
||||
@@ -680,6 +719,16 @@ void codegen_tests() {
|
||||
std::string::npos,
|
||||
"activation bias gains the same isolated resident-object lead");
|
||||
|
||||
PSXRecomp::CodeGenConfig branch_keep_config;
|
||||
branch_keep_config.ws_cull_branch_keep_sites.insert(0x80010000u);
|
||||
const std::string branch_keep = generate_first_instruction(
|
||||
0x14400002u, {}, false, branch_keep_config); // bne v0,zero,+2
|
||||
check(branch_keep.find(
|
||||
"psx_ws_x_margin() > 0 ? 0 : (cpu->gpr[2] != cpu->gpr[0])") !=
|
||||
std::string::npos &&
|
||||
branch_keep.find("ws branch keep") != std::string::npos,
|
||||
"codegen emits guarded branch keep predicate");
|
||||
|
||||
PSXRecomp::CodeGenConfig plane_nx_config;
|
||||
plane_nx_config.ws_cull_plane_nx_sites.insert(0x80010000u);
|
||||
const std::string plane_nx = generate_first_instruction(
|
||||
|
||||
@@ -205,6 +205,7 @@ extern void gte_precision_timeline_invalidate(void);
|
||||
extern void gte_precision_speculative_begin(void);
|
||||
extern void gte_precision_speculative_end(void);
|
||||
extern void gte_precision_store_word(uint32_t addr, uint8_t reg);
|
||||
extern void gte_precision_tracking_set(int enabled);
|
||||
/* Sub-pixel vertex precision ([video] geometry_correction). Enables the side
|
||||
* cache that retains the 16.16 projection fraction the GTE discards when it
|
||||
* saturates SXY to integer screen pixels. Guest-visible GTE state is
|
||||
|
||||
@@ -75,6 +75,11 @@ typedef struct {
|
||||
int32_t offset_x, offset_y;
|
||||
} GpuDrawArea;
|
||||
void gpu_get_draw_area(GpuDrawArea* out);
|
||||
/* Non-zero when the frame that just reached vblank used two side-by-side draw
|
||||
* 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);
|
||||
void gpu_vertical_split_debug(int *active, int *left_age, int *right_age);
|
||||
uint16_t gpu_vram_peek(int x, int y);
|
||||
|
||||
/* Shaded quad vertex capture (Phase 4.5 debug). */
|
||||
@@ -317,6 +322,7 @@ uint32_t gpu_texture_correction_hits(void);
|
||||
* with no sprite-tag helper: gte.cpp notes every RTPS/RTPT projection; a frame
|
||||
* that projects enough vertices is stamped as gameplay. */
|
||||
void gpu_ws_set_gte_game_mode(int on);
|
||||
void gpu_ws_set_precise_nclip(int on);
|
||||
void psx_ws_note_gte_project(int nverts);
|
||||
/* Optional authoritative gameplay-state gate. When configured, it replaces
|
||||
* heuristic gameplay classification for native-wide presentation. */
|
||||
|
||||
@@ -5281,6 +5281,8 @@ static void handle_gpu_state(int id, const char *json)
|
||||
gpu_get_draw_area(&da);
|
||||
uint64_t nop, fill, draw, env, copy;
|
||||
gpu_get_gp0_stats(&nop, &fill, &draw, &env, ©);
|
||||
int split_active = 0, split_left_age = 0, split_right_age = 0;
|
||||
gpu_vertical_split_debug(&split_active, &split_left_age, &split_right_age);
|
||||
GpuWsDebug ws;
|
||||
gpu_ws_get_debug(&ws);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||||
@@ -5295,6 +5297,7 @@ static void handle_gpu_state(int id, const char *json)
|
||||
"\"gp0_nop\":%llu,\"gp0_fill\":%llu,\"gp0_draw\":%llu,\"gp0_env\":%llu,\"gp0_copy\":%llu,"
|
||||
"\"draw_area\":[%u,%u,%u,%u],"
|
||||
"\"draw_offset\":[%d,%d],"
|
||||
"\"vertical_split\":{\"active\":%d,\"left_age\":%d,\"right_age\":%d},"
|
||||
"\"ws\":{\"configured\":%d,\"active\":%d,\"game_mode\":%d,"
|
||||
"\"present_native_43\":%d,\"x_margin\":%d,"
|
||||
"\"activation_margin\":%d,\"squash\":[%d,%d],"
|
||||
@@ -5323,6 +5326,7 @@ static void handle_gpu_state(int id, const char *json)
|
||||
(unsigned long long)copy,
|
||||
da.left, da.top, da.right, da.bottom,
|
||||
da.offset_x, da.offset_y,
|
||||
split_active, split_left_age, split_right_age,
|
||||
ws.configured, ws.active, ws.game_mode,
|
||||
ws.present_native_43, ws.x_margin, ws.activation_margin,
|
||||
ws.xnum, ws.xden,
|
||||
|
||||
+109
-1
@@ -87,6 +87,7 @@ static uint32_t ws_anchor_addr = 0; /* scratchpad addr of anchor SXY */
|
||||
static int ws_hud_sprt = 0; /* edge-anchor untagged HUD SPRTs */
|
||||
static int ws_auto_ui_squash;
|
||||
static int ws_auto_ui_dense;
|
||||
static int ws_active(void);
|
||||
static uint64_t ws_auto_ui_candidate_count;
|
||||
static uint64_t ws_auto_ui_transform_count;
|
||||
#define WS_UI_PREPASS_MAX 2048u
|
||||
@@ -167,6 +168,7 @@ static int ws_gte_game_mode_cfg = 0;
|
||||
static uint32_t ws_gte_frame = (uint32_t)-1;
|
||||
static uint32_t ws_gte_count = 0;
|
||||
static uint32_t ws_last_gte_stamp = (uint32_t)-1000;
|
||||
static int ws_precise_nclip_cfg = 0;
|
||||
#define WS_GAMEPLAY_STATE_VALUES_MAX 16
|
||||
static uint32_t ws_gameplay_state_addr = 0;
|
||||
static uint32_t ws_gameplay_state_values[WS_GAMEPLAY_STATE_VALUES_MAX];
|
||||
@@ -181,6 +183,8 @@ static int ws_gameplay_state_value_count = 0;
|
||||
* this many consecutive frames (save/options/memory-card) — reverts to 4:3. */
|
||||
#define WS_GTE_GAME_MODE_HYSTERESIS 45u
|
||||
void gpu_ws_set_gte_game_mode(int on) { ws_gte_game_mode_cfg = on ? 1 : 0; }
|
||||
void gpu_ws_set_precise_nclip(int on) { ws_precise_nclip_cfg = on ? 1 : 0; }
|
||||
int gpu_ws_precise_nclip_enabled(void) { return ws_precise_nclip_cfg && ws_active(); }
|
||||
void gpu_ws_set_gameplay_state_gate(uint32_t addr,
|
||||
const uint32_t *values, int nvalues) {
|
||||
if (nvalues < 0) nvalues = 0;
|
||||
@@ -2166,6 +2170,20 @@ static uint32_t texture_disable; /* bit 15 */
|
||||
static uint32_t draw_area_left, draw_area_top;
|
||||
static uint32_t draw_area_right, draw_area_bottom;
|
||||
|
||||
typedef struct GpuVerticalSplitTrace {
|
||||
uint8_t left_seen;
|
||||
uint8_t right_seen;
|
||||
uint16_t display_w;
|
||||
uint16_t display_h;
|
||||
} GpuVerticalSplitTrace;
|
||||
|
||||
static GpuVerticalSplitTrace split_trace_this;
|
||||
static GpuVerticalSplitTrace split_trace_last;
|
||||
static uint8_t split_recent_left_age = 255;
|
||||
static uint8_t split_recent_right_age = 255;
|
||||
static uint16_t split_recent_display_w = 0;
|
||||
static uint16_t split_recent_display_h = 0;
|
||||
|
||||
/* Draw offset (set by GP0(E5h)) */
|
||||
static int32_t draw_offset_x, draw_offset_y;
|
||||
/* Instrumentation: per-vblank range/count of GP0(E5) draw-offset-Y sets. If a
|
||||
@@ -2232,6 +2250,59 @@ static int ws_is_fb_base(uint32_t bx) {
|
||||
for (int i = 0; i < ws_fb_n; i++) if (ws_fb_base[i] == bx) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void split_trace_reset(GpuVerticalSplitTrace *trace) {
|
||||
memset(trace, 0, sizeof(*trace));
|
||||
}
|
||||
|
||||
static void split_trace_note_draw_area(void) {
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
if (di.disabled || di.depth24 || di.width < 256 || di.height < 128)
|
||||
return;
|
||||
if ((di.width & 1u) != 0)
|
||||
return;
|
||||
if (di.display_x + di.width > 1024u || di.display_y + di.height > 512u)
|
||||
return;
|
||||
if (draw_area_right <= draw_area_left || draw_area_bottom <= draw_area_top)
|
||||
return;
|
||||
|
||||
const int disp_l = (int)di.display_x;
|
||||
const int disp_r = (int)(di.display_x + di.width - 1u);
|
||||
const int mid = disp_l + (int)(di.width / 2u);
|
||||
const int tol = (int)di.width / 32 > 8 ? (int)di.width / 32 : 8;
|
||||
|
||||
const int area_l = (int)draw_area_left;
|
||||
const int area_r = (int)draw_area_right;
|
||||
|
||||
if (split_trace_this.display_w != 0 &&
|
||||
(split_trace_this.display_w != (uint16_t)di.width ||
|
||||
split_trace_this.display_h != (uint16_t)di.height)) {
|
||||
split_trace_reset(&split_trace_this);
|
||||
}
|
||||
split_trace_this.display_w = (uint16_t)di.width;
|
||||
split_trace_this.display_h = (uint16_t)di.height;
|
||||
|
||||
if (area_l <= disp_l + tol &&
|
||||
area_r >= mid - 1 - tol && area_r <= mid - 1 + tol) {
|
||||
split_trace_this.left_seen = 1;
|
||||
}
|
||||
if (area_l >= mid - tol && area_l <= mid + tol &&
|
||||
area_r >= disp_r - tol) {
|
||||
split_trace_this.right_seen = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int gpu_last_frame_vertical_split_screen(void) {
|
||||
return split_recent_left_age <= 8 && split_recent_right_age <= 8;
|
||||
}
|
||||
|
||||
void gpu_vertical_split_debug(int *active, int *left_age, int *right_age) {
|
||||
if (active) *active = gpu_last_frame_vertical_split_screen();
|
||||
if (left_age) *left_age = split_recent_left_age;
|
||||
if (right_age) *right_age = split_recent_right_age;
|
||||
}
|
||||
|
||||
/* Point the renderer's wide mirror at the current back buffer (draw_area_left)
|
||||
* when native-wide is active and that buffer is a known display buffer; else
|
||||
* disable mirroring for this draw. Called when the draw env changes. */
|
||||
@@ -2363,6 +2434,12 @@ static void gpu_reset_state(int clear_vram) {
|
||||
draw_area_top = 0;
|
||||
draw_area_right = 0;
|
||||
draw_area_bottom = 0;
|
||||
split_trace_reset(&split_trace_this);
|
||||
split_trace_reset(&split_trace_last);
|
||||
split_recent_left_age = 255;
|
||||
split_recent_right_age = 255;
|
||||
split_recent_display_w = 0;
|
||||
split_recent_display_h = 0;
|
||||
draw_offset_x = 0;
|
||||
draw_offset_y = 0;
|
||||
texture_window_value = 0;
|
||||
@@ -2655,6 +2732,26 @@ void gpu_vblank_tick(void) {
|
||||
g_doff_cnt_last = g_doff_cnt_this;
|
||||
}
|
||||
g_doff_min_this = 0x7fffffff; g_doff_max_this = -0x7fffffff; g_doff_cnt_this = 0;
|
||||
split_trace_last = split_trace_this;
|
||||
if (split_trace_this.display_w != 0 &&
|
||||
(split_recent_display_w != split_trace_this.display_w ||
|
||||
split_recent_display_h != split_trace_this.display_h)) {
|
||||
split_recent_left_age = 255;
|
||||
split_recent_right_age = 255;
|
||||
split_recent_display_w = split_trace_this.display_w;
|
||||
split_recent_display_h = split_trace_this.display_h;
|
||||
}
|
||||
if (split_trace_this.left_seen) {
|
||||
split_recent_left_age = 0;
|
||||
} else if (split_recent_left_age < 255) {
|
||||
split_recent_left_age++;
|
||||
}
|
||||
if (split_trace_this.right_seen) {
|
||||
split_recent_right_age = 0;
|
||||
} else if (split_recent_right_age < 255) {
|
||||
split_recent_right_age++;
|
||||
}
|
||||
split_trace_reset(&split_trace_this);
|
||||
gpustat_poll_count = 0;
|
||||
/* Trusted package-selected plugins run on guest VBlank, independent of
|
||||
* host presentation, pacing, turbo, or skipped frames. */
|
||||
@@ -3916,6 +4013,7 @@ static void gp0_exec_draw_area_tl(void) {
|
||||
uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu;
|
||||
draw_area_left = param & 0x3FF;
|
||||
draw_area_top = (param >> 10) & 0x3FF;
|
||||
split_trace_note_draw_area();
|
||||
gr_set_draw_area((int)draw_area_left, (int)draw_area_top,
|
||||
(int)draw_area_right, (int)draw_area_bottom);
|
||||
ws_nw_sync_target(); /* back buffer (draw_area_left) → wide mirror surface */
|
||||
@@ -3928,6 +4026,7 @@ static void gp0_exec_draw_area_br(void) {
|
||||
uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu;
|
||||
draw_area_right = param & 0x3FF;
|
||||
draw_area_bottom = (param >> 10) & 0x3FF;
|
||||
split_trace_note_draw_area();
|
||||
gr_set_draw_area((int)draw_area_left, (int)draw_area_top,
|
||||
(int)draw_area_right, (int)draw_area_bottom);
|
||||
ws_nw_sync_target(); /* back buffer (draw_area_left) → wide mirror surface */
|
||||
@@ -4352,7 +4451,16 @@ void gpu_ws_prepass_linked_list(uint32_t start_addr) {
|
||||
ws_ui_prepass[out++] = ws_ui_prepass[i];
|
||||
}
|
||||
ws_ui_prepass_count = out;
|
||||
ws_auto_ui_dense = ws_ui_prepass_count >= 32u;
|
||||
/*
|
||||
* A high final-layer primitive count is a good "dense 2D menu" signal for
|
||||
* titles without an explicit gameplay detector, where grouping everything
|
||||
* around the centre avoids tearing text grids apart. For GTE-gated 3D
|
||||
* titles, though, reaching this path already means a gameplay frame is
|
||||
* being stretched. WipEout 3's race HUD is dense enough to trip the old
|
||||
* 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;
|
||||
if (ws_ui_prepass_count == 0) return;
|
||||
|
||||
WsUiGroupItem groups[WS_UI_PREPASS_MAX];
|
||||
|
||||
+17
-6
@@ -8,6 +8,7 @@
|
||||
#include <cstring>
|
||||
|
||||
extern "C" uint32_t psx_read_word(uint32_t addr);
|
||||
extern "C" int gpu_ws_precise_nclip_enabled(void);
|
||||
|
||||
namespace PSXRecomp {
|
||||
namespace GTE {
|
||||
@@ -922,12 +923,22 @@ void gte_rtpt(GTEState* gte, uint32_t instr) {
|
||||
// ---------------------------------------------------------------------------
|
||||
void gte_nclip(GTEState* gte, uint32_t instr) {
|
||||
gte->FLAG = 0;
|
||||
int16_t sx0 = static_cast<int16_t>(gte->SXY[0] & 0xFFFF);
|
||||
int16_t sy0 = static_cast<int16_t>(gte->SXY[0] >> 16);
|
||||
int16_t sx1 = static_cast<int16_t>(gte->SXY[1] & 0xFFFF);
|
||||
int16_t sy1 = static_cast<int16_t>(gte->SXY[1] >> 16);
|
||||
int16_t sx2 = static_cast<int16_t>(gte->SXY[2] & 0xFFFF);
|
||||
int16_t sy2 = static_cast<int16_t>(gte->SXY[2] >> 16);
|
||||
int32_t sx0 = static_cast<int16_t>(gte->SXY[0] & 0xFFFF);
|
||||
int32_t sy0 = static_cast<int16_t>(gte->SXY[0] >> 16);
|
||||
int32_t sx1 = static_cast<int16_t>(gte->SXY[1] & 0xFFFF);
|
||||
int32_t sy1 = static_cast<int16_t>(gte->SXY[1] >> 16);
|
||||
int32_t sx2 = static_cast<int16_t>(gte->SXY[2] & 0xFFFF);
|
||||
int32_t sy2 = static_cast<int16_t>(gte->SXY[2] >> 16);
|
||||
if (gpu_ws_precise_nclip_enabled() &&
|
||||
s_precise_sxy[0].valid && s_precise_sxy[1].valid &&
|
||||
s_precise_sxy[2].valid) {
|
||||
sx0 = s_precise_sxy[0].x16 >> 16;
|
||||
sy0 = s_precise_sxy[0].y16 >> 16;
|
||||
sx1 = s_precise_sxy[1].x16 >> 16;
|
||||
sy1 = s_precise_sxy[1].y16 >> 16;
|
||||
sx2 = s_precise_sxy[2].x16 >> 16;
|
||||
sy2 = s_precise_sxy[2].y16 >> 16;
|
||||
}
|
||||
int64_t mac0 = (int64_t)sx0 * (sy1 - sy2) +
|
||||
(int64_t)sx1 * (sy2 - sy0) +
|
||||
(int64_t)sx2 * (sy0 - sy1);
|
||||
|
||||
+179
-28
@@ -1215,6 +1215,13 @@ static int g_video_aspect_den = 3;
|
||||
static bool g_ws_adaptive_view = false;
|
||||
static int g_ws_adaptive_max_num = 16;
|
||||
static int g_ws_adaptive_max_den = 9;
|
||||
/* game.toml [netplay] local_viewport = "vertical_split": during real netplay,
|
||||
* present only this peer's native split-screen half and stretch it to the
|
||||
* window. Presentation-only; the guest still renders the original framebuffer. */
|
||||
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 */
|
||||
|
||||
extern "C" int psx_mod_set_fixed_display_aspect(
|
||||
uint32_t numerator, uint32_t denominator) {
|
||||
@@ -1417,6 +1424,7 @@ static bool g_ws_hud_sprt = false;
|
||||
/* Runtime-only transition cleanup; kept out of gpu.h because generated game
|
||||
* units include that ABI header and do not need this frontend-only setter. */
|
||||
extern "C" void gpu_ws_set_clear_reveal(int on);
|
||||
extern "C" void gpu_ws_set_precise_nclip(int on);
|
||||
extern "C" void gpu_ws_set_nw_textured_edges(int on, int scale_pct);
|
||||
extern "C" void gpu_ws_set_signed_x_bound_sites(const uint32_t*, const uint32_t*, int);
|
||||
/* Widescreen engages at game entry (fntrace_is_game_started): the BIOS boot
|
||||
@@ -1452,6 +1460,81 @@ static int aspect_gcd(int a, int b) {
|
||||
return a > 0 ? a : 1;
|
||||
}
|
||||
|
||||
static int64_t aspect_gcd64(int64_t a, int64_t b) {
|
||||
if (a < 0) a = -a;
|
||||
if (b < 0) b = -b;
|
||||
while (b) { int64_t t = a % b; a = b; b = t; }
|
||||
return a > 0 ? a : 1;
|
||||
}
|
||||
|
||||
static void netplay_local_viewport_projection_aspect(
|
||||
int present_num, int present_den, int* proj_num, int* proj_den) {
|
||||
if (!proj_num || !proj_den) return;
|
||||
*proj_num = present_num;
|
||||
*proj_den = present_den;
|
||||
|
||||
if (g_netplay_local_viewport != 1 ||
|
||||
!psx_netplay_active() ||
|
||||
!gpu_last_frame_vertical_split_screen() ||
|
||||
present_num * 3 <= present_den * 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
if (di.disabled || di.depth24 || di.width < 2 || di.height == 0)
|
||||
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 = (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;
|
||||
if (n <= 0 || d <= 0) return;
|
||||
int64_t gcd = aspect_gcd64(n, d);
|
||||
n /= gcd;
|
||||
d /= gcd;
|
||||
if (n <= 0 || d <= 0 || n > INT32_MAX || d > INT32_MAX)
|
||||
return;
|
||||
|
||||
*proj_num = (int)n;
|
||||
*proj_den = (int)d;
|
||||
}
|
||||
|
||||
static int g_ws_projection_num = 4;
|
||||
static int g_ws_projection_den = 3;
|
||||
static int g_ws_projection_mode = -1;
|
||||
static void refresh_widescreen_projection() {
|
||||
if (!g_ws_engaged) return;
|
||||
|
||||
const bool wide = g_video_aspect_num * 3 != g_video_aspect_den * 4;
|
||||
const int mode = wide ? (g_ws_native_wide ? 2 : 1) : 0;
|
||||
int proj_num = g_video_aspect_num;
|
||||
int proj_den = g_video_aspect_den;
|
||||
if (mode == 1) {
|
||||
netplay_local_viewport_projection_aspect(
|
||||
g_video_aspect_num, g_video_aspect_den, &proj_num, &proj_den);
|
||||
}
|
||||
|
||||
if (mode == g_ws_projection_mode &&
|
||||
proj_num == g_ws_projection_num &&
|
||||
proj_den == g_ws_projection_den) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_ws_projection_mode = mode;
|
||||
g_ws_projection_num = proj_num;
|
||||
g_ws_projection_den = proj_den;
|
||||
gte_set_display_aspect(mode == 1 ? proj_num : 4,
|
||||
mode == 1 ? proj_den : 3);
|
||||
gpu_ws_configure(proj_num, proj_den, g_ws_anchor_addr,
|
||||
g_ws_hud_sprt ? 1 : 0, mode);
|
||||
}
|
||||
|
||||
/* Follow the host window without feeding its absolute pixel size into guest
|
||||
* rendering. Only the ratio matters: gpu_ws_configure derives the PSX-native
|
||||
* sidecar width from it, just as the fixed 16:9/21:9 modes do. */
|
||||
@@ -1484,14 +1567,7 @@ static void update_adaptive_widescreen() {
|
||||
SDL_RenderSetLogicalSize(sdl_renderer, g_logical_w, 480 * g_video_scale);
|
||||
}
|
||||
|
||||
if (g_ws_engaged) {
|
||||
const bool wide = num * 3 != den * 4;
|
||||
const int mode = wide ? (g_ws_native_wide ? 2 : 1) : 0;
|
||||
gte_set_display_aspect(mode == 1 ? num : 4,
|
||||
mode == 1 ? den : 3);
|
||||
gpu_ws_configure(num, den, g_ws_anchor_addr,
|
||||
g_ws_hud_sprt ? 1 : 0, mode);
|
||||
}
|
||||
refresh_widescreen_projection();
|
||||
}
|
||||
|
||||
/* SDL GL attributes are global inputs to the next context creation. Set the
|
||||
@@ -1509,13 +1585,8 @@ static void configure_core_gl_context_attributes() {
|
||||
* Re-engages with the chosen mode in place if widescreen is already running. */
|
||||
extern "C" void psx_ws_set_native_wide(int on) {
|
||||
g_ws_native_wide = on ? 1 : 0;
|
||||
if (g_ws_engaged && g_video_aspect_num * 3 != g_video_aspect_den * 4) {
|
||||
int mode = g_ws_native_wide ? 2 : 1;
|
||||
gte_set_display_aspect(mode == 1 ? g_video_aspect_num : 4,
|
||||
mode == 1 ? g_video_aspect_den : 3);
|
||||
gpu_ws_configure(g_video_aspect_num, g_video_aspect_den,
|
||||
g_ws_anchor_addr, g_ws_hud_sprt ? 1 : 0, mode);
|
||||
}
|
||||
g_ws_projection_mode = -1;
|
||||
refresh_widescreen_projection();
|
||||
}
|
||||
extern "C" int psx_ws_get_native_wide(void) { return g_ws_native_wide; }
|
||||
|
||||
@@ -2404,6 +2475,29 @@ static void shutdown_runtime(void);
|
||||
static int g_netplay_from_lobby = 0;
|
||||
static int g_netplay_vsync_forced_off = 0;
|
||||
|
||||
static void apply_netplay_local_viewport_aspect(bool netplay_enabled) {
|
||||
if (!netplay_enabled ||
|
||||
g_netplay_local_viewport != 1 ||
|
||||
g_netplay_local_viewport_aspect == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (g_netplay_local_viewport_aspect) {
|
||||
case 1:
|
||||
(void)psx_mod_set_fixed_display_aspect(16u, 9u);
|
||||
break;
|
||||
case 2:
|
||||
(void)psx_mod_set_fixed_display_aspect(21u, 9u);
|
||||
break;
|
||||
case 3:
|
||||
(void)psx_mod_set_fixed_display_aspect(16u, 9u);
|
||||
(void)psx_mod_set_adaptive_display_aspect(21u, 9u);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Host-only: lockstep already couples peers. Driver vsync on top of the
|
||||
* wall-clock pacer double-blocks the vblank callback (present is before the
|
||||
* guest resumes), which shows up as MotK FMV ~30–40 FPS in netplay vs ~50+
|
||||
@@ -4633,6 +4727,36 @@ static void netplay_note_present(void) {
|
||||
s_present_last_ms = now ? now : 1ull;
|
||||
}
|
||||
|
||||
static int netplay_local_viewport_slot(void) {
|
||||
if (g_netplay_local_viewport != 1 || !psx_netplay_active())
|
||||
return -1;
|
||||
if (!gpu_last_frame_vertical_split_screen())
|
||||
return -1;
|
||||
int slot = psx_netplay_local_slot();
|
||||
return (slot == 0 || slot == 1) ? slot : -1;
|
||||
}
|
||||
|
||||
static int crop_present_to_netplay_local_viewport(uint32_t* pixels,
|
||||
int* width,
|
||||
int height) {
|
||||
if (!pixels || !width || *width < 2 || height <= 0)
|
||||
return 0;
|
||||
const int slot = netplay_local_viewport_slot();
|
||||
if (slot < 0)
|
||||
return 0;
|
||||
|
||||
const int src_w = *width;
|
||||
const int crop_w = src_w / 2;
|
||||
const int crop_x = slot == 0 ? 0 : (src_w - crop_w);
|
||||
for (int y = 0; y < height; ++y) {
|
||||
uint32_t* dst = pixels + (size_t)y * (size_t)crop_w;
|
||||
uint32_t* src = pixels + (size_t)y * (size_t)src_w + crop_x;
|
||||
memmove(dst, src, (size_t)crop_w * sizeof(uint32_t));
|
||||
}
|
||||
*width = crop_w;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void netplay_present_gap_stats(uint32_t *p95_out, uint32_t *max_out) {
|
||||
unsigned n = s_present_gaps_n;
|
||||
unsigned idx;
|
||||
@@ -6243,14 +6367,11 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
extern int fntrace_is_game_started(void);
|
||||
if (fntrace_is_game_started()) {
|
||||
g_ws_engaged = true;
|
||||
int mode = g_ws_native_wide ? 2 : 1;
|
||||
/* Native-wide: GTE drawn un-squashed — feed it the 4:3 ratio
|
||||
* (identity squash). Squash mode: feed the real wide aspect. */
|
||||
gte_set_display_aspect(mode == 1 ? g_video_aspect_num : 4,
|
||||
mode == 1 ? g_video_aspect_den : 3);
|
||||
gpu_ws_configure(g_video_aspect_num, g_video_aspect_den,
|
||||
g_ws_anchor_addr, g_ws_hud_sprt ? 1 : 0, mode);
|
||||
g_ws_projection_mode = -1;
|
||||
refresh_widescreen_projection();
|
||||
}
|
||||
} else {
|
||||
refresh_widescreen_projection();
|
||||
}
|
||||
|
||||
/* Rollback resim (§33/§47): short catch-up keeps hold-last; long catch-up
|
||||
@@ -6359,6 +6480,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
if (g_gl_active)
|
||||
gl_renderer_set_interpolation_suspended(
|
||||
fmv_frame || mdec_recently_active(2));
|
||||
const bool local_viewport_crop = netplay_local_viewport_slot() >= 0;
|
||||
bool local_viewport_crop_applied = false;
|
||||
|
||||
/* Canonical present width. Native-wide does NOT widen the canonical read
|
||||
* (that bled across adjacent framebuffers); it composites into a separate
|
||||
@@ -6369,7 +6492,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* wide compositor, present the wider surface (canonical width + EXTRA)
|
||||
* from the displayed buffer's surface. FMV/menu frames stay 4:3. */
|
||||
bool wide_present = (!fmv_frame && !di.depth24 && g_ws_engaged &&
|
||||
ws_native_wide_active() && gr_wide_supported());
|
||||
ws_native_wide_active() && gr_wide_supported() &&
|
||||
!local_viewport_crop);
|
||||
if (wide_present) present_w = w + (uint32_t)ws_nw_extra();
|
||||
|
||||
/* Native-wide invariant: canonical (320-wide) content is NEVER
|
||||
@@ -6395,7 +6519,8 @@ 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
|
||||
if (g_gl_active && g_gl_fbo_present && !di.depth24) {
|
||||
if (g_gl_active && g_gl_fbo_present && !di.depth24 &&
|
||||
!local_viewport_crop) {
|
||||
if (wide_present) {
|
||||
/* GPU-direct native-wide present: blit the displayed buffer's
|
||||
* wide FBO straight to the window (GPU-side, like the canonical
|
||||
@@ -6422,7 +6547,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* VRAM image (deterministic blit, no readback), mirroring the GL path;
|
||||
* 24-bit (FMV) frames go through the CPU present (Phase 3). The Vulkan
|
||||
* window has no SDL_Renderer, so we must never fall through below. */
|
||||
if (g_vk_active) {
|
||||
if (g_vk_active && !local_viewport_crop) {
|
||||
if (di.depth24) {
|
||||
/* 24-bit (FMV): packed RGB lives in the CPU mirror — do NOT
|
||||
* sync_cpu (FBO readback clobbers RGB888). Batch per-scanline
|
||||
@@ -6518,9 +6643,18 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
depth24_fix_trailing_margin(sdl_pixel_buf, present_w, h,
|
||||
di.display_x);
|
||||
|
||||
int present_px_w = (int)present_w * active_scale;
|
||||
int present_px_h = (int)h * active_scale;
|
||||
if (crop_present_to_netplay_local_viewport(sdl_pixel_buf,
|
||||
&present_px_w,
|
||||
present_px_h)) {
|
||||
pin_43 = false;
|
||||
local_viewport_crop_applied = true;
|
||||
}
|
||||
|
||||
smooth_60_present(sdl_pixel_buf,
|
||||
present_w * (uint32_t)active_scale,
|
||||
h * (uint32_t)active_scale,
|
||||
(uint32_t)present_px_w,
|
||||
(uint32_t)present_px_h,
|
||||
!g_gl_active && !g_vk_active && !di.depth24 && !fmv_frame);
|
||||
|
||||
/* Frame blending (CRT-persistence masker for 30fps double-buffered
|
||||
@@ -6545,7 +6679,9 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
!g_smooth_60fps.load(std::memory_order_acquire)) {
|
||||
static uint32_t prev_buf[640 * 512];
|
||||
static uint32_t prev_px = 0;
|
||||
const uint32_t npx = present_w * h;
|
||||
const uint32_t npx = local_viewport_crop_applied
|
||||
? (uint32_t)(present_px_w * present_px_h)
|
||||
: present_w * h;
|
||||
if (npx <= (uint32_t)(640 * 512)) {
|
||||
if (prev_px == npx) {
|
||||
for (uint32_t i = 0; i < npx; i++) {
|
||||
@@ -6573,6 +6709,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
#ifndef PSX_SDL_NO_RENDER
|
||||
int src_w = (int)present_w * active_scale;
|
||||
int src_h = (int)h * active_scale;
|
||||
if (netplay_local_viewport_slot() >= 0 && src_w >= 2)
|
||||
src_w /= 2;
|
||||
if (g_gl_active) {
|
||||
/* OpenGL present: upload the active display rect and draw a full-screen
|
||||
* quad. SDL_GL_SwapWindow handles vsync; the wall-clock pacer above
|
||||
@@ -10327,6 +10465,12 @@ int main(int argc, char** argv) {
|
||||
g_netplay_disc_expect.required_leadout_lba =
|
||||
gc.netplay_required_leadout_lba;
|
||||
g_netplay_disc_expect.required_disc_fp = gc.netplay_required_disc_fp;
|
||||
g_netplay_local_viewport =
|
||||
(gc.netplay_local_viewport == "vertical_split") ? 1 : 0;
|
||||
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 == "adaptive") ? 3 : 0;
|
||||
if (!gc.discs.empty()) resolved_disc = gc.discs.front();
|
||||
if (gc.runtime.has_memcard_dir) memcard_dir = gc.runtime.memcard_dir;
|
||||
if (gc.runtime.has_window_title) window_title = gc.runtime.window_title;
|
||||
@@ -10422,6 +10566,7 @@ int main(int argc, char** argv) {
|
||||
gc.ws_bg2d_packet_cap);
|
||||
/* [widescreen] gte_game_mode — 3D-title gameplay detector (Ape). */
|
||||
gpu_ws_set_gte_game_mode(gc.ws_gte_game_mode ? 1 : 0);
|
||||
gpu_ws_set_precise_nclip(gc.ws_precise_nclip ? 1 : 0);
|
||||
gpu_ws_set_gameplay_state_gate(
|
||||
gc.ws_gameplay_state_addr,
|
||||
gc.ws_gameplay_state_values.data(),
|
||||
@@ -10579,6 +10724,10 @@ int main(int argc, char** argv) {
|
||||
if (i >= 2) player_mode[i] = gc.runtime.default_p1_mode;
|
||||
}
|
||||
}
|
||||
if (gc.runtime.has_default_p1_device)
|
||||
player_device[0] = gc.runtime.default_p1_device;
|
||||
if (PSX_MAX_PLAYERS >= 2 && gc.runtime.has_default_p2_device)
|
||||
player_device[1] = gc.runtime.default_p2_device;
|
||||
for (int i = 0; i < PSX_MAX_PLAYERS; ++i)
|
||||
ctrl_locked_mode[i] = player_mode[i];
|
||||
ctrl_lock_mode = gc.runtime.controller_lock_mode;
|
||||
@@ -11909,6 +12058,7 @@ int main(int argc, char** argv) {
|
||||
g_turbo_loads_enabled = 0;
|
||||
g_frame_interpolation_blend = g_frame_interpolation_blend_default;
|
||||
mod_runtime_activate_plugins();
|
||||
apply_netplay_local_viewport_aspect(net_cfg.enabled);
|
||||
if (g_mod_controller_mode_override[0] >= 0)
|
||||
player_mode[0] = g_mod_controller_mode_override[0];
|
||||
if (g_mod_controller_mode_override[1] >= 0)
|
||||
@@ -13553,6 +13703,7 @@ soft_return_lobby:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
apply_netplay_local_viewport_aspect(net_cfg.enabled);
|
||||
std::printf("psxrecomp: rematch from lobby (netplay=%d)\n",
|
||||
net_cfg.enabled ? 1 : 0);
|
||||
std::fflush(stdout);
|
||||
|
||||
Reference in New Issue
Block a user