From 73fd9402945b41834918c24f84237186ef10d2b3 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 19 Sep 2026 20:02:29 -0700 Subject: [PATCH] Retain streamed tile textures with live palettes and stable scene bounds --- runtime/include/gpu.h | 8 +++++ runtime/include/gpu_gl_renderer.h | 2 ++ runtime/src/gpu.c | 29 +++++++++++++---- runtime/src/gpu_gl_renderer.c | 32 ++++++++++++++++--- runtime/tests/test_gl_readback_region.c | 16 ++++++++++ .../test_gpu_textured_dot_nw_shift_exec.c | 1 + runtime/tests/test_ws_view_anchor_gpu.c | 25 +++++++++++++++ 7 files changed, 103 insertions(+), 10 deletions(-) diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index e4eb546d..2abe4547 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -172,6 +172,10 @@ void gpu_ws_set_full_2d(int on); /* Signed 16-bit camera/min/max; active is a nonzero byte. Requires bg2d hooks. * Zero addresses disable the feature. No guest memory is written. */ void gpu_ws_set_view_anchor(uint32_t camera, uint32_t min, uint32_t max, uint32_t active); +/* Optional authored scene bounds when native camera locks do not delimit + * visible geometry. Set before layer setup; disable when leaving the scene. + * Changes presentation only, and has no effect without view anchoring. */ +void gpu_ws_set_view_bounds_override(int enabled, int minimum, int maximum); /* Bracket each bg2d packet producer, with its guest packet pointer. The * independent-layer mask identifies parallax backdrops that may anchor to * their own map edges; linked/foreground layers retain the world origin. */ @@ -185,6 +189,10 @@ void gpu_ws_bg2d_end_view_layer(unsigned layer, uint32_t packet); * native while this arena is registered. Allocate once during mod activation. */ void gpu_ws_bg2d_set_host_arena(uint32_t base, uint32_t size); #define GPU_WS_BG2D_PACKET_MAGIC 0x58364247u +#define GPU_WS_BG2D_BANK_PACKET_MAGIC 0x58364248u +/* Bank packets pack signed shift in +16 low half and immutable bank ID in + * its high half. Their indices use the bank, but palettes remain live VRAM. + * Keep the original magic readable for pending packets in older snapshots. */ #define GPU_WS_BG2D_MIRROR_X 0x80000000u struct WsViewAnchor; int gpu_ws_bg2d_get_view(unsigned layer, struct WsViewAnchor *view); diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index e9ad1263..73fe0e61 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -21,6 +21,8 @@ int gl_renderer_init_context(struct SDL_Window *win); /* Select a retained immutable bank for the next textured submission; zero * selects live VRAM. Emulation/GL owning thread only. Returns 0 if unavailable. */ int gl_renderer_select_texture_bank(uint16_t id); +/* Retain texture indices while sampling current guest CLUTs (fades/animation). */ +int gl_renderer_select_texture_bank_live_clut(uint16_t id); int gl_renderer_texture_banks_supported(void); /* Set the GL swap interval / vsync mode (1=vsync, 0=immediate, -1=adaptive). diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 2bdc1517..2fa2d383 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -514,6 +514,7 @@ static int ws_nw_offset(void) { int ws_nw_extra(void) { return 2 * ws_nw_offset(); } static uint32_t ws_view_camera_addr, ws_view_min_addr, ws_view_max_addr, ws_view_active_addr; +static int ws_view_bounds_override, ws_view_bounds_min, ws_view_bounds_max; static WsViewAnchor ws_view; static uint32_t ws_view_frame = UINT32_MAX; void gpu_ws_set_view_anchor(uint32_t camera, uint32_t min, uint32_t max, uint32_t active) { @@ -521,10 +522,16 @@ void gpu_ws_set_view_anchor(uint32_t camera, uint32_t min, uint32_t max, uint32_ ws_view_min_addr = min; ws_view_max_addr = max; ws_view_active_addr = active; + ws_view_bounds_override = 0; ws_view_frame = UINT32_MAX; memset(&ws_view, 0, sizeof(ws_view)); gr_wide_set_view(0, 0, 0, 0); } +void gpu_ws_set_view_bounds_override(int enabled, int minimum, int maximum) { + ws_view_bounds_override = enabled && minimum <= maximum; + ws_view_bounds_min = minimum; + ws_view_bounds_max = maximum; +} static int ws_view_enabled(void) { return ws_view_camera_addr && ws_view_min_addr && ws_view_max_addr && ws_view_active_addr && ws_native_wide_active() && ws_disp_w() <= 384; @@ -541,8 +548,8 @@ static void ws_view_sample(void) { if (!ws_view_enabled()) return; ws_view = ws_view_anchor(ws_nw_offset(), (int16_t)psx_read_half(ws_view_camera_addr), - (int16_t)psx_read_half(ws_view_min_addr), - (int16_t)psx_read_half(ws_view_max_addr)); + ws_view_bounds_override ? ws_view_bounds_min : (int16_t)psx_read_half(ws_view_min_addr), + ws_view_bounds_override ? ws_view_bounds_max : (int16_t)psx_read_half(ws_view_max_addr)); if (!psx_read_byte(ws_view_active_addr)) { ws_view.left = ws_view.right = ws_nw_offset(); ws_view.shift = ws_view.pad_left = ws_view.pad_right = 0; @@ -1221,8 +1228,8 @@ static uint32_t ws_bg2d_host_packet(void) { uint32_t offset = command - g_bg2d_host_base - 4u; if ((offset & 31u) || offset > g_bg2d_host_size - 32u) return 0; uint32_t packet = command - 4u; - return (psx_read_word(packet + 28u) & ~GPU_WS_BG2D_MIRROR_X) == - GPU_WS_BG2D_PACKET_MAGIC ? packet : 0; + uint32_t magic = psx_read_word(packet + 28u) & ~GPU_WS_BG2D_MIRROR_X; + return magic == GPU_WS_BG2D_PACKET_MAGIC || magic == GPU_WS_BG2D_BANK_PACKET_MAGIC ? packet : 0; } #define WS_VIEW_LAYERS 8 static int ws_view_layer = -1; @@ -1292,7 +1299,9 @@ static WsViewAnchor ws_view_packet(void) { uint32_t packet = ws_bg2d_host_packet(); if (packet) { WsViewAnchor v = ws_view_current(); - v.shift = (int32_t)psx_read_word(packet + 16u); + uint32_t metadata = psx_read_word(packet + 16u); + v.shift = (psx_read_word(packet + 28u) & ~GPU_WS_BG2D_MIRROR_X) == + GPU_WS_BG2D_BANK_PACKET_MAGIC ? (int16_t)metadata : (int32_t)metadata; v.pad_left = (int32_t)psx_read_word(packet + 20u); v.pad_right = (int32_t)psx_read_word(packet + 24u); v.left = ws_nw_offset() + v.shift - v.pad_left; @@ -4720,7 +4729,9 @@ static void gp0_exec_textured_16x16(void) { * a stale ring slot; suppressing here caused the stage-start black flicker. */ uint32_t host_packet = ws_bg2d_host_packet(); int host_tile = host_packet != 0; - if (host_tile && !ws_native_wide_active()) return; + /* A pending full-width OT can outlive a resize back to 4:3. Keep its + * central tiles; the normal draw area clips the extra columns. Dropping + * every host packet here would blank that transition frame. */ int ws_w = host_tile ? 0 : ws_sprt_fixed_transform(&x0, y0, 16); if (!host_tile) x0 += ws_nw_hud_shift(x0, 16); x0 += draw_offset_x; y0 += draw_offset_y; @@ -4734,6 +4745,11 @@ static void gp0_exec_textured_16x16(void) { int dw = (ws_w && ws_w != 16) ? ws_w : 16; if (draw_area_out_rect(x0, y0, dw, 16)) return; } + uint16_t bank = host_tile && + (psx_read_word(host_packet + 28u) & ~GPU_WS_BG2D_MIRROR_X) == GPU_WS_BG2D_BANK_PACKET_MAGIC ? + (uint16_t)(psx_read_word(host_packet + 16u) >> 16) : 0; + if (bank && (gr_backend() != GR_BACKEND_OPENGL || + !gl_renderer_select_texture_bank_live_clut(bank))) return; setup_textured_draw(color24, semi_trans, raw_texture); if (host_tile && (psx_read_word(host_packet + 28u) & GPU_WS_BG2D_MIRROR_X)) gr_draw_textured_rect_scaled(x0, y0, 16, 16, u0 + 15, v0, u0 - 1, v0 + 16, @@ -4743,6 +4759,7 @@ static void gp0_exec_textured_16x16(void) { clut_x, clut_y, current_texpage()); else gr_draw_textured_rect(x0, y0, 16, 16, u0, v0, clut_x, clut_y, current_texpage()); + if (bank) (void)gl_renderer_select_texture_bank(0); } /* ---- GP0 command execution ---- */ diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 18d1591a..b555a369 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -362,6 +362,7 @@ static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh); static int s_raster_ok = 0; /* full GPU pipeline available */ static GLuint s_bank_tex[65536]; static GLuint s_selected_bank_tex; +static int s_selected_bank_live_clut, s_tb_bank_live_clut; /* Authoritative VRAM: hr color texture + stencil (mask bit) FBO. */ static GLuint s_hr_tex = 0, s_hr_fbo = 0, s_hr_rb = 0; @@ -398,6 +399,7 @@ static float s_pq[3]; /* TEX program uniforms. */ static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1; +static GLint s_uPalette = -1; static GLint s_uRaw = -1, s_uSemipass = -1, s_uSemimode = -1; static GLint s_uTwin = -1, s_uMaskset = -1, s_uFilter = -1; static GLint s_uLimits = -1; @@ -1105,6 +1107,7 @@ static const char *TEX_FS = "flat in ivec4 v_limits; /* prim uv sampling bounds (inclusive, post-wrap) */\n" "flat in int v_semi; /* GP0 command has semi-transparency enabled */\n" "uniform usampler2D u_vram;\n" + "uniform usampler2D u_palette;\n" "uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n" "uniform int u_semimode; /* PS1 blend mode; drives dual-source factors */\n" "uniform ivec4 u_twin; /* texture window: mask_x, mask_y, off_x, off_y */\n" @@ -1116,6 +1119,11 @@ static const char *TEX_FS = " if (any(greaterThanEqual(p, textureSize(u_vram, 0)))) return 0;\n" " return int(texelFetch(u_vram, p, 0).r);\n" "}\n" + "int palette_at(int x, int y){\n" + " ivec2 p = ivec2(x & 1023, y & 511);\n" + " if (any(greaterThanEqual(p, textureSize(u_palette, 0)))) return 0;\n" + " return int(texelFetch(u_palette, p, 0).r);\n" + "}\n" "int fetch_texel(int u, int v){\n" " u &= 255; v &= 255;\n" " if ((u_twin.x | u_twin.y) != 0) {\n" @@ -1127,10 +1135,10 @@ static const char *TEX_FS = " }\n" " if (v_depth == 0) {\n" " int px = vram_at(v_tpage.x + (u >> 2), v_tpage.y + v);\n" - " return vram_at(v_clut.x + ((px >> ((u & 3) * 4)) & 0xF), v_clut.y);\n" + " return palette_at(v_clut.x + ((px >> ((u & 3) * 4)) & 0xF), v_clut.y);\n" " } else if (v_depth == 1) {\n" " int px = vram_at(v_tpage.x + (u >> 1), v_tpage.y + v);\n" - " return vram_at(v_clut.x + ((px >> ((u & 1) * 8)) & 0xFF), v_clut.y);\n" + " return palette_at(v_clut.x + ((px >> ((u & 1) * 8)) & 0xFF), v_clut.y);\n" " }\n" " return vram_at(v_tpage.x + u, v_tpage.y + v);\n" "}\n" @@ -1526,7 +1534,7 @@ static void flush_pack_if_sampling(int tpage_x, int tpage_y, int depth, int clut_x, int clut_y) { if (!s_pack_dirty.set) return; int page_w = depth == 0 ? 64 : depth == 1 ? 128 : 256; /* VRAM columns */ - if (rect_intersects(&s_pack_dirty, tpage_x, tpage_y, + if (tpage_x >= 0 && rect_intersects(&s_pack_dirty, tpage_x, tpage_y, tpage_x + page_w - 1, tpage_y + 255)) { flush_flat_batch(); flush_tex_batch(); /* queued draws are part of s_pack_dirty — realise them before packing */ @@ -1876,6 +1884,10 @@ static void flush_tex_batch(void) { p_glActiveTexture(PSXGL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, s_tb_bank_tex ? s_tb_bank_tex : s_raw_tex); p_glUniform1i(s_uVram, 0); + p_glActiveTexture(PSXGL_TEXTURE0 + 1); + glBindTexture(GL_TEXTURE_2D, s_tb_bank_tex && !s_tb_bank_live_clut ? s_tb_bank_tex : s_raw_tex); + p_glUniform1i(s_uPalette, 1); + p_glActiveTexture(PSXGL_TEXTURE0); p_glUniform4i(s_uTwin, s_tb_twin[0], s_tb_twin[1], s_tb_twin[2], s_tb_twin[3]); p_glUniform1i(s_uMaskset, s_tb_mask); p_glUniform1i(s_uFilter, s_tb_filter); @@ -2072,6 +2084,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys, flush_cpu_upload(); /* if a CPU->VRAM upload is pending it flushes the batch first */ if (!s_selected_bank_tex) flush_pack_if_sampling(base_x, base_y, depth, clut_x, clut_y); + else if (s_selected_bank_live_clut) + flush_pack_if_sampling(-1, 0, depth, clut_x, clut_y); mark_prim_dirty(xs, ys, 3, 1 /* textured */); /* Append to the textured batch. Flush first if this prim's blend/mask/twin/ @@ -2111,7 +2125,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys, !mod_texture_bank_batchable(s_selected_bank_tex != 0, s_mask_check, semi); int reason = -1; if (s_tb_n > 0) { - if (s_tb_bank_tex != s_selected_bank_tex) reason = 0; + if (s_tb_bank_tex != s_selected_bank_tex || s_tb_bank_live_clut != s_selected_bank_live_clut) reason = 0; else if (isolate) reason = 0; else if (batch_semi != s_tb_semi) reason = 1; else if (s_mask_set != s_tb_mask) reason = 2; @@ -2128,6 +2142,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys, if (s_tb_n == 0) { /* opening a batch: capture its keyed state */ s_tb_semi = batch_semi; s_tb_mask = s_mask_set; s_tb_filter = s_tex_filter; s_tb_gate = gate; s_tb_bank_tex = s_selected_bank_tex; + s_tb_bank_live_clut = s_selected_bank_live_clut; s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy; } float *vp = &s_tb[s_tb_n * TEXV]; @@ -2875,6 +2890,7 @@ static int init_gpu_raster(void) { if (!make_fbo(&s_scratch_fbo, s_scratch_tex, 0)) return 0; s_uVram = p_glGetUniformLocation(s_tex_prog, "u_vram"); + s_uPalette = p_glGetUniformLocation(s_tex_prog, "u_palette"); s_uTpage = p_glGetUniformLocation(s_tex_prog, "u_tpage"); s_uClut = p_glGetUniformLocation(s_tex_prog, "u_clut"); s_uDepth = p_glGetUniformLocation(s_tex_prog, "u_depth"); @@ -3016,6 +3032,7 @@ int gl_renderer_select_texture_bank(uint16_t id) { uint32_t width, height; const uint16_t* pixels; GLint alignment, row_length; + s_selected_bank_live_clut = 0; if (!id) { s_selected_bank_tex = 0; return 1; } if (!gl_renderer_texture_banks_supported()) return 0; if (!s_bank_tex[id]) { @@ -3045,6 +3062,12 @@ int gl_renderer_select_texture_bank(uint16_t id) { return 1; } +int gl_renderer_select_texture_bank_live_clut(uint16_t id) { + if (!gl_renderer_select_texture_bank(id)) return 0; + s_selected_bank_live_clut = id != 0; + return 1; +} + int gl_renderer_init_context(SDL_Window *win) { s_win = win; s_present_w = 0; @@ -3151,6 +3174,7 @@ void gl_renderer_shutdown(void) { } memset(s_bank_tex, 0, sizeof s_bank_tex); s_selected_bank_tex = s_tb_bank_tex = 0; + s_selected_bank_live_clut = s_tb_bank_live_clut = 0; if (s_ctx) { ensure_cpu(); SDL_GL_DeleteContext(s_ctx); s_ctx = NULL; diff --git a/runtime/tests/test_gl_readback_region.c b/runtime/tests/test_gl_readback_region.c index 24f9e7d1..29b97a88 100644 --- a/runtime/tests/test_gl_readback_region.c +++ b/runtime/tests/test_gl_readback_region.c @@ -135,6 +135,22 @@ int main(int argc,char **argv){ check(gl_renderer_select_texture_bank(0),"reset bank after direct texture"); check(glb_vram_read(302,252)==0x001f,"retained 16-bit texel"); verify("retained banks and original VRAM ordered together"); + /* A streamed scene can retain indices while CLUT uploads/fades continue. + * Alternate both modes of the SAME bank with pending draws: palette source + * must participate in the batch key, and stock packets must reset it. */ + glb_vram_write(1,0,0x7c00); + check(gl_renderer_select_texture_bank_live_clut(7),"select bank with live CLUT"); + glb_draw_shaded_textured_triangle(400,250,64,0,0x808080,432,250,64,0,0x808080,400,282,64,0,0x808080,0,0,0,1); + check(gl_renderer_select_texture_bank(7),"same bank with retained CLUT"); + glb_draw_shaded_textured_triangle(440,250,64,0,0x808080,472,250,64,0,0x808080,440,282,64,0,0x808080,0,0,0,1); + check(glb_vram_read(402,252)==0x7c00,"live guest palette used"); + check(glb_vram_read(442,252)==0x03e0,"same-bank palette source is a batch key"); + glb_vram_write(1,0,0x001f); + check(gl_renderer_select_texture_bank_live_clut(7),"live CLUT after update"); + glb_draw_shaded_textured_triangle(480,250,64,0,0x808080,512,250,64,0,0x808080,480,282,64,0,0x808080,0,0,0,1); + check(gl_renderer_select_texture_bank(0),"reset live-CLUT bank"); + check(glb_vram_read(482,252)==0x001f,"palette update visible without replacing indices"); + verify("retained indices with animated guest CLUT"); verify_bank_batching(); /* World and UI use different origins in an anchored wide frame. Keep the * canonical-center optimization enabled to catch an erroneous blit over the diff --git a/runtime/tests/test_gpu_textured_dot_nw_shift_exec.c b/runtime/tests/test_gpu_textured_dot_nw_shift_exec.c index ffdc089f..b82c14ad 100644 --- a/runtime/tests/test_gpu_textured_dot_nw_shift_exec.c +++ b/runtime/tests/test_gpu_textured_dot_nw_shift_exec.c @@ -19,6 +19,7 @@ int g_psx_vram_dirty_tracking; GrBackend gr_backend(void) { return GR_BACKEND_SOFTWARE; } int gl_renderer_texture_banks_supported(void) { return 0; } int gl_renderer_select_texture_bank(uint16_t id) { return id == 0; } +int gl_renderer_select_texture_bank_live_clut(uint16_t id) { return id == 0; } uint16_t mod_texture_packet_bank(uint32_t p, const uint32_t *words, uint32_t n) { (void)p; (void)words; (void)n; return 0; } diff --git a/runtime/tests/test_ws_view_anchor_gpu.c b/runtime/tests/test_ws_view_anchor_gpu.c index 3d0a81a9..cbc27b82 100644 --- a/runtime/tests/test_ws_view_anchor_gpu.c +++ b/runtime/tests/test_ws_view_anchor_gpu.c @@ -55,6 +55,18 @@ int main(void) { assert(psx_ws_bg2d_startcol(0, 63) == 0); assert(psx_ws_bg2d_cols(21) == 28); assert(ws_view_current().left == 0 && ws_view_current().right == 106); + /* A camera lock inside continuous geometry must not become a new scene + * edge. An explicit scene bound still anchors at the real outer edges. */ + half_at(0x80097202u, 1968); half_at(0x80097216u, 1968); + gpu_ws_set_view_bounds_override(1, 0, 5120); + ws_view_sample(); + assert(ws_view_current().left == 53 && ws_view_current().shift == 0); + half_at(0x80097202u, 0); ws_view_sample(); + assert(ws_view_current().left == 0 && ws_view_current().right == 106); + gpu_ws_set_view_bounds_override(0, 0, 5120); + half_at(0x80097202u, 1968); ws_view_sample(); + assert(ws_view_current().left == 0 && ws_view_current().right == 106); + half_at(0x80097216u, 0); half_at(0x80097202u, 0); /* With the option disabled every original BG helper remains centered. */ gpu_ws_set_view_anchor(0, 0, 0, 0); assert(psx_ws_bg2d_cols(21) == 29); @@ -82,7 +94,20 @@ int main(void) { assert(last_scaled_rect.calls==1 && last_scaled_rect.x==400); assert(last_scaled_rect.w==16 && last_scaled_rect.h==16); assert(last_scaled_rect.u0==15 && last_scaled_rect.u1==-1); + ws_mode = 0; + gp0_cmd_buf[1]=pack_vertex(20,20); + int before_resize_calls=last_scaled_rect.calls; + gp0_exec_textured_16x16(); + assert(last_scaled_rect.calls==before_resize_calls+1 && last_scaled_rect.x==20); + configure_native_wide_16_9(); half_at(0x10001e, 0x5836u); + /* New metadata packs a bank ID beside a signed view shift; old saved + * packet metadata remains supported, with no accidental sign extension. */ + half_at(0x10001c, 0x4248u); + half_at(0x100010, (uint16_t)-53); half_at(0x100012, 0x6001u); + assert(ws_view_packet().shift == -53); + half_at(0x10001c, 0x4247u); half_at(0x100012, 0xffffu); + assert(ws_view_packet().shift == -53); gp0_cmd_source_addr = 0x00010004u; parse_vertex(pack_vertex(1400, 20), &px, &py); assert(px == -648 && py == 20); /* Ordinary hardware packet unchanged. */