Support adaptive host tile packets and mirrored panorama extensions

This commit is contained in:
Matthew Stanley
2026-09-19 19:06:42 -07:00
parent 41e92d8a32
commit 95a977953b
7 changed files with 161 additions and 10 deletions
+12
View File
@@ -177,6 +177,17 @@ void gpu_ws_set_view_anchor(uint32_t camera, uint32_t min, uint32_t max, uint32_
* their own map edges; linked/foreground layers retain the world origin. */
void gpu_ws_bg2d_begin_view_layer(unsigned layer, uint32_t packet, unsigned independent_mask);
void gpu_ws_bg2d_end_view_layer(unsigned layer, uint32_t packet);
/* Opt-in host-produced SPRT_16 packets, 32-byte slots in enhancement DMA RAM.
* The 16-byte standard packet is followed by signed view shift, left/right
* padding and GPU_WS_BG2D_PACKET_MAGIC, optionally ORed with MIRROR_X.
* Coordinates use signed 16-bit X; ordinary
* packets retain PS1 signed 11-bit coordinates. The guest tile loop/ring stays
* 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_MIRROR_X 0x80000000u
struct WsViewAnchor;
int gpu_ws_bg2d_get_view(unsigned layer, struct WsViewAnchor *view);
void gpu_ws_set_auto_ui_squash(int on);
/* [widescreen.bg2d] Capcom 2D background tile-loop widen — hooked at the renderer's
* column-count / start-tile-col / start-screen-x instructions. Identity at 4:3
@@ -470,6 +481,7 @@ typedef struct {
int nw_extra; /* native-wide frame growth (display px), 0 if off */
int view_anchor, view_left, view_right, view_shift, view_pad_left, view_pad_right;
uint64_t cur_frame;
uint32_t bg2d_generations; /* Game background submissions, independent of vblank. */
uint32_t last_tag_frame; /* frame of newest tagged prim */
uint32_t last_3d_frame; /* frame of newest shaded prim (diagnostic) */
uint32_t gte_verts; /* RTPS/RTPT verts in the last completed frame */
+2
View File
@@ -5361,6 +5361,7 @@ static void handle_gpu_state(int id, const char *json)
"\"mode\":%d,\"nw_extra\":%d,"
"\"view_anchor\":%d,\"view_left\":%d,\"view_right\":%d,"
"\"view_shift\":%d,\"view_padding\":[%d,%d],"
"\"bg2d_generations\":%u,"
"\"cur_frame\":%llu,\"last_tag_frame\":%u,\"last_3d_frame\":%u,"
"\"gte_verts\":%u,\"last_world3d_frame\":%u,"
"\"ovh_prims\":%u,\"last_ovh_frame\":%u,"
@@ -5393,6 +5394,7 @@ static void handle_gpu_state(int id, const char *json)
ws.mode, ws.nw_extra,
ws.view_anchor, ws.view_left, ws.view_right, ws.view_shift,
ws.view_pad_left, ws.view_pad_right,
ws.bg2d_generations,
(unsigned long long)ws.cur_frame, ws.last_tag_frame,
ws.last_3d_frame, ws.gte_verts, ws.last_world3d_frame,
ws.ovh_prims, ws.last_ovh_frame,
+72 -9
View File
@@ -1207,6 +1207,23 @@ static uint32_t g_bg2d_layer_struct_stride = 0x54;
static uint32_t g_bg2d_packet_cap = 1000;
static int g_bg2d_native_cols = 21;
static int g_bg2d_parent_links = 1;
static uint32_t g_bg2d_host_base, g_bg2d_host_size;
static uint32_t g_bg2d_host_generation;
void gpu_ws_bg2d_set_host_arena(uint32_t base, uint32_t size) {
g_bg2d_host_base = base & 0x1fffffffu;
g_bg2d_host_size = size >= 32u && !(base & 31u) ? size : 0u;
}
static uint32_t ws_bg2d_host_packet(void) {
uint32_t command = gp0_cmd_source_addr & 0x1fffffffu;
if (!g_bg2d_host_size || command < g_bg2d_host_base + 4u) return 0;
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;
}
#define WS_VIEW_LAYERS 8
static int ws_view_layer = -1;
static unsigned ws_view_independent_mask;
@@ -1237,11 +1254,22 @@ static WsViewAnchor ws_bg2d_layer_view(int layer) {
v.shift = origin - extra;
v.left = origin;
v.right = 2 * extra - origin;
} else if (g_bg2d_host_size && hi > lo && hi - lo < frame_width) {
int pad = frame_width - (hi - lo);
v.pad_left = pad / 2;
v.pad_right = pad - v.pad_left;
v.shift = sx - lo + v.pad_left - extra;
v.left = sx > lo ? sx - lo : 0;
v.right = hi > sx + width ? hi - sx - width : 0;
}
return v;
}
void gpu_ws_bg2d_begin_view_layer(unsigned layer, uint32_t packet, unsigned independent_mask) {
if (g_bg2d_host_size) {
ws_view_sample();
if (layer == 0) ++g_bg2d_host_generation;
}
if (!ws_view_enabled() || layer >= WS_VIEW_LAYERS) return;
ws_view_independent_mask = independent_mask;
ws_view_layer = (int)layer;
@@ -1249,6 +1277,11 @@ void gpu_ws_bg2d_begin_view_layer(unsigned layer, uint32_t packet, unsigned inde
ws_view_layers[layer].end = ws_view_layers[layer].begin;
ws_view_layers[layer].frame = (uint32_t)s_frame_count;
}
int gpu_ws_bg2d_get_view(unsigned layer, WsViewAnchor *view) {
if (!view || !ws_native_wide_active() || ws_disp_w() > 384) return 0;
*view = ws_bg2d_layer_view((int)layer);
return ws_disp_w();
}
void gpu_ws_bg2d_end_view_layer(unsigned layer, uint32_t packet) {
if (!ws_view_enabled() || layer >= WS_VIEW_LAYERS) return;
ws_view_layers[layer].end = packet & 0x1fffffffu;
@@ -1256,6 +1289,16 @@ void gpu_ws_bg2d_end_view_layer(unsigned layer, uint32_t packet) {
ws_view_layer = -1;
}
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);
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;
v.right = 2 * ws_nw_offset() - v.left - v.pad_left - v.pad_right;
return v;
}
if (!ws_view_enabled()) return ws_view_current();
uint32_t addr = gp0_cmd_source_addr & 0x1fffffffu;
for (unsigned i = 0; i < WS_VIEW_LAYERS; i++)
@@ -1284,6 +1327,7 @@ void gpu_ws_bg2d_set_parent_links(int on) {
}
static int ws_bg2d_left_cols(void) {
if (g_bg2d_host_size) return 0;
if (!ws_native_wide_active()) return 0;
/* Only the ~320 gameplay mode; the engine's 512 hi-res mode (title) draws
* its own 33 columns and centres itself — never double-shift it. */
@@ -1293,6 +1337,7 @@ static int ws_bg2d_left_cols(void) {
return (off + 15) / 16; /* ceil to whole tile columns */
}
static int ws_bg2d_right_cols(void) {
if (g_bg2d_host_size) return 0;
if (!ws_native_wide_active() || ws_disp_w() > 384) return 0;
return (ws_bg2d_layer_view(ws_view_layer).right + 15) / 16;
}
@@ -1507,6 +1552,7 @@ long gpu_ws_mmx6_refill_cols(void) { return g_mmx6_refill_cols; }
* No-op at 4:3 / 512 hi-res (left==0). Idempotent over the native columns the engine
* already streamed correctly, so re-streaming them is harmless. */
void psx_ws_mmx6_bg_refill_all(void) {
if (g_bg2d_host_size) return;
if (!g_mmx6_freshfix) return;
if (!ws_native_wide_active() || ws_disp_w() > 384) return;
if (ws_nw_offset() <= 0) return;
@@ -1924,6 +1970,7 @@ void gpu_ws_get_debug(GpuWsDebug* out) {
out->view_shift = view.shift;
out->view_pad_left = view.pad_left; out->view_pad_right = view.pad_right;
out->cur_frame = s_frame_count;
out->bg2d_generations = g_bg2d_host_generation;
out->last_tag_frame = ws_last_tag_stamp;
out->last_3d_frame = ws_last_3d_stamp;
out->gte_verts = ws_gte_prev_verts;
@@ -2519,6 +2566,7 @@ static int ws_nw_left_hud_packet(void) {
}
static int32_t ws_nw_hud_shift(int32_t x, int32_t w) {
if (!ws_native_wide_active()) return 0;
if (g_bg2d_host_size && ws_tagged_world_primitive()) return 0;
int32_t off = ws_nw_offset();
if (off <= 0) return 0;
int hud_edge = ws_tagged_hud_edge();
@@ -3669,7 +3717,9 @@ static int32_t sign_extend(uint32_t val, int bits) {
/* Parse a vertex position word: signed 11-bit X and Y */
static void parse_vertex(uint32_t word, int32_t* x, int32_t* y) {
*x = sign_extend(word & 0x7FFu, 11);
int extended = ws_bg2d_host_packet() || (g_bg2d_host_size &&
ws_native_wide_active() && ws_tagged_world_primitive());
*x = extended ? (int16_t)word : sign_extend(word & 0x7FFu, 11);
*y = sign_extend((word >> 16) & 0x7FFu, 11);
}
@@ -4668,8 +4718,11 @@ static void gp0_exec_textured_16x16(void) {
/* Never suppress MMX6 BG packets at a guessed finite-map boundary. The
* classifier cannot distinguish an authored layer entering the reveal from
* a stale ring slot; suppressing here caused the stage-start black flicker. */
int ws_w = ws_sprt_fixed_transform(&x0, y0, 16);
x0 += ws_nw_hud_shift(x0, 16); /* native-wide HUD corner re-anchor (no-op else) */
uint32_t host_packet = ws_bg2d_host_packet();
int host_tile = host_packet != 0;
if (host_tile && !ws_native_wide_active()) return;
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;
int u0 = gp0_cmd_buf[2] & 0xFF;
int v0 = (gp0_cmd_buf[2] >> 8) & 0xFF;
@@ -4682,7 +4735,10 @@ static void gp0_exec_textured_16x16(void) {
if (draw_area_out_rect(x0, y0, dw, 16)) return;
}
setup_textured_draw(color24, semi_trans, raw_texture);
if (ws_w && ws_w != 16)
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,
clut_x, clut_y, current_texpage());
else if (ws_w && ws_w != 16)
gr_draw_textured_rect_scaled(x0, y0, ws_w, 16, u0, v0, u0 + 16, v0 + 16,
clut_x, clut_y, current_texpage());
else
@@ -5608,16 +5664,21 @@ static void gp0_execute_command(void) {
gp0_ring_record(gp0_cmd_buf, gp0_words_needed);
extern void ws_bg_phase_note(uint32_t op);
ws_bg_phase_note(opcode); /* native-wide 2D-backdrop stretch: background-phase latch */
if (ws_view_camera_addr) {
if (ws_view_camera_addr || g_bg2d_host_size) {
WsViewAnchor v = ws_view_current();
/* Draw env packets select the back-buffer band after linked-list
* begin. Clear narrow-room padding at its first actual draw. */
static uint32_t clear_frame[512];
if (opcode >= 0x20 && opcode <= 0x7F && ws_view_enabled() &&
(v.pad_left || v.pad_right) && draw_area_top < 512 &&
clear_frame[draw_area_top] != (uint32_t)s_frame_count + 1u &&
static uint32_t clear_generation[512];
if (opcode >= 0x20 && opcode <= 0x7F &&
(ws_view_enabled() || (g_bg2d_host_size && ws_native_wide_active() && ws_disp_w() <= 384)) &&
(g_bg2d_host_size || v.pad_left || v.pad_right) && draw_area_top < 512 &&
(g_bg2d_host_size
? clear_generation[draw_area_top] != g_bg2d_host_generation
: clear_frame[draw_area_top] != (uint32_t)s_frame_count + 1u) &&
ws_is_fb_base(draw_area_left)) {
clear_frame[draw_area_top] = (uint32_t)s_frame_count + 1u;
clear_generation[draw_area_top] = g_bg2d_host_generation;
gr_wide_clear_margins((int)draw_area_left, (int)draw_area_top,
(int)draw_area_bottom - (int)draw_area_top + 1, 0, 3);
}
@@ -5625,7 +5686,9 @@ static void gp0_execute_command(void) {
int32_t hud_delta;
/* Existing HUD placement stays fixed in the physical frame. Only the
* wide world mirror receives the camera-edge origin and room clip. */
if (ws_nw_left_hud_packet() || ws_nw_explicit_hud_delta(&hud_delta))
if (ws_nw_left_hud_packet() ||
(!(g_bg2d_host_size && ws_tagged_world_primitive()) &&
ws_nw_explicit_hud_delta(&hud_delta)))
v.shift = v.pad_left = v.pad_right = 0;
gr_wide_set_view(ws_view_enabled(), v.shift, v.pad_left, v.pad_right);
}
+9
View File
@@ -71,6 +71,15 @@ int main(int argc,char **argv){
printf("driver=%s renderer=%s scale=%d\n",glGetString(GL_VERSION),glGetString(GL_RENDERER),scale);
glb_set_draw_area(0,0,1023,511);glb_set_mask_bits(0,0);glb_set_semi_transparency(0,0);glb_set_color_modulation(128,128,128,1);
verify("initial upload");
/* Adaptive backdrop reflection must retain every edge texel at both native
* and supersampled scales; the ordinary rect stays forward-facing. */
for(int x=0;x<16;++x) glb_vram_write(512+x,0,(uint16_t)(0x400+x+1));
glb_draw_textured_rect(700,200,16,1,0,0,0,0,0x108);
glb_draw_textured_rect_scaled(720,200,16,1,15,0,-1,1,0,0,0x108);
for(int x=0;x<16;++x) {
check(glb_vram_read(700+x,200)==0x401+x,"forward panorama texel");
check(glb_vram_read(720+x,200)==0x410-x,"reflected panorama texel");
}
glb_draw_flat_rect(1020,511,1,1,0x7fff);
check(glb_vram_read(1020,511)==0x7fff,"test pixel value");
GlCohEvent event;int found=0;
@@ -15,6 +15,16 @@ uint32_t g_debug_current_func_addr;
uint32_t g_debug_last_store_pc;
CPUState *debug_cpu_ptr;
int g_psx_vram_dirty_tracking;
/* This fixture uses captured draw calls, with no immutable texture banks. */
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; }
uint16_t mod_texture_packet_bank(uint32_t p, const uint32_t *words, uint32_t n) {
(void)p; (void)words; (void)n; return 0;
}
int mod_texture_packet_precision(uint32_t p, float q[3], float xy[6]) {
(void)p; (void)q; (void)xy; return 0;
}
static uint32_t test_ram[0x00200000u / 4u];
@@ -24,6 +34,7 @@ static struct {
int u, v;
uint16_t clut_x, clut_y, texpage;
} last_textured_rect;
static struct { int calls, x, y, w, h, u0, v0, u1, v1; } last_scaled_rect;
static uint32_t pack_vertex(int16_t x, int16_t y) {
return (uint16_t)x | ((uint32_t)(uint16_t)y << 16);
@@ -313,7 +324,11 @@ void gr_draw_textured_rect_scaled(int x, int y, int w, int h,
int u0, int v0, int u1, int v1,
uint16_t clut_x, uint16_t clut_y,
uint16_t texpage) {
(void)x; (void)y; (void)w; (void)h; (void)u0; (void)v0; (void)u1;
last_scaled_rect.calls++;
last_scaled_rect.x=x; last_scaled_rect.y=y;
last_scaled_rect.w=w; last_scaled_rect.h=h;
last_scaled_rect.u0=u0; last_scaled_rect.v0=v0;
last_scaled_rect.u1=u1; last_scaled_rect.v1=v1;
(void)v1; (void)clut_x; (void)clut_y; (void)texpage;
}
void gr_draw_line(int x0, int y0, int x1, int y1, uint16_t color) {
+13
View File
@@ -42,6 +42,19 @@ static void draw(int shift, int pad_left, int pad_right) {
}
int main(void) {
/* Same framing at 32:9 / 64:9 and beyond; finite rooms keep symmetric
* padding while a full stage transfers the reveal to the available side. */
const int extras[] = {267, 693, 1600};
for (unsigned i = 0; i < sizeof extras / sizeof extras[0]; ++i) {
int e = extras[i];
WsViewAnchor left = ws_view_anchor(e,0,0,5120);
WsViewAnchor right = ws_view_anchor(e,5120,0,5120);
WsViewAnchor room = ws_view_anchor(e,200,200,200);
assert(left.left==0 && left.right==2*e && left.shift==-e);
assert(right.left==2*e && right.right==0 && right.shift==e);
assert(room.left==0 && room.right==0 && room.shift==0);
assert(room.pad_left==e && room.pad_right==e);
}
check(0, 0, 1000, 0, 106, -53);
check(20, 0, 1000, 20, 86, -33);
check(500, 0, 1000, 53, 53, 0);
+37
View File
@@ -63,6 +63,43 @@ int main(void) {
ws_mode = 0;
assert(psx_ws_bg2d_cols(21) == 21);
assert(psx_ws_bg2d_startx(0) == 0);
/* The opt-in host arena keeps guest loops and the ring native, while
* its packet coordinates remain signed 16-bit beyond PS1's +/-1024. */
configure_native_wide_16_9();
gpu_ws_bg2d_set_host_arena(0x80100000u, 64u); /* mapped fixture memory */
half_at(0x10001c, 0x4247u); half_at(0x10001e, 0x5836u);
gp0_cmd_source_addr = 0x00100004u;
int32_t px, py;
parse_vertex(pack_vertex(1400, 20), &px, &py);
assert(px == 1400 && py == 20);
/* Reflection reverses the exact 16 texels, without reading the adjacent
* tile's first column. Its metadata is confined to the registered arena. */
half_at(0x10001e, 0xd836u);
gp0_cmd_buf[0]=0x7d808080u;
gp0_cmd_buf[1]=pack_vertex(400,20);
gp0_cmd_buf[2]=0x79804000u;
gp0_exec_textured_16x16();
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);
half_at(0x10001e, 0x5836u);
gp0_cmd_source_addr = 0x00010004u;
parse_vertex(pack_vertex(1400, 20), &px, &py);
assert(px == -648 && py == 20); /* Ordinary hardware packet unchanged. */
gpu_ws_tag_world_primitive(0x80010000u, 1);
parse_vertex(pack_vertex(1400, 20), &px, &py);
assert(px == 1400 && py == 20);
parse_vertex(pack_vertex(-1400, 20), &px, &py);
assert(px == -1400 && py == 20);
gpu_ws_tag_world_primitive(0x80010000u, 0);
parse_vertex(pack_vertex(1400, 20), &px, &py);
assert(px == -648 && py == 20);
assert(psx_ws_bg2d_cols(21) == 21);
assert(psx_ws_bg2d_startcol(3, 63) == 3);
assert(psx_ws_bg2d_startx(-5) == -5);
assert(psx_ws_bg2d_stream_left(100) == 100);
assert(psx_ws_bg2d_stream_right(436) == 436);
gpu_ws_bg2d_set_host_arena(0, 0);
puts("ws_view_anchor_gpu: sampled camera, per-layer coverage, packet origin, disabled identity PASS");
return 0;
}