Skip to content

Commit cece002

Browse files
mstanOpokXeno
andcommitted
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>
1 parent 1a73e98 commit cece002

5 files changed

Lines changed: 212 additions & 24 deletions

File tree

WIDESCREEN.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,26 @@ clear_reveal = true # clear synthetic native-wide side margin
7070
nw_left_hud_packet_lo = "0x000E3400" # optional targeted left-HUD packet range
7171
nw_left_hud_packet_hi = "0x000E4100" # (half-open); avoids shifting 2D scenery.
7272
offer_ultrawide = true # separate experimental 21:9 launcher row.
73+
adaptive_view = true # expose live resize-driven aspect mode.
7374
```
7475

76+
When `adaptive_view` is enabled by the game, the launcher's **Aspect ratio**
77+
selector gains an **Adaptive** entry. The previously selected fixed aspect
78+
still determines the initial window shape. Once the game window exists,
79+
resizing it continuously
80+
updates the GTE/native-wide projection, cull margins, wide render target, and
81+
present aspect. The live ratio is clamped to 4:3 on the narrow side and to the
82+
widest mode the game offers (`16:9`, or `21:9` with `offer_ultrawide`). BIOS,
83+
FMV, menus, and other title-classified 2D frames retain their existing 4:3
84+
pillarbox policy. The user choice persists as `[video] adaptive_view` in
85+
`settings.toml`.
86+
87+
The native-wide draw-area early-out correction was independently identified
88+
and contributed by **OpokXeno** in
89+
[PR #73](https://github.com/mstan/psxrecomp/pull/73). The landed variant keeps
90+
that mirror-space correction while restricting it to active framebuffer
91+
targets, so offscreen texture targets retain the original host-side pruning.
92+
7593
Tomba's values are Ghidra-evidenced: `0x8005E08C` is the shared per-prim helper
7694
all ~23 character render functions (RTPS cluster `0x800459E0``0x8004FB54`) call;
7795
the RTPS preamble stores the anchor SXY to scratchpad `0x1F800070`.

recompiler/src/config_loader.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
889889
std::vector<WidescreenSignedBoundSite> ws_signed_x_bound_sites;
890890
bool ws_offered = true;
891891
bool vulkan_offered = false;
892+
bool ws_adaptive_view = false;
892893
bool ws_ultrawide_offered = false;
893894
if (cfg.contains("video")) {
894895
const toml::value& video = toml::find(cfg, "video");
@@ -1060,6 +1061,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
10601061
ws_offered = toml::find<bool>(ws, "offer");
10611062
if (ws.contains("offer_ultrawide"))
10621063
ws_ultrawide_offered = toml::find<bool>(ws, "offer_ultrawide");
1064+
if (ws.contains("adaptive_view"))
1065+
ws_adaptive_view = toml::find<bool>(ws, "adaptive_view");
10631066
}
10641067

10651068
// Optional [widescreen.cull] block — world-space draw-cull widening.
@@ -1279,6 +1282,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
12791282
/*ws_signed_x_bound_sites*/ ws_signed_x_bound_sites,
12801283
/*ws_offered*/ ws_offered,
12811284
/*vulkan_offered*/ vulkan_offered,
1285+
/*ws_adaptive_view*/ ws_adaptive_view,
12821286
/*ws_ultrawide_offered*/ ws_ultrawide_offered,
12831287
/*ws_bg2d_count_site*/ ws_bg2d_count_site,
12841288
/*ws_bg2d_startcol_site*/ ws_bg2d_startcol_site,
@@ -1438,6 +1442,10 @@ UserSettings load_user_settings(const fs::path& path) {
14381442
s.aspect_num = n; s.aspect_den = d; s.has_aspect_ratio = true;
14391443
}
14401444
});
1445+
if (v.contains("adaptive_view")) try_get([&]{
1446+
s.adaptive_view = toml::find<bool>(v, "adaptive_view");
1447+
s.has_adaptive_view = true;
1448+
});
14411449
}
14421450
if (doc.contains("audio")) {
14431451
const toml::value& a = toml::find(doc, "audio");
@@ -1600,6 +1608,8 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
16001608
f << "frame_interpolation_fps = " << s.frame_interpolation_fps << "\n";
16011609
if (s.has_aspect_ratio)
16021610
f << "aspect_ratio = \"" << s.aspect_num << ":" << s.aspect_den << "\"\n";
1611+
if (s.has_adaptive_view)
1612+
f << "adaptive_view = " << (s.adaptive_view ? "true" : "false") << "\n";
16031613
f << "\n[audio]\n";
16041614
if (s.has_spu_hq)
16051615
f << "spu_hq = " << (s.spu_hq ? "true" : "false") << "\n";

recompiler/src/config_loader.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,12 @@ struct GameConfig {
700700
bool ws_offered = true;
701701
bool vulkan_offered = false;
702702

703+
// [widescreen] adaptive_view — let the user opt into a live, resize-driven
704+
// aspect instead of selecting only fixed 4:3/16:9/21:9 modes. The fixed
705+
// aspect remains the initial window shape; the live view is clamped to the
706+
// widest aspect this title offers.
707+
bool ws_adaptive_view = false;
708+
703709
// [widescreen] offer_ultrawide — expose a separate experimental 21:9
704710
// launcher choice for titles that have explicitly tested it. Default off;
705711
// ordinary widescreen offer remains the independent 16:9 choice.
@@ -810,6 +816,7 @@ struct UserSettings {
810816
bool has_netplay_lobby_url = false; std::string netplay_lobby_url;
811817
bool has_aspect_ratio = false; int aspect_num = 4; // display aspect W:H
812818
int aspect_den = 3; // (4:3 = native)
819+
bool has_adaptive_view = false; bool adaptive_view = false;
813820
// [audio]
814821
bool has_spu_hq = false; bool spu_hq = false;
815822
// [bios] / [disc] / [memcard]

runtime/src/gpu.c

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ static int ws_hud_sprt = 0; /* edge-anchor untagged HUD SPRTs *
8282
* in native-wide mode. */
8383
static int ws_mode = 0;
8484
static int ws_cfg_num = 4, ws_cfg_den = 3;
85+
static void ws_nw_sync_target(void);
8586

8687
#define WS_TAG_BUCKETS 4096 /* power of two */
8788
#define WS_TAG_PROBES 8
@@ -279,13 +280,22 @@ static int ws_active(void) { return ws_configured() && !gpu_ws_present_native_43
279280
int ws_native_wide_active(void) {
280281
return ws_mode == 2 && !gpu_ws_present_native_43();
281282
}
283+
/* Cull/spawn setup often runs while a scene is loading, before the first GTE
284+
* frame can classify it as gameplay. Keep that pre-render setup aware of the
285+
* configured native-wide viewport; presentation itself remains gated by
286+
* ws_native_wide_active(). */
287+
static int ws_native_wide_configured(void) {
288+
return ws_mode == 2 && ws_cfg_num * 3 > ws_cfg_den * 4;
289+
}
290+
static int ws_nw_configured_offset(void) {
291+
if (!ws_native_wide_configured()) return 0;
292+
int numr = 3 * ws_cfg_num - 4 * ws_cfg_den;
293+
int w = (int)ws_disp_w();
294+
return (w * numr + 4 * ws_cfg_den) / (8 * ws_cfg_den);
295+
}
282296
static int ws_nw_offset(void) {
283297
if (!ws_native_wide_active()) return 0;
284-
int numr = 3 * ws_cfg_num - 4 * ws_cfg_den; /* > 0 for aspects wider than 4:3 */
285-
if (numr <= 0) return 0;
286-
int w = (int)ws_disp_w();
287-
int v = (w * numr + 4 * ws_cfg_den) / (8 * ws_cfg_den); /* round-to-nearest */
288-
return v;
298+
return ws_nw_configured_offset();
289299
}
290300
int ws_nw_extra(void) { return 2 * ws_nw_offset(); }
291301

@@ -446,7 +456,12 @@ int psx_ws_x_margin(void) {
446456
* that previously fell outside the 4:3 cull window; the wide compositor then
447457
* rasterizes it into the revealed margins. Same recompiler emit sites as the
448458
* squash path ([widescreen.cull]); 0 at 4:3 so the cull stays byte-identical. */
449-
if (ws_native_wide_active()) return ws_nw_offset() + ws_cull_guard_pixels;
459+
/* Unlike rendering/presentation, do not wait for game-mode detection here.
460+
* Tomba 2 builds its terrain-cell and actor spawn lists during scene load;
461+
* returning zero until the first 3D frame permanently bakes a 4:3 frustum
462+
* into those lists. */
463+
if (ws_native_wide_configured())
464+
return ws_nw_configured_offset() + ws_cull_guard_pixels;
450465
if (!ws_active()) return 0;
451466
return (160 * (ws_xden - ws_xnum) + ws_xnum / 2) / ws_xnum;
452467
}
@@ -1261,6 +1276,10 @@ void gpu_ws_configure(int aspect_num, int aspect_den,
12611276
}
12621277
ws_anchor_addr = sprite_anchor_addr;
12631278
ws_hud_sprt = hud_sprt_squash;
1279+
/* Adaptive view can change mode/extent without the guest reissuing E3/E4
1280+
* immediately. Keep the active mirror target and its scissor in lockstep
1281+
* with the new live aspect instead of waiting for another draw-env packet. */
1282+
ws_nw_sync_target();
12641283
}
12651284

12661285
/* 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) {
23492368
* clips those for free; our GL path was building two triangles per clipped
23502369
* prim (gpu_share ~0.9, host FPS ~5–10). Skip the host rasterizer when the
23512370
* post-offset bbox cannot touch the draw area. Side effects that must still
2352-
* run (texpage latch, oversize reject) happen before these checks. */
2371+
* run (texpage latch, oversize reject) happen before these checks.
2372+
*
2373+
* Native-wide is intentionally different in X: the mirror renderer translates
2374+
* canonical framebuffer coordinates by the live reveal offset and scissors to
2375+
* the FULL wide surface, not GP0(E3/E4)'s 4:3 X range. Commit 31015ce originally
2376+
* compared every primitive only with the guest draw area here, rejecting the
2377+
* margin geometry before the mirror renderer could see it. Match the mirror's
2378+
* exact X extent while its framebuffer target is active; Y remains the guest
2379+
* draw area because native-wide does not extend vertically. At 4:3, in FMV/
2380+
* menus, in squash mode, and for offscreen texture targets, margin is zero and
2381+
* this remains the original fast reject byte-for-byte.
2382+
*
2383+
* OpokXeno independently identified the same host-side regression on Xenogears
2384+
* and contributed the original generalized fix in psxrecomp PR #73:
2385+
* https://github.com/mstan/psxrecomp/pull/73
2386+
* Keep that credit with this guarded framebuffer-target variant. */
2387+
static inline int32_t draw_area_wide_x_margin(void) {
2388+
if (!ws_native_wide_active() || !ws_is_fb_base(draw_area_left)) return 0;
2389+
return (int32_t)ws_nw_offset();
2390+
}
2391+
2392+
static inline void draw_area_host_x_bounds(int32_t *left, int32_t *right) {
2393+
int32_t margin = draw_area_wide_x_margin();
2394+
if (margin > 0) {
2395+
/* ws_nw_sync_target uses draw_area_left as the framebuffer base and
2396+
* configures a surface ws_disp_w()+2*margin pixels wide. */
2397+
*left = (int32_t)draw_area_left - margin;
2398+
*right = (int32_t)draw_area_left + (int32_t)ws_disp_w() + margin - 1;
2399+
} else {
2400+
*left = (int32_t)draw_area_left;
2401+
*right = (int32_t)draw_area_right;
2402+
}
2403+
}
2404+
23532405
static inline int draw_area_out_point(int32_t x, int32_t y) {
2354-
return x < (int32_t)draw_area_left || x > (int32_t)draw_area_right
2406+
int32_t left, right;
2407+
draw_area_host_x_bounds(&left, &right);
2408+
return x < left || x > right
23552409
|| y < (int32_t)draw_area_top || y > (int32_t)draw_area_bottom;
23562410
}
23572411

@@ -2363,14 +2417,18 @@ static inline int draw_area_out_bbox(const int32_t *vx, const int32_t *vy, int n
23632417
if (vy[i] < miny) miny = vy[i];
23642418
if (vy[i] > maxy) maxy = vy[i];
23652419
}
2366-
return maxx < (int32_t)draw_area_left || minx > (int32_t)draw_area_right
2420+
int32_t left, right;
2421+
draw_area_host_x_bounds(&left, &right);
2422+
return maxx < left || minx > right
23672423
|| maxy < (int32_t)draw_area_top || miny > (int32_t)draw_area_bottom;
23682424
}
23692425

23702426
static inline int draw_area_out_rect(int32_t x, int32_t y, int w, int h) {
23712427
if (w <= 0 || h <= 0) return 1;
2372-
return (x + w - 1) < (int32_t)draw_area_left
2373-
|| x > (int32_t)draw_area_right
2428+
int32_t left, right;
2429+
draw_area_host_x_bounds(&left, &right);
2430+
return (x + w - 1) < left
2431+
|| x > right
23742432
|| (y + h - 1) < (int32_t)draw_area_top
23752433
|| y > (int32_t)draw_area_bottom;
23762434
}

0 commit comments

Comments
 (0)