Retain streamed tile textures with live palettes and stable scene bounds

This commit is contained in:
Matthew Stanley
2026-09-19 20:02:29 -07:00
parent 04c6dc383c
commit 73fd940294
7 changed files with 103 additions and 10 deletions
+8
View File
@@ -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);
+2
View File
@@ -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).
+23 -6
View File
@@ -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 ---- */
+28 -4
View File
@@ -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;
+16
View File
@@ -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
@@ -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;
}
+25
View File
@@ -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. */