From 39ee7d79400e500eeb3ea26e3d2c35d889e9d5b5 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 20 Sep 2026 12:01:32 -0700 Subject: [PATCH] Support adaptive screen masks and native effect presentation --- runtime/CMakeLists.txt | 3 +++ runtime/include/gpu.h | 8 ++++++ runtime/include/ws_screen_mask.h | 27 ++++++++++++++++++++ runtime/src/gpu.c | 38 +++++++++++++++++++++++++++++ runtime/tests/test_ws_screen_mask.c | 29 ++++++++++++++++++++++ runtime/tests/test_ws_world_scene.c | 9 ++++++- 6 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 runtime/include/ws_screen_mask.h create mode 100644 runtime/tests/test_ws_screen_mask.c diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index a015673c..a92837b6 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -288,6 +288,9 @@ if(BUILD_TESTING) add_executable(ws_repeat_rect_test tests/test_ws_repeat_rect.c) target_include_directories(ws_repeat_rect_test PRIVATE include) add_test(NAME ws_repeat_rect_test COMMAND ws_repeat_rect_test) + add_executable(ws_screen_mask_test tests/test_ws_screen_mask.c) + target_include_directories(ws_screen_mask_test PRIVATE include) + add_test(NAME ws_screen_mask_test COMMAND ws_screen_mask_test) add_executable(mod_gpu_dma_aperture_test tests/test_mod_gpu_dma_aperture.c) diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 2abe4547..30583a5e 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -393,6 +393,14 @@ void gpu_ws_tag_black_reveal_rect(uint32_t prim); * reveal margins only. The caller verifies the composite's source period. * Original UVs, texel density, and canonical VRAM writes are unchanged. */ void gpu_ws_tag_repeat_rect(uint32_t prim, int32_t period); +/* Title-identified flat screen mask: extend its off-screen vertical boundary + * to the adaptive edge, preserving the interior opening and canonical image. + * Packet guarded, cleared on reset/load, inert outside native-wide gameplay. */ +void gpu_ws_tag_screen_mask_quad(uint32_t prim); +/* Pure emulation-thread predicate for a known native-only effect/scene. + * Nonzero forces native 4:3; zero defers. No GPU calls or guest writes inside. + * Host configuration survives reset/load; classification reads live state. */ +void gpu_ws_set_native_scene_predicate(int (*predicate)(void)); /* Targeted alternative for sprite-heavy 2D games: corner-anchor only primitives * whose ordering-table packet lives in the configured half-open RAM range. */ void gpu_ws_set_nw_left_hud_packet_range(uint32_t lo, uint32_t hi); diff --git a/runtime/include/ws_screen_mask.h b/runtime/include/ws_screen_mask.h new file mode 100644 index 00000000..9e228bf8 --- /dev/null +++ b/runtime/include/ws_screen_mask.h @@ -0,0 +1,27 @@ +#ifndef PSXRECOMP_WS_SCREEN_MASK_H +#define PSXRECOMP_WS_SCREEN_MASK_H +#include + +/* Extend only an off-screen vertical boundary of a title-identified mask. + * The interior/slanted light boundary stays at its original coordinates. + * Result is wholly outside canonical VRAM; no triangle size limit applies. */ +typedef struct { int x, y, w, h; } WsScreenMaskBand; +static inline int ws_screen_mask_band(const int32_t x[4], const int32_t y[4], + int width, int height, int margin, + WsScreenMaskBand *out) { + if (!out || width <= 0 || height <= 0 || margin <= 0 || + y[0] != y[1] || y[2] != y[3] || y[0] != 0 || y[2] != height) + return 0; + if (x[0] == x[2] && x[0] <= 0 && x[1] > x[0] && x[3] > x[0] && + x[0] > -margin) { + *out = (WsScreenMaskBand){-margin, 0, margin + x[0], height}; + return 1; + } + if (x[1] == x[3] && x[1] >= width && x[0] < x[1] && x[2] < x[1] && + x[1] < width + margin) { + *out = (WsScreenMaskBand){x[1], 0, width + margin - x[1], height}; + return 1; + } + return 0; +} +#endif diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index bdd4a131..776f4c9b 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -40,6 +40,7 @@ #include "ws_prepass_guard.h" #include "ws_hud_anchor.h" #include "ws_repeat_rect.h" +#include "ws_screen_mask.h" #include #include #include @@ -176,6 +177,7 @@ typedef struct { uint32_t key; uint32_t stamp; int32_t anchor_x; } WsTag; static WsTag ws_tags[WS_TAG_BUCKETS]; static WsHudAnchorTag ws_hud_anchor_tags[WS_HUD_ANCHOR_TABLE_SIZE]; static WsHudAnchorTag ws_reveal_clear_tags[WS_HUD_ANCHOR_TABLE_SIZE]; +static WsHudAnchorTag ws_screen_mask_tags[WS_HUD_ANCHOR_TABLE_SIZE]; static WsRepeatRectTag ws_repeat_rect_tags[WS_REPEAT_RECT_TAG_TABLE_SIZE]; static uint32_t ws_last_tag_stamp = (uint32_t)-1000; /* frame of newest tag */ static uint32_t ws_last_3d_stamp = (uint32_t)-1000; /* frame of newest shaded prim (diagnostic) */ @@ -407,6 +409,7 @@ static void ws_reset_scene_history(void) { static uint32_t s_ws_fmv_frame_cache = 0xFFFFFFFFu; static int s_ws_fmv_cached = 0; static PSXModRetainedScenePredicate s_ws_retained_scene_predicate; +static int (*s_ws_native_scene_predicate)(void); static WsSceneHold s_ws_scene_hold; static uint32_t ws_display_origin(void); @@ -415,8 +418,14 @@ void psx_mod_set_retained_scene_predicate(PSXModRetainedScenePredicate predicate ws_scene_hold_reset(&s_ws_scene_hold); } +void gpu_ws_set_native_scene_predicate(int (*predicate)(void)) { + s_ws_native_scene_predicate = predicate; +} + int gpu_ws_present_native_43(void) { if (!ws_engaged()) return 0; + if (ws_mode == 2 && s_ws_native_scene_predicate && s_ws_native_scene_predicate()) + return 1; int game_mode = ws_game_mode(); if (!game_mode) ws_scene_latch.confirmed = 0; int native_43 = !game_mode || ws_2d_only_scene(); @@ -2147,6 +2156,18 @@ void gpu_ws_tag_repeat_rect(uint32_t prim, int32_t period) { period, &guard, (uint32_t)s_frame_count); } +void gpu_ws_tag_screen_mask_quad(uint32_t prim) { + if (!ws_native_wide_configured() || (prim & 3u) || prim > UINT32_MAX - 4u) + return; + uint32_t words[12], count = 0; + if (!ws_hud_command_words(prim + 4u, words, &count) || count != 5u || + ((words[0] >> 24) & 0xfdu) != 0x28u) return; + WsPrepassPacketGuard guard = ws_prepass_packet_guard(words, count); + ws_hud_anchor_insert(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE, + (prim + 4u) & 0x1ffffcu, 0, &guard, + (uint32_t)s_frame_count); +} + static int ws_nw_explicit_hud_delta(int32_t *out_delta) { if (out_delta) *out_delta = 0; if (!ws_native_wide_active() || gp0_cmd_source_addr == 0xFFFFFFFFu) @@ -3066,6 +3087,7 @@ static void gpu_reset_state(int clear_vram) { gp0_cmd_source_addr = 0xFFFFFFFFu; ws_hud_anchor_clear(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE); ws_hud_anchor_clear(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE); + ws_hud_anchor_clear(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE); ws_repeat_rect_tag_clear(ws_repeat_rect_tags); polyline_color = 0; polyline_prev_x = polyline_prev_y = 0; @@ -4064,6 +4086,21 @@ static void gp0_exec_mono_quad(void) { int rej_b = psx_gpu_triangle_oversize(vx, vy, 2, 1, 3); if (rej_a && rej_b) return; + /* Each added band is outside the original screen and executes in the + * mask's own OT position/blend mode. Canonical VRAM clips it away. */ + if (ws_native_wide_active() && gp0_cmd_source_addr != UINT32_MAX && + ws_hud_anchor_lookup(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE, + gp0_cmd_source_addr & 0x1ffffcu, gp0_cmd_buf, 5u, + (uint32_t)s_frame_count, NULL)) { + WsScreenMaskBand band; + if (ws_screen_mask_band(vx, vy, ws_disp_w(), ws_disp_h(), + draw_area_wide_x_margin(), &band)) { + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_flat_rect(band.x + draw_offset_x, band.y + draw_offset_y, + band.w, band.h, color); + } + } + /* Full-screen filters are commonly encoded as an axis-aligned quad. Drawing * a semi-transparent quad as two independent triangles blends their shared * diagonal twice; render the equivalent rectangle once to avoid that seam. @@ -6467,6 +6504,7 @@ int gpu_snapshot_read(const uint8_t *p, uint32_t len) { ws_scene_hold_reset(&s_ws_scene_hold); ws_hud_anchor_clear(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE); ws_hud_anchor_clear(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE); + ws_hud_anchor_clear(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE); ws_repeat_rect_tag_clear(ws_repeat_rect_tags); /* Sync renderer clip/scissor to restored GP0(E3/E4); vars alone leave GL * on a stale draw area after savestate load. */ diff --git a/runtime/tests/test_ws_screen_mask.c b/runtime/tests/test_ws_screen_mask.c new file mode 100644 index 00000000..b8571340 --- /dev/null +++ b/runtime/tests/test_ws_screen_mask.c @@ -0,0 +1,29 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include "ws_screen_mask.h" +#include "ws_hud_anchor.h" +int main(void) { + int32_t x[4]={-96,160,-96,68},y[4]={0,0,240,240}; + WsScreenMaskBand b; + for(int margin=0;margin<16000;margin+=53) { + int result=ws_screen_mask_band(x,y,320,240,margin,&b); + assert(result==(margin>96)); + if(result) assert(b.x==-margin && b.x+b.w==-96 && b.y==0 && b.h==240); + } + x[0]=160;x[1]=416;x[2]=308;x[3]=416; + assert(ws_screen_mask_band(x,y,320,240,694,&b)); + assert(b.x==416 && b.w==598); + x[3]=415; assert(!ws_screen_mask_band(x,y,320,240,694,&b)); + x[3]=416;y[3]=239;assert(!ws_screen_mask_band(x,y,320,240,694,&b)); + /* Same packet address with different geometry/colour must lose its tag. */ + WsHudAnchorTag tags[WS_HUD_ANCHOR_TABLE_SIZE]={0}; + uint32_t words[5]={0x2a7b7b7b,0x0000ffa0,0xa0,0xf0ffa0,0xf00044}; + WsPrepassPacketGuard guard=ws_prepass_packet_guard(words,5); + ws_hud_anchor_insert(tags,WS_HUD_ANCHOR_TABLE_SIZE,0xa68b4,0,&guard,20); + assert(ws_hud_anchor_lookup(tags,WS_HUD_ANCHOR_TABLE_SIZE,0xa68b4,words,5,20,0)); + words[2]++;assert(!ws_hud_anchor_lookup(tags,WS_HUD_ANCHOR_TABLE_SIZE,0xa68b4,words,5,20,0)); + words[2]--;ws_hud_anchor_clear(tags,WS_HUD_ANCHOR_TABLE_SIZE); + assert(!ws_hud_anchor_lookup(tags,WS_HUD_ANCHOR_TABLE_SIZE,0xa68b4,words,5,20,0)); +} diff --git a/runtime/tests/test_ws_world_scene.c b/runtime/tests/test_ws_world_scene.c index d4c9e0a5..590c1373 100644 --- a/runtime/tests/test_ws_world_scene.c +++ b/runtime/tests/test_ws_world_scene.c @@ -5,11 +5,12 @@ uint64_t s_frame_count; static uint32_t state_word; -static int world, calls, fmv, hold; +static int world, calls, fmv, hold, native_effect; uint32_t psx_read_word(uint32_t address) { (void)address; return state_word; } int mdec_recently_active(uint32_t frames) { (void)frames; return fmv; } static int world_scene(void) { ++calls; return world; } static int retained_scene(void) { return hold; } +static int native_scene(void) { return native_effect; } static int native_43(void) { ++s_frame_count; return gpu_ws_present_native_43(); } int main(void) { @@ -38,6 +39,12 @@ int main(void) { assert(native_43() == 1); /* 24-bit FMV veto */ display_depth = 0; assert(native_43() == 0); + gpu_ws_set_native_scene_predicate(native_scene); + native_effect=1; assert(native_43()==1); /* Native attack overrides world. */ + native_effect=0; assert(native_43()==0); /* Recovery restores adaptive. */ + native_effect=1; ws_mode=0; assert(native_43()==0); /* Off stays inert. */ + ws_mode=2; gpu_ws_set_native_scene_predicate(NULL); + assert(native_43()==0); /* No policy left behind after unregister. */ psx_mod_set_retained_scene_predicate(retained_scene); hold = 0; assert(native_43() == 0);