feat: add adaptive native widescreen

Add a resize-driven aspect option, keep native-wide culling and spawning hooks engaged across live aspect changes, and widen host GP0 draw-area pruning only for the active wide framebuffer.

Credit OpokXeno's independently discovered draw-area pruning fix documented in PR #73.

Co-authored-by: Opok <novelasevilliousesp@gmail.com>
This commit is contained in:
Matthew Stanley
2026-07-22 21:58:41 -07:00
parent 1a73e9826c
commit cece00296a
5 changed files with 212 additions and 24 deletions
+18
View File
@@ -70,8 +70,26 @@ clear_reveal = true # clear synthetic native-wide side margin
nw_left_hud_packet_lo = "0x000E3400" # optional targeted left-HUD packet range
nw_left_hud_packet_hi = "0x000E4100" # (half-open); avoids shifting 2D scenery.
offer_ultrawide = true # separate experimental 21:9 launcher row.
adaptive_view = true # expose live resize-driven aspect mode.
```
When `adaptive_view` is enabled by the game, the launcher's **Aspect ratio**
selector gains an **Adaptive** entry. The previously selected fixed aspect
still determines the initial window shape. Once the game window exists,
resizing it continuously
updates the GTE/native-wide projection, cull margins, wide render target, and
present aspect. The live ratio is clamped to 4:3 on the narrow side and to the
widest mode the game offers (`16:9`, or `21:9` with `offer_ultrawide`). BIOS,
FMV, menus, and other title-classified 2D frames retain their existing 4:3
pillarbox policy. The user choice persists as `[video] adaptive_view` in
`settings.toml`.
The native-wide draw-area early-out correction was independently identified
and contributed by **OpokXeno** in
[PR #73](https://github.com/mstan/psxrecomp/pull/73). The landed variant keeps
that mirror-space correction while restricting it to active framebuffer
targets, so offscreen texture targets retain the original host-side pruning.
Tomba's values are Ghidra-evidenced: `0x8005E08C` is the shared per-prim helper
all ~23 character render functions (RTPS cluster `0x800459E0`–`0x8004FB54`) call;
the RTPS preamble stores the anchor SXY to scratchpad `0x1F800070`.
+10
View File
@@ -889,6 +889,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
std::vector<WidescreenSignedBoundSite> ws_signed_x_bound_sites;
bool ws_offered = true;
bool vulkan_offered = false;
bool ws_adaptive_view = false;
bool ws_ultrawide_offered = false;
if (cfg.contains("video")) {
const toml::value& video = toml::find(cfg, "video");
@@ -1060,6 +1061,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
ws_offered = toml::find<bool>(ws, "offer");
if (ws.contains("offer_ultrawide"))
ws_ultrawide_offered = toml::find<bool>(ws, "offer_ultrawide");
if (ws.contains("adaptive_view"))
ws_adaptive_view = toml::find<bool>(ws, "adaptive_view");
}
// Optional [widescreen.cull] block — world-space draw-cull widening.
@@ -1279,6 +1282,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
/*ws_signed_x_bound_sites*/ ws_signed_x_bound_sites,
/*ws_offered*/ ws_offered,
/*vulkan_offered*/ vulkan_offered,
/*ws_adaptive_view*/ ws_adaptive_view,
/*ws_ultrawide_offered*/ ws_ultrawide_offered,
/*ws_bg2d_count_site*/ ws_bg2d_count_site,
/*ws_bg2d_startcol_site*/ ws_bg2d_startcol_site,
@@ -1438,6 +1442,10 @@ UserSettings load_user_settings(const fs::path& path) {
s.aspect_num = n; s.aspect_den = d; s.has_aspect_ratio = true;
}
});
if (v.contains("adaptive_view")) try_get([&]{
s.adaptive_view = toml::find<bool>(v, "adaptive_view");
s.has_adaptive_view = true;
});
}
if (doc.contains("audio")) {
const toml::value& a = toml::find(doc, "audio");
@@ -1600,6 +1608,8 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
f << "frame_interpolation_fps = " << s.frame_interpolation_fps << "\n";
if (s.has_aspect_ratio)
f << "aspect_ratio = \"" << s.aspect_num << ":" << s.aspect_den << "\"\n";
if (s.has_adaptive_view)
f << "adaptive_view = " << (s.adaptive_view ? "true" : "false") << "\n";
f << "\n[audio]\n";
if (s.has_spu_hq)
f << "spu_hq = " << (s.spu_hq ? "true" : "false") << "\n";
+7
View File
@@ -700,6 +700,12 @@ struct GameConfig {
bool ws_offered = true;
bool vulkan_offered = false;
// [widescreen] adaptive_view — let the user opt into a live, resize-driven
// aspect instead of selecting only fixed 4:3/16:9/21:9 modes. The fixed
// aspect remains the initial window shape; the live view is clamped to the
// widest aspect this title offers.
bool ws_adaptive_view = false;
// [widescreen] offer_ultrawide — expose a separate experimental 21:9
// launcher choice for titles that have explicitly tested it. Default off;
// ordinary widescreen offer remains the independent 16:9 choice.
@@ -810,6 +816,7 @@ struct UserSettings {
bool has_netplay_lobby_url = false; std::string netplay_lobby_url;
bool has_aspect_ratio = false; int aspect_num = 4; // display aspect W:H
int aspect_den = 3; // (4:3 = native)
bool has_adaptive_view = false; bool adaptive_view = false;
// [audio]
bool has_spu_hq = false; bool spu_hq = false;
// [bios] / [disc] / [memcard]
+69 -11
View File
@@ -82,6 +82,7 @@ static int ws_hud_sprt = 0; /* edge-anchor untagged HUD SPRTs *
* in native-wide mode. */
static int ws_mode = 0;
static int ws_cfg_num = 4, ws_cfg_den = 3;
static void ws_nw_sync_target(void);
#define WS_TAG_BUCKETS 4096 /* power of two */
#define WS_TAG_PROBES 8
@@ -279,13 +280,22 @@ static int ws_active(void) { return ws_configured() && !gpu_ws_present_native_43
int ws_native_wide_active(void) {
return ws_mode == 2 && !gpu_ws_present_native_43();
}
/* Cull/spawn setup often runs while a scene is loading, before the first GTE
* frame can classify it as gameplay. Keep that pre-render setup aware of the
* configured native-wide viewport; presentation itself remains gated by
* ws_native_wide_active(). */
static int ws_native_wide_configured(void) {
return ws_mode == 2 && ws_cfg_num * 3 > ws_cfg_den * 4;
}
static int ws_nw_configured_offset(void) {
if (!ws_native_wide_configured()) return 0;
int numr = 3 * ws_cfg_num - 4 * ws_cfg_den;
int w = (int)ws_disp_w();
return (w * numr + 4 * ws_cfg_den) / (8 * ws_cfg_den);
}
static int ws_nw_offset(void) {
if (!ws_native_wide_active()) return 0;
int numr = 3 * ws_cfg_num - 4 * ws_cfg_den; /* > 0 for aspects wider than 4:3 */
if (numr <= 0) return 0;
int w = (int)ws_disp_w();
int v = (w * numr + 4 * ws_cfg_den) / (8 * ws_cfg_den); /* round-to-nearest */
return v;
return ws_nw_configured_offset();
}
int ws_nw_extra(void) { return 2 * ws_nw_offset(); }
@@ -446,7 +456,12 @@ int psx_ws_x_margin(void) {
* that previously fell outside the 4:3 cull window; the wide compositor then
* rasterizes it into the revealed margins. Same recompiler emit sites as the
* squash path ([widescreen.cull]); 0 at 4:3 so the cull stays byte-identical. */
if (ws_native_wide_active()) return ws_nw_offset() + ws_cull_guard_pixels;
/* Unlike rendering/presentation, do not wait for game-mode detection here.
* Tomba 2 builds its terrain-cell and actor spawn lists during scene load;
* returning zero until the first 3D frame permanently bakes a 4:3 frustum
* into those lists. */
if (ws_native_wide_configured())
return ws_nw_configured_offset() + ws_cull_guard_pixels;
if (!ws_active()) return 0;
return (160 * (ws_xden - ws_xnum) + ws_xnum / 2) / ws_xnum;
}
@@ -1261,6 +1276,10 @@ void gpu_ws_configure(int aspect_num, int aspect_den,
}
ws_anchor_addr = sprite_anchor_addr;
ws_hud_sprt = hud_sprt_squash;
/* Adaptive view can change mode/extent without the guest reissuing E3/E4
* immediately. Keep the active mirror target and its scissor in lockstep
* with the new live aspect instead of waiting for another draw-env packet. */
ws_nw_sync_target();
}
/* Called from generated code at the entry of each [widescreen]
@@ -2349,9 +2368,44 @@ static void raster_pixel(int32_t x, int32_t y, uint16_t color) {
* clips those for free; our GL path was building two triangles per clipped
* prim (gpu_share ~0.9, host FPS ~5–10). Skip the host rasterizer when the
* post-offset bbox cannot touch the draw area. Side effects that must still
* run (texpage latch, oversize reject) happen before these checks. */
* run (texpage latch, oversize reject) happen before these checks.
*
* Native-wide is intentionally different in X: the mirror renderer translates
* canonical framebuffer coordinates by the live reveal offset and scissors to
* the FULL wide surface, not GP0(E3/E4)'s 4:3 X range. Commit 31015ce originally
* compared every primitive only with the guest draw area here, rejecting the
* margin geometry before the mirror renderer could see it. Match the mirror's
* exact X extent while its framebuffer target is active; Y remains the guest
* draw area because native-wide does not extend vertically. At 4:3, in FMV/
* menus, in squash mode, and for offscreen texture targets, margin is zero and
* this remains the original fast reject byte-for-byte.
*
* OpokXeno independently identified the same host-side regression on Xenogears
* and contributed the original generalized fix in psxrecomp PR #73:
* https://github.com/mstan/psxrecomp/pull/73
* Keep that credit with this guarded framebuffer-target variant. */
static inline int32_t draw_area_wide_x_margin(void) {
if (!ws_native_wide_active() || !ws_is_fb_base(draw_area_left)) return 0;
return (int32_t)ws_nw_offset();
}
static inline void draw_area_host_x_bounds(int32_t *left, int32_t *right) {
int32_t margin = draw_area_wide_x_margin();
if (margin > 0) {
/* ws_nw_sync_target uses draw_area_left as the framebuffer base and
* configures a surface ws_disp_w()+2*margin pixels wide. */
*left = (int32_t)draw_area_left - margin;
*right = (int32_t)draw_area_left + (int32_t)ws_disp_w() + margin - 1;
} else {
*left = (int32_t)draw_area_left;
*right = (int32_t)draw_area_right;
}
}
static inline int draw_area_out_point(int32_t x, int32_t y) {
return x < (int32_t)draw_area_left || x > (int32_t)draw_area_right
int32_t left, right;
draw_area_host_x_bounds(&left, &right);
return x < left || x > right
|| y < (int32_t)draw_area_top || y > (int32_t)draw_area_bottom;
}
@@ -2363,14 +2417,18 @@ static inline int draw_area_out_bbox(const int32_t *vx, const int32_t *vy, int n
if (vy[i] < miny) miny = vy[i];
if (vy[i] > maxy) maxy = vy[i];
}
return maxx < (int32_t)draw_area_left || minx > (int32_t)draw_area_right
int32_t left, right;
draw_area_host_x_bounds(&left, &right);
return maxx < left || minx > right
|| maxy < (int32_t)draw_area_top || miny > (int32_t)draw_area_bottom;
}
static inline int draw_area_out_rect(int32_t x, int32_t y, int w, int h) {
if (w <= 0 || h <= 0) return 1;
return (x + w - 1) < (int32_t)draw_area_left
|| x > (int32_t)draw_area_right
int32_t left, right;
draw_area_host_x_bounds(&left, &right);
return (x + w - 1) < left
|| x > right
|| (y + h - 1) < (int32_t)draw_area_top
|| y > (int32_t)draw_area_bottom;
}
+108 -13
View File
@@ -556,6 +556,12 @@ extern "C" void debug_get_fmv_config(int *auto_skip, uint32_t *total_table,
* in config_loader.h). */
static int g_video_aspect_num = 4;
static int g_video_aspect_den = 3;
/* Resize-driven widescreen. The user's fixed aspect is still used to shape the
* initial window; after the game window exists these values follow its live
* aspect, clamped to 4:3..the widest mode offered by the title. */
static bool g_ws_adaptive_view = false;
static int g_ws_adaptive_max_num = 16;
static int g_ws_adaptive_max_den = 9;
/* [widescreen] per-game hooks (see config_loader.h): anchor scratch addr for
* tagged sprite prims + HUD SPRT center-squash. Inert at 0/false. */
static uint32_t g_ws_anchor_addr = 0;
@@ -593,6 +599,53 @@ static void clamp_window_aspect(int* w, int* h, int num, int den) {
*h = width * den / num;
}
static int aspect_gcd(int a, int b) {
while (b) { int t = a % b; a = b; b = t; }
return a > 0 ? a : 1;
}
/* 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. */
static void update_adaptive_widescreen() {
if (!g_ws_adaptive_view || !sdl_window) return;
int width = 0, height = 0;
SDL_GetWindowSize(sdl_window, &width, &height);
if (width <= 0 || height <= 0) return;
int num = width, den = height;
if ((int64_t)width * 3 <= (int64_t)height * 4) {
num = 4; den = 3;
} else if ((int64_t)width * g_ws_adaptive_max_den >=
(int64_t)height * g_ws_adaptive_max_num) {
num = g_ws_adaptive_max_num;
den = g_ws_adaptive_max_den;
} else {
int divisor = aspect_gcd(num, den);
num /= divisor;
den /= divisor;
}
if (num == g_video_aspect_num && den == g_video_aspect_den) return;
g_video_aspect_num = num;
g_video_aspect_den = den;
gl_renderer_set_display_aspect(num, den);
if (sdl_renderer) {
g_logical_w = 480 * num * g_video_scale / den;
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);
}
}
/* SDL GL attributes are global inputs to the next context creation. Set the
* runtime requirements immediately before every GL window, because launcher
* teardown resets them and macOS otherwise supplies a legacy 2.1 context. */
@@ -3437,12 +3490,17 @@ static void sdl_vblank_present(void) {
/* Mod hooks. Run after all normal input sampling. */
mod_call_frame_hooks();
/* Resize-driven mode updates the pending aspect during BIOS boot too, but
* the actual wide compositor remains disengaged until game entry. */
update_adaptive_widescreen();
/* Engage widescreen at game entry: BIOS boot stays authentic 4:3. */
if (!g_ws_engaged) {
extern int fntrace_is_game_started(void);
if (fntrace_is_game_started()) {
g_ws_engaged = true;
int mode = g_ws_native_wide ? 2 : 1;
const bool wide = g_video_aspect_num * 3 != g_video_aspect_den * 4;
int mode = wide ? (g_ws_native_wide ? 2 : 1) : 0;
/* 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,
@@ -4522,6 +4580,7 @@ int main(int argc, char** argv) {
int ctrl_locked_p2_mode = PSXRecompV4::PAD_MODE_HYBRID;
bool ws_offered = true; /* game.toml [widescreen] offer; false hides the launcher toggle + clamps 4:3 */
bool ws_ultrawide_offered = false;
bool ws_adaptive_view_supported = false;
bool vulkan_offered = false; /* game.toml [video] offer_vulkan; developer opt-in for launcher visibility */
int resolved_deadzone = -1; /* <0 => keep input.ini/runtime default (12000) */
/* Localization: the effective language (game.toml default -> settings.toml ->
@@ -4695,6 +4754,7 @@ int main(int argc, char** argv) {
gc.ws_cull_h_imms.data(), (int)gc.ws_cull_h_imms.size());
ws_offered = gc.ws_offered;
ws_ultrawide_offered = gc.ws_ultrawide_offered;
ws_adaptive_view_supported = gc.ws_adaptive_view;
vulkan_offered = gc.vulkan_offered;
/* Register the [widescreen.backdrop] store PCs so the dirty-RAM
* interpreter applies the backdrop screenX squash on the interp
@@ -5000,6 +5060,7 @@ int main(int argc, char** argv) {
g_video_aspect_num = us.aspect_num;
g_video_aspect_den = us.aspect_den;
}
if (us.has_adaptive_view) g_ws_adaptive_view = us.adaptive_view;
if (us.has_spu_hq) g_audio_spu_hq = us.spu_hq;
if (us.has_bios_path && !bios_from_cli) {
settings_bios_storage = us.bios_path.string();
@@ -5058,6 +5119,9 @@ int main(int argc, char** argv) {
g_video_aspect_num = ws_offered ? 16 : 4;
g_video_aspect_den = ws_offered ? 9 : 3;
}
if (!ws_adaptive_view_supported) g_ws_adaptive_view = false;
g_ws_adaptive_max_num = ws_ultrawide_offered ? 21 : 16;
g_ws_adaptive_max_den = 9;
/* Latency knobs: env overrides win over config (for A/B measurement).
* PSX_LOW_LATENCY_INPUT=0/1 ; PSX_VSYNC=1(vsync)/0(immediate)/-1(adaptive);
@@ -5173,6 +5237,7 @@ int main(int argc, char** argv) {
seed.has_frame_interpolation_fps = true;
seed.aspect_num = g_video_aspect_num;
seed.aspect_den = g_video_aspect_den; seed.has_aspect_ratio = true;
seed.adaptive_view = g_ws_adaptive_view; seed.has_adaptive_view = true;
seed.spu_hq = g_audio_spu_hq; seed.has_spu_hq = true;
seed.skip_launcher = skip_launcher_setting; seed.has_skip_launcher = true;
if (has_netplay_player_name) {
@@ -5231,9 +5296,13 @@ int main(int argc, char** argv) {
has_netplay_player_name ? netplay_player_name.c_str() : "");
ls.pad_mode[0] = seed.p1_mode;
ls.pad_mode[1] = seed.p2_mode;
/* aspect_index: 0 = 4:3, 1 = 16:9, 2 = 21:9 (see RecompLauncherCSettings). */
ls.aspect_index = (seed.aspect_num * 9 == seed.aspect_den * 21) ? 2 :
(seed.aspect_num == 16 && seed.aspect_den == 9) ? 1 : 0;
/* Adaptive is part of the aspect vocabulary, not a second toggle.
* It follows 21:9 when ultrawide is offered, otherwise 16:9. */
const int adaptive_aspect_index = ws_ultrawide_offered ? 3 : 2;
ls.aspect_index = seed.adaptive_view ? adaptive_aspect_index :
(seed.aspect_num * 9 == seed.aspect_den * 21) ? 2 :
(seed.aspect_num == 16 && seed.aspect_den == 9) ? 1 : 0;
ls.adaptive_view = seed.adaptive_view ? 1 : 0;
/* ---- deeper PSX-style settings (capability-gated via launcher_profile
* below). Sourced 1:1 from PSXRecompV4::UserSettings (config_loader.h). */
@@ -5301,6 +5370,17 @@ int main(int argc, char** argv) {
"OpenGL (Recommended)",
"Vulkan"
};
static const char* const kAdaptiveAspectLabels[] = {
"4:3 (Native)",
"16:9 (Widescreen)",
"21:9 (Ultrawide)",
"Adaptive"
};
static const char* const kAdaptiveAspectLabelsNoUltrawide[] = {
"4:3 (Native)",
"16:9 (Widescreen)",
"Adaptive"
};
/* System identity + full PS1 settings-surface capability set (theme,
* platform label, rom_noun, pad-mode support, aspect mask base, and
* all has_* deep-settings flags) — one profile call keeps them from
@@ -5323,6 +5403,12 @@ int main(int argc, char** argv) {
gi.locked_pad_mode = p1_mode; /* force the game's declared mode (default_mode) */
gi.lock_device = ctrl_lock_device ? 1 : 0;
gi.aspect_mask = 0x1 | (ws_offered ? 0x2 : 0) | (ws_ultrawide_offered ? 0x4 : 0);
if (ws_adaptive_view_supported) {
gi.aspect_labels = ws_ultrawide_offered
? kAdaptiveAspectLabels : kAdaptiveAspectLabelsNoUltrawide;
gi.num_aspect_labels = ws_ultrawide_offered ? 4 : 3;
gi.aspect_experimental = 1;
}
gi.renderer_labels = kPsxRendererLabels;
gi.num_renderers = vulkan_offered ? 3 : 2;
/* Localization menu: shown only when the game declares languages. */
@@ -5364,14 +5450,21 @@ int main(int argc, char** argv) {
}
seed.fullscreen = ls.fullscreen; seed.has_fullscreen = true;
seed.skip_launcher = ls.skip_launcher != 0; seed.has_skip_launcher = true;
/* aspect_index round-trips 0/1/2 -> 4:3 / 16:9 / 21:9, superseding the
* legacy ls.widescreen bool (still set above for older callers). */
switch (ls.aspect_index) {
case 2: seed.aspect_num = 21; seed.aspect_den = 9; break;
case 1: seed.aspect_num = 16; seed.aspect_den = 9; break;
default: seed.aspect_num = 4; seed.aspect_den = 3; break;
/* Adaptive preserves the previous fixed aspect as the initial
* window shape. Selecting a fixed entry turns adaptation off. */
if (ws_adaptive_view_supported &&
ls.aspect_index == adaptive_aspect_index) {
seed.adaptive_view = true;
} else {
seed.adaptive_view = false;
switch (ls.aspect_index) {
case 2: seed.aspect_num = 21; seed.aspect_den = 9; break;
case 1: seed.aspect_num = 16; seed.aspect_den = 9; break;
default: seed.aspect_num = 4; seed.aspect_den = 3; break;
}
}
seed.has_aspect_ratio = true;
seed.has_adaptive_view = true;
/* has_texture_filter is on (PSX profile) so the launcher edits
* ls.texture_filter directly (0=nearest,1=bilinear); ls.linear_filter
* is the legacy fallback field for consoles without the cap and is
@@ -5473,6 +5566,7 @@ int main(int argc, char** argv) {
g_frame_interpolation_fps = seed.frame_interpolation_fps;
g_video_aspect_num = seed.aspect_num;
g_video_aspect_den = seed.aspect_den;
g_ws_adaptive_view = seed.adaptive_view;
g_audio_spu_hq = seed.spu_hq;
skip_launcher_setting = seed.skip_launcher;
if (seed.has_bios_path) {
@@ -5592,17 +5686,18 @@ session_reboot:
* this aspect; native-wide fills it with a genuinely wider frame (no
* stretch), squash mode stretches the 4:3 frame into it. */
gl_renderer_set_display_aspect(g_video_aspect_num, g_video_aspect_den);
if (g_video_aspect_num * 3 != g_video_aspect_den * 4) {
if (g_video_aspect_num * 3 != g_video_aspect_den * 4 || g_ws_adaptive_view) {
/* Hold widescreen off through the BIOS boot (authentic 4:3 logos);
* the per-frame present path engages it at game entry. */
g_ws_engaged = false;
std::fprintf(stdout,
"psxrecomp: widescreen %d:%d (%s%s%s; engages at game entry)\n",
"psxrecomp: widescreen %d:%d (%s%s%s%s; engages at game entry)\n",
g_video_aspect_num, g_video_aspect_den,
g_ws_native_wide ? "native-wide, present 1:1"
: "GTE X-squash + stretched present",
g_ws_anchor_addr ? " + sprite tags" : "",
g_ws_hud_sprt ? " + HUD squash" : "");
g_ws_hud_sprt ? " + HUD squash" : "",
g_ws_adaptive_view ? " + adaptive view" : "");
}
/* Present-time screen-colour model (verified-enhancement LUT). Default raw
* is byte-identical; PSX_SCREEN env overrides this at scanout. */