diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 58755663..f5d8c449 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -212,6 +212,10 @@ void psx_ws_sprite_tag(struct CPUState* cpu); * by this; 0 when native-wide is inactive). */ int ws_native_wide_active(void); int ws_nw_extra(void); +int ws_nw_present_width(void); +void gpu_ws_set_netplay_local_viewport(int enabled, int slot); +int gpu_ws_netplay_local_viewport_base_x(void); +int gpu_ws_netplay_local_viewport_width(void); /* True when the current frame must present at native 4:3 (FMV video or a * full-2D menu/title screen), so the squash is suppressed and content drawn * pixel-native. The present path uses the same predicate to pillarbox. */ diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index a82439d9..c4b80f1a 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -128,6 +128,7 @@ static int ws_engaged(void) { return ws_mode != 0; } /* Forward decls: defined later but used by psx_ws_backdrop_x above them. */ static int32_t ws_scale_about(int32_t x, int32_t ax); +static int32_t ws_disp_x(void); static int32_t ws_disp_w(void); static void ws_clear_all_reveal_margins(void); @@ -349,8 +350,48 @@ int ws_native_wide_active(void) { 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); +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; + ws_nw_sync_target(); +} + +static int ws_local_viewport_layout(int *base_x, int *source_w, + int *wide_w, int *offset) { + if (!ws_local_viewport_cfg || !ws_native_wide_configured() || + !ws_vertical_split_active()) + return 0; + GpuDisplayInfo di; + gpu_get_display_info(&di); + if (di.disabled || di.depth24 || di.width < 2 || di.height == 0) + return 0; + if ((di.width & 1u) != 0) + return 0; + + int src_w = (int)di.width / 2; + int target_w = ((int)di.height * ws_cfg_num + ws_cfg_den / 2) / ws_cfg_den; + if (target_w < src_w) + target_w = src_w; + int off = (target_w - src_w) / 2; + int final_w = src_w + off * 2; + + if (base_x) *base_x = (int)di.display_x + (ws_local_viewport_slot ? src_w : 0); + if (source_w) *source_w = src_w; + if (wide_w) *wide_w = final_w; + if (offset) *offset = off; + return final_w > src_w && off > 0; +} + static int ws_nw_configured_offset(void) { if (!ws_native_wide_configured()) return 0; + int local_offset = 0; + if (ws_local_viewport_layout(NULL, NULL, NULL, &local_offset)) + return local_offset; 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); @@ -361,6 +402,23 @@ static int ws_nw_offset(void) { } int ws_nw_extra(void) { return 2 * ws_nw_offset(); } +int ws_nw_present_width(void) { + int wide_w = 0; + if (ws_local_viewport_layout(NULL, NULL, &wide_w, NULL)) + return wide_w; + return (int)ws_disp_w() + ws_nw_extra(); +} + +int gpu_ws_netplay_local_viewport_base_x(void) { + int base = 0; + return ws_local_viewport_layout(&base, NULL, NULL, NULL) ? base : 0; +} + +int gpu_ws_netplay_local_viewport_width(void) { + int wide_w = 0; + return ws_local_viewport_layout(NULL, NULL, &wide_w, NULL) ? wide_w : 0; +} + /* Per-side X cull margin in screen/world units (the game's draw classifier * works in objX-camX where 1 unit ~= 1 native-4:3 screen pixel). The squash * shows a half-view of 160/s pixels (s = squash factor = ws_xnum/ws_xden), so @@ -1413,14 +1471,15 @@ int psx_ws_cull_bltz_at(const uint32_t *words, int n, int idx) { * x is the int16 screenX the handler was about to store. */ int psx_ws_backdrop_x(int x) { if (ws_native_wide_active()) { + int32_t X = ws_disp_x(); int32_t W = (int32_t)ws_disp_w(); int32_t extra = ws_nw_extra(); int32_t cx = W / 2; - int32_t d = (int16_t)x - cx; - return (int)(cx + (d * (W + extra) + (d >= 0 ? W / 2 : -W / 2)) / W); + int32_t d = (int16_t)x - X - cx; + return (int)(X + cx + (d * (W + extra) + (d >= 0 ? W / 2 : -W / 2)) / W); } if (!ws_active()) return (int16_t)x; - int32_t cx = ws_disp_w() / 2; /* screen centre (=160 @ 320) */ + int32_t cx = ws_disp_x() + ws_disp_w() / 2; /* screen centre (=160 @ 320) */ return ws_scale_about((int16_t)x, cx); } @@ -1828,8 +1887,20 @@ static int32_t ws_scale_len(int32_t w) { return s < 1 ? 1 : s; } -/* Horizontal display width in drawing space (e.g. 320). */ +/* Horizontal display viewport in drawing space (e.g. 320). For a selected + * netplay-local split viewport this is that player's authored half, not the + * whole composed display. */ +static int32_t ws_disp_x(void) { + int base = 0; + if (ws_local_viewport_layout(&base, NULL, NULL, NULL)) + return (int32_t)base; + return 0; +} + static int32_t ws_disp_w(void) { + int src_w = 0; + if (ws_local_viewport_layout(NULL, &src_w, NULL, NULL)) + return (int32_t)src_w; GpuDisplayInfo di; gpu_get_display_info(&di); return di.width ? (int32_t)di.width : 320; @@ -1846,8 +1917,8 @@ static int32_t ws_disp_h(void) { * display; ordinary world-space rectangles remain untouched. */ static void ws_expand_fullscreen_rect(int32_t *x, int32_t y, int *w, int h) { if (!ws_native_wide_active()) return; - int W = (int)ws_disp_w(), H = (int)ws_disp_h(); - if (*x <= 0 && *x + *w >= W && y <= 0 && y + h >= H) { + int X = (int)ws_disp_x(), W = (int)ws_disp_w(), H = (int)ws_disp_h(); + if (*x <= X && *x + *w >= X + W && y <= 0 && y + h >= H) { int off = ws_nw_offset(); *x -= off; *w += 2 * off; @@ -1862,11 +1933,12 @@ static void ws_expand_fullscreen_rect(int32_t *x, int32_t y, int *w, int h) { * one zone, so its pieces share a pivot and stay aligned. (Full-2D menu * screens never reach here — they get zero squash + 4:3 pillarbox instead.) */ static int32_t ws_hud_pivot(int32_t x, int32_t w) { + int32_t X = ws_disp_x(); int32_t W = ws_disp_w(); - int32_t cx = 2 * x + w; /* 2*centre, avoids losing the half */ - if (3 * cx < 2 * W) return 0; - if (3 * cx > 4 * W) return W; - return W / 2; + int32_t cx = 2 * (x - X) + w; /* 2*centre, avoids losing the half */ + if (3 * cx < 2 * W) return X; + if (3 * cx > 4 * W) return X + W; + return X + W / 2; } /* Automatic UI correction is deliberately tied to draw provenance, not to @@ -1938,8 +2010,8 @@ static int ws_auto_ui_transform_quad(int32_t vx[4], const int32_t vy[4]) { if (vy[i] > max_y) max_y = vy[i]; } int32_t width = max_x - min_x, height = max_y - min_y; - int32_t W = ws_disp_w(), H = ws_disp_h(); - if ((min_x <= 0 && max_x >= W && min_y <= 0 && max_y >= H) || + int32_t X = ws_disp_x(), W = ws_disp_w(), H = ws_disp_h(); + if ((min_x <= X && max_x >= X + W && min_y <= 0 && max_y >= H) || (width > W / 2 && height > H / 4)) return 0; @@ -1953,8 +2025,8 @@ static int ws_auto_ui_transform_quad(int32_t vx[4], const int32_t vy[4]) { static int ws_auto_ui_transform_rect(int32_t *x, int32_t y, int *w, int h) { if (!x || !w || *w <= 0 || psx_ws_prim_is_tagged()) return 0; - int32_t W = ws_disp_w(), H = ws_disp_h(); - if ((*x <= 0 && *x + *w >= W && y <= 0 && y + h >= H) || + int32_t X = ws_disp_x(), W = ws_disp_w(), H = ws_disp_h(); + if ((*x <= X && *x + *w >= X + W && y <= 0 && y + h >= H) || (*w > W / 2 && h > H / 4)) return 0; int32_t anchor; @@ -2031,8 +2103,9 @@ static int32_t ws_nw_hud_shift(int32_t x, int32_t w) { * for live A/B: some HUD composites (Tomba's AP counter) render through * the tagged sprite funnel and stay inset without it. */ if (ws_anchor_addr && !ws_nw_hud_tag_rects && psx_ws_prim_is_tagged()) return 0; + int32_t X = ws_disp_x(); int32_t W = ws_disp_w(); - int32_t cx = 2 * x + w; /* 2*centre, avoids losing the half */ + int32_t cx = 2 * (x - X) + w; /* 2*centre, avoids losing the half */ if (3 * cx < 2 * W) return -off; /* left third -> pull to left edge */ if (3 * cx > 4 * W) return off; /* right third -> push to right edge */ return 0; /* middle third -> stay centred */ @@ -2077,6 +2150,7 @@ static int ws_nw_backdrop_stretch_quad(int32_t *vx, const int32_t *vy) { if (!ws_nw_backdrop || !ws_native_wide_active()) return 0; int32_t extra = ws_nw_extra(); if (extra <= 0) return 0; + int32_t X = ws_disp_x(); int32_t W = ws_disp_w(); const int32_t EDGE = 24; /* slack for "touches the frame edge" */ int32_t minx = vx[0], maxx = vx[0], miny = vy[0], maxy = vy[0]; @@ -2087,7 +2161,7 @@ static int ws_nw_backdrop_stretch_quad(int32_t *vx, const int32_t *vy) { if (vy[i] > maxy) maxy = vy[i]; } /* Must span the full display width and a real vertical extent. */ - if (minx > EDGE || maxx < W - EDGE || (maxy - miny) < 64) return 0; + if (minx > X + EDGE || maxx < X + W - EDGE || (maxy - miny) < 64) return 0; /* Axis-aligned: every vertex X sits at either the min or the max edge, and * every Y at the top or bottom edge (a true screen-space rectangle). */ for (int i = 0; i < 4; i++) { @@ -2097,7 +2171,7 @@ static int ws_nw_backdrop_stretch_quad(int32_t *vx, const int32_t *vy) { } /* Stretch X about the display centre by (W+extra)/W so [0,W] -> [-off, W+off], * which the wide compositor (+off) maps onto the full [0, W+extra] surface. */ - int32_t cx = W / 2; + int32_t cx = X + W / 2; for (int i = 0; i < 4; i++) { int32_t d = vx[i] - cx; vx[i] = cx + (d * (W + extra) + (d >= 0 ? W / 2 : -W / 2)) / W; @@ -2170,6 +2244,45 @@ 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; +static int draw_area_intersects_rect(int x, int y, int w, int h) { + if (w <= 0 || h <= 0) + return 0; + int r = x + w - 1; + int b = y + h - 1; + return !((int)draw_area_right < x || + (int)draw_area_left > r || + (int)draw_area_bottom < y || + (int)draw_area_top > b); +} + +static int ws_local_viewport_draw_target(int *base_x) { + int base = 0, src_w = 0; + if (!ws_local_viewport_layout(&base, &src_w, NULL, NULL)) + return 0; + GpuDisplayInfo di; + gpu_get_display_info(&di); + if (draw_area_intersects_rect(base, (int)di.display_y, + src_w, (int)di.height)) { + if (base_x) *base_x = base; + return 1; + } + return 0; +} + +static int ws_display_viewport_draw_target(int *base_x) { + if (!ws_native_wide_active()) + return 0; + GpuDisplayInfo di; + gpu_get_display_info(&di); + if (di.disabled || di.depth24 || di.width == 0 || di.height == 0) + return 0; + if (!draw_area_intersects_rect((int)di.display_x, (int)di.display_y, + (int)di.width, (int)di.height)) + return 0; + if (base_x) *base_x = (int)di.display_x; + return 1; +} + typedef struct GpuVerticalSplitTrace { uint8_t left_seen; uint8_t right_seen; @@ -2293,10 +2406,14 @@ static void split_trace_note_draw_area(void) { } } -int gpu_last_frame_vertical_split_screen(void) { +static int ws_vertical_split_active(void) { return split_recent_left_age <= 8 && split_recent_right_age <= 8; } +int gpu_last_frame_vertical_split_screen(void) { + return ws_vertical_split_active(); +} + 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; @@ -2308,7 +2425,18 @@ void gpu_vertical_split_debug(int *active, int *left_age, int *right_age) { * disable mirroring for this draw. Called when the draw env changes. */ static void ws_nw_sync_target(void) { if (!ws_native_wide_active()) { gr_wide_disable_target(); return; } - gr_wide_configure((int)ws_disp_w() + ws_nw_extra(), ws_nw_offset()); + gr_wide_configure(ws_nw_present_width(), ws_nw_offset()); + int local_base = 0; + if (ws_local_viewport_draw_target(&local_base)) { + gr_wide_set_target(local_base); + return; + } + if (ws_local_viewport_cfg) { gr_wide_disable_target(); return; } + int display_base = 0; + if (ws_display_viewport_draw_target(&display_base)) { + gr_wide_set_target(display_base); + return; + } uint32_t base = draw_area_left; if (ws_is_fb_base(base)) gr_wide_set_target((int)base); else gr_wide_disable_target(); @@ -3175,7 +3303,11 @@ static void raster_pixel(int32_t x, int32_t y, uint16_t color) { * 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; + int local_base = 0; + if (!ws_native_wide_active()) return 0; + if (ws_local_viewport_draw_target(&local_base)) return (int32_t)ws_nw_offset(); + if (ws_display_viewport_draw_target(NULL)) return (int32_t)ws_nw_offset(); + if (!ws_is_fb_base(draw_area_left)) return 0; return (int32_t)ws_nw_offset(); } @@ -3971,7 +4103,11 @@ static void gp0_exec_fill_rect(void) { /* Native-wide: when the game clears a display buffer, clear the full width * of that buffer's wide surface over the same rows — refreshing the centred * content region and keeping the revealed margins clean. */ - if (ws_native_wide_active() && ws_is_fb_base(dst_x)) { + int local_base = 0; + if (ws_native_wide_active() && + (ws_is_fb_base(dst_x) || + (ws_local_viewport_draw_target(&local_base) && + dst_x == (uint32_t)local_base))) { gr_wide_clear((int)dst_x, (int)dst_y, (int)height, color16); } } @@ -4362,15 +4498,15 @@ static void ws_ui_prepass_add(const uint32_t *words, uint32_t source_addr, } int32_t width = max_x - min_x, height = max_y - min_y; - int32_t W = ws_disp_w(), H = ws_disp_h(); - if ((min_x <= 0 && max_x >= W && min_y <= 0 && max_y >= H) || + int32_t X = ws_disp_x(), W = ws_disp_w(), H = ws_disp_h(); + if ((min_x <= X && max_x >= X + W && min_y <= 0 && max_y >= H) || (width > W / 2 && height > H / 4)) return; WsUiPrepassItem *item = &ws_ui_prepass[ws_ui_prepass_count++]; item->group.key = ws_auto_ui_group_key_words(words, op, min_y, height); - item->group.x = min_x; + item->group.x = min_x - X; item->group.width = width; item->group.anchor = 0; item->src_addr = source_addr & 0x1FFFFCu; @@ -4463,13 +4599,14 @@ void gpu_ws_prepass_linked_list(uint32_t start_addr) { ws_auto_ui_dense = ws_ui_prepass_count >= 32u && !ws_gte_game_mode_cfg; if (ws_ui_prepass_count == 0) return; + const int32_t group_origin = ws_disp_x(); WsUiGroupItem groups[WS_UI_PREPASS_MAX]; for (uint32_t i = 0; i < ws_ui_prepass_count; i++) groups[i] = ws_ui_prepass[i].group; ws_ui_group_assign(groups, ws_ui_prepass_count, ws_disp_w(), ws_auto_ui_dense); for (uint32_t i = 0; i < ws_ui_prepass_count; i++) - ws_ui_prepass[i].group.anchor = groups[i].anchor; + ws_ui_prepass[i].group.anchor = group_origin + groups[i].anchor; } /* Per-opcode execution counters (exposed via gpu_get_opcode_stats) */ diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 0c0a3e2a..6230e189 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1425,6 +1425,9 @@ static bool g_ws_hud_sprt = false; * 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_netplay_local_viewport(int enabled, int slot); +extern "C" int gpu_ws_netplay_local_viewport_base_x(void); +extern "C" int gpu_ws_netplay_local_viewport_width(void); 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 @@ -1512,7 +1515,13 @@ 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; + const bool local_native_wide = + g_netplay_local_viewport == 1 && psx_netplay_active() && + gpu_last_frame_vertical_split_screen(); + const bool native_wide = (g_netplay_local_viewport == 1) + ? local_native_wide + : (g_ws_native_wide != 0); + const int mode = wide ? (native_wide ? 2 : 1) : 0; int proj_num = g_video_aspect_num; int proj_den = g_video_aspect_den; if (mode == 1) { @@ -2479,9 +2488,11 @@ static void apply_netplay_local_viewport_aspect(bool netplay_enabled) { if (!netplay_enabled || g_netplay_local_viewport != 1 || g_netplay_local_viewport_aspect == 0) { + gpu_ws_set_netplay_local_viewport(0, 0); return; } + gpu_ws_set_netplay_local_viewport(1, psx_netplay_local_slot()); switch (g_netplay_local_viewport_aspect) { case 1: (void)psx_mod_set_fixed_display_aspect(16u, 9u); @@ -6426,6 +6437,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { bool pin_43 = false; /* pillarbox this present (FMV, or a native-wide game frame that could not present wide) */ bool depth24_frame = false; + bool local_viewport_crop_applied = false; if (s_force_present_after_load && g_gl_active) gl_renderer_flush_cpu_uploads(); { @@ -6480,8 +6492,11 @@ 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; + const int local_viewport_slot = netplay_local_viewport_slot(); + const bool local_viewport_crop = local_viewport_slot >= 0; + bool local_viewport_wide = + local_viewport_crop && g_ws_engaged && ws_native_wide_active() && + gr_wide_supported() && gpu_ws_netplay_local_viewport_width() > 0; /* Canonical present width. Native-wide does NOT widen the canonical read * (that bled across adjacent framebuffers); it composites into a separate @@ -6493,8 +6508,12 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { * 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() && - !local_viewport_crop); - if (wide_present) present_w = w + (uint32_t)ws_nw_extra(); + (!local_viewport_crop || local_viewport_wide)); + if (wide_present) { + present_w = local_viewport_wide + ? (uint32_t)gpu_ws_netplay_local_viewport_width() + : (w + (uint32_t)ws_nw_extra()); + } /* Native-wide invariant: canonical (320-wide) content is NEVER * stretched across the wide window — a game frame that cannot present @@ -6597,12 +6616,16 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { * hires path if the displayed buffer has no surface yet. */ int s = gr_scale(); int sw = (int)present_w * s; + int base_x = local_viewport_wide + ? gpu_ws_netplay_local_viewport_base_x() + : (int)di.display_x; int n = gr_render_wide_display(sdl_pixel_buf, (int)(sw * sizeof(uint32_t)), - (int)di.display_x, (int)di.display_y, (int)h); + base_x, (int)di.display_y, (int)h); if (n > 0) { active_scale = s; } else { wide_present = false; + local_viewport_wide = false; present_w = w; pres_entry->path = PRES_PATH_CANONICAL; pres_entry->wide_fellback = 1; @@ -6645,7 +6668,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { 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, + if (!local_viewport_wide && + crop_present_to_netplay_local_viewport(sdl_pixel_buf, &present_px_w, present_px_h)) { pin_43 = false; @@ -6709,7 +6733,7 @@ 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) + if (local_viewport_crop_applied && src_w >= 2) src_w /= 2; if (g_gl_active) { /* OpenGL present: upload the active display rect and draw a full-screen @@ -12831,6 +12855,7 @@ session_reboot: nrc, net_cfg.local_slot, net_cfg.bind_hostport, net_cfg.peer_hostport); return 1; } + apply_netplay_local_viewport_aspect(net_cfg.enabled); std::printf("psxrecomp: netplay transport=%s slot=%d input_player=%d delay=%d " "force_turn=%d bind=%s peer=%s session=%u\n", psx_netplay_transport_name(),