Preserve active display field during interlaced raster drawing

This commit is contained in:
Alexandros Mandravillis
2026-09-06 13:13:03 +03:00
parent 155e269ba5
commit 0661ce7773
7 changed files with 254 additions and 14 deletions
+42
View File
@@ -0,0 +1,42 @@
# Interlaced primitive drawing
The GPU preserves one row parity while drawing in 480-line interlaced mode
when GP0(E1h) prohibits drawing to the display area. The display origin and
current field select that parity. Progressive modes and the draw-to-display
override draw both parities. Fills, copies and VRAM transfers keep both fields.
`gpu_raster_skipped_row()` reads current GPU state for each primitive. The
renderer facade applies the field mask with native-row clip rectangles and
restores the original clip. This also preserves the corresponding rows in
scaled surfaces. Each clipped triangle retains its precision and perspective
metadata, which hardware backends otherwise consume on the first submission.
No title address or title detection selects this behavior.
Pro Pinball: Timeshock! USA SLUS-00639 repeatedly draws and reads a test pixel
at (1020,511) during startup. Frozen source 63446a28 always returned the drawn
pixel and stayed black. This correction lets the guest's existing field test
finish. The same disc, BIOS and settings then reach the table.
The field latch uses the runtime's existing vertical-blank model. This change
does not claim scanline-accurate timing. Row clipping can increase submissions
for large interlaced primitives; no general performance claim is made.
## Verification
Configure `runtime/` with `BUILD_TESTING=ON`, then run
`ctest --test-dir <build> -R gpu_interlace_render_test --output-on-failure`.
The fixture covers the mode predicate, origin/field parity, all nine primitive
families, clip restoration, transfers, mode changes, scaled output and
triangle metadata consumed by a backend. The original renderer fails the
same row-preservation assertions.
## Prior art
- [PSX-SPX GPU specification](https://psx-spx.consoledev.net/graphicsprocessingunitgpu/)
describes interlace affecting draw commands.
- [DuckStation difficult games](https://github.com/stenzek/duckstation/wiki/Difficult-to-Emulate-Games)
identifies required line skipping for Pro Pinball startup.
These sources supplied behavioral evidence. The implementation and regression
fixture were written independently. Exact title run receipts and route limits
are maintained by the Timeshock correction project.
+9
View File
@@ -440,6 +440,15 @@ if(BUILD_TESTING)
target_include_directories(gpu_sw_edges_test PRIVATE src)
add_test(NAME gpu_sw_edges_test COMMAND gpu_sw_edges_test)
add_executable(gpu_interlace_render_test
tests/test_gpu_interlace_render.c src/gpu_render.c
src/gpu_sw_renderer.c src/gpu_vram_dirty.c)
target_include_directories(gpu_interlace_render_test PRIVATE include)
if(NOT MSVC)
target_link_libraries(gpu_interlace_render_test PRIVATE m)
endif()
add_test(NAME gpu_interlace_render_test COMMAND gpu_interlace_render_test)
add_executable(gpu_vk_upload_alignment_test
tests/test_gpu_vk_upload_alignment.c)
target_include_directories(gpu_vk_upload_alignment_test PRIVATE src)
+2 -1
View File
@@ -16,7 +16,8 @@ extern "C" {
#endif
void gpu_init(void);
uint32_t gpu_read_gpustat(void); /* 0x1F801814 read */
uint32_t gpu_read_gpustat(void);
int gpu_raster_skipped_row(void); /* 0x1F801814 read */
uint32_t gpu_read_gpuread(void); /* 0x1F801810 read */
void gpu_write_gp0(uint32_t val); /* 0x1F801810 write */
void gpu_write_gp1(uint32_t val); /* 0x1F801814 write */
+16
View File
@@ -0,0 +1,16 @@
#ifndef PSX_GPU_INTERLACE_H
#define PSX_GPU_INTERLACE_H
/* GP0 raster primitives skip the active field only in 480i mode with
* drawing-to-display prohibited. VRAM transfer commands do not use this rule.
* Return -1 for progressive drawing, otherwise the native VRAM row parity
* that must remain unchanged. The display origin participates in that parity. */
static inline int psx_gpu_raster_skipped_row(unsigned interlace,
unsigned high_vertical_resolution, unsigned draw_to_display,
unsigned display_y, unsigned field) {
if (!interlace || !high_vertical_resolution || draw_to_display)
return -1;
return (int)((display_y + field) & 1u);
}
#endif
+9
View File
@@ -11,6 +11,7 @@
*/
#include "gpu.h"
#include "gpu_interlace.h"
#include "display_scanout.h"
#include "pgxp.h"
#include "mod_memory.h"
@@ -2732,6 +2733,14 @@ void gpu_init(void) {
/* ---- GPUSTAT read (0x1F801814) ---- */
/* Primitive rasterization must leave the active display field untouched in
* 480-line interlaced mode unless GP0(E1h).10 permits drawing to it.
* Transfers, fills, and copies are not raster primitives. */
int gpu_raster_skipped_row(void) {
return psx_gpu_raster_skipped_row(vertical_interlace, vres,
draw_to_display, display_area_y, lcf);
}
uint32_t gpu_read_gpustat(void) {
/* Advance vblank when polled enough times from within a single function.
* This handles BIOS VSYNC wait loops that poll LCF in tight loops.
+63 -13
View File
@@ -7,10 +7,11 @@
* gl_backend_get(); if that returns NULL (GL unavailable / init failed) we
* stay on software so a misconfigured [video] renderer never bricks boot.
*
* The software backend table points straight at the existing sw_* functions,
* so the software path is byte-for-byte unchanged. */
* Primitive dispatch also applies the GPU's interlaced field mask through
* the backend clip, at native VRAM row granularity. */
#include "gpu_render.h"
#include "gpu.h"
#include "gpu_sw_renderer.h"
#include <stdio.h>
@@ -64,6 +65,11 @@ extern const GpuRenderBackend *vk_backend_get(void);
static const GpuRenderBackend *g_b = &SW_BACKEND;
static GrBackend g_effective = GR_BACKEND_SOFTWARE;
/* Backends consume triangle metadata on submission. Re-arm it for each
* clipped row belonging to the same interlaced primitive. */
static int g_pc_enabled, g_pq_enabled;
static int32_t g_pc[6];
static float g_pq[3];
void gr_set_backend(GrBackend backend) {
if (backend == GR_BACKEND_OPENGL) {
@@ -105,48 +111,92 @@ void gr_set_texture_window(uint32_t raw) { g_b->set_texture_window(r
void gr_set_color_modulation(int r, int g, int b, int raw) { g_b->set_color_modulation(r, g, b, raw); }
void gr_set_precise_triangle(int enabled, int32_t x0, int32_t y0, int32_t x1, int32_t y1,
int32_t x2, int32_t y2) {
g_pc_enabled = enabled;
g_pc[0]=x0; g_pc[1]=y0; g_pc[2]=x1; g_pc[3]=y1; g_pc[4]=x2; g_pc[5]=y2;
if (g_b->set_precise_triangle)
g_b->set_precise_triangle(enabled, x0, y0, x1, y1, x2, y2);
}
void gr_set_perspective_triangle(int enabled, float q0, float q1, float q2) {
g_pq_enabled=enabled; g_pq[0]=q0; g_pq[1]=q1; g_pq[2]=q2;
if (g_b->set_perspective_triangle)
g_b->set_perspective_triangle(enabled, q0, q1, q2);
}
void gr_fill_rect(int x, int y, int w, int h, uint16_t c) { g_b->fill_rect(x, y, w, h, c); }
void gr_copy_rect(int sx, int sy, int dx, int dy, int w, int h) { g_b->copy_rect(sx, sy, dx, dy, w, h); }
/* Apply field masking through each backend's native-coordinate clip. This
* preserves the same rows in canonical VRAM and supersampled surfaces, and
* leaves non-raster transfers untouched. Bounds are already offset by gpu.c.
* Query live GPU state per primitive so reset/restore/mode changes cannot
* leave a stale renderer-side field latch. */
static int gr_min(int a, int b) { return a < b ? a : b; }
static int gr_max(int a, int b) { return a > b ? a : b; }
static void gr_rearm_triangle(void) {
if (g_b->set_precise_triangle)
g_b->set_precise_triangle(g_pc_enabled,g_pc[0],g_pc[1],g_pc[2],g_pc[3],g_pc[4],g_pc[5]);
if (g_b->set_perspective_triangle)
g_b->set_perspective_triangle(g_pq_enabled,g_pq[0],g_pq[1],g_pq[2]);
}
#define GR_TRIANGLE_ROWS(ylo, yhi, draw_call) do { \
/* Precise positions can cross the integer primitive bounds. */ \
int lo = (ylo), hi = (yhi); \
if (g_pc_enabled) { lo = 0; hi = 511; } \
GR_RASTER_ROWS(lo, hi, (gr_rearm_triangle(), draw_call)); \
g_pc_enabled = g_pq_enabled = 0; \
gr_rearm_triangle(); \
} while (0)
#define GR_RASTER_ROWS(ylo, yhi, draw_call) do { \
int skip = gpu_raster_skipped_row(); \
if (skip < 0) { draw_call; } else { \
int cx1, cy1, cx2, cy2; \
g_b->get_draw_area(&cx1, &cy1, &cx2, &cy2); \
int first = gr_max(gr_max((ylo), cy1), 0); \
int last = gr_min(gr_min((yhi), cy2), 511); \
for (int row = first; row <= last; ++row) { \
if ((row & 1) == skip) continue; \
g_b->set_draw_area(cx1, row, cx2, row); \
draw_call; \
} \
g_b->set_draw_area(cx1, cy1, cx2, cy2); \
} \
} while (0)
void gr_draw_flat_triangle(int x0, int y0, int x1, int y1, int x2, int y2, uint16_t c) {
g_b->draw_flat_triangle(x0, y0, x1, y1, x2, y2, c);
GR_TRIANGLE_ROWS(gr_min(y0, gr_min(y1, y2)), gr_max(y0, gr_max(y1, y2)), g_b->draw_flat_triangle(x0, y0, x1, y1, x2, y2, c));
}
void gr_draw_gouraud_triangle(int x0, int y0, uint16_t c0, int x1, int y1, uint16_t c1,
int x2, int y2, uint16_t c2) {
g_b->draw_gouraud_triangle(x0, y0, c0, x1, y1, c1, x2, y2, c2);
GR_TRIANGLE_ROWS(gr_min(y0, gr_min(y1, y2)), gr_max(y0, gr_max(y1, y2)), g_b->draw_gouraud_triangle(x0, y0, c0, x1, y1, c1, x2, y2, c2));
}
void gr_draw_textured_triangle(int x0, int y0, int u0, int v0, int x1, int y1, int u1, int v1,
int x2, int y2, int u2, int v2,
uint16_t clut_x, uint16_t clut_y, uint16_t texpage) {
g_b->draw_textured_triangle(x0, y0, u0, v0, x1, y1, u1, v1, x2, y2, u2, v2,
clut_x, clut_y, texpage);
GR_TRIANGLE_ROWS(gr_min(y0, gr_min(y1, y2)), gr_max(y0, gr_max(y1, y2)), g_b->draw_textured_triangle(x0, y0, u0, v0, x1, y1, u1, v1, x2, y2, u2, v2,
clut_x, clut_y, texpage));
}
void gr_draw_shaded_textured_triangle(int x0, int y0, int u0, int v0, uint32_t c0,
int x1, int y1, int u1, int v1, uint32_t c1,
int x2, int y2, int u2, int v2, uint32_t c2,
uint16_t clut_x, uint16_t clut_y,
uint16_t texpage, int raw) {
g_b->draw_shaded_textured_triangle(x0, y0, u0, v0, c0, x1, y1, u1, v1, c1,
x2, y2, u2, v2, c2, clut_x, clut_y, texpage, raw);
GR_TRIANGLE_ROWS(gr_min(y0, gr_min(y1, y2)), gr_max(y0, gr_max(y1, y2)), g_b->draw_shaded_textured_triangle(x0, y0, u0, v0, c0, x1, y1, u1, v1, c1,
x2, y2, u2, v2, c2, clut_x, clut_y, texpage, raw));
}
void gr_draw_flat_rect(int x, int y, int w, int h, uint16_t c) {
GR_RASTER_ROWS(y, y + h - 1, g_b->draw_flat_rect(x, y, w, h, c));
}
void gr_draw_flat_rect(int x, int y, int w, int h, uint16_t c) { g_b->draw_flat_rect(x, y, w, h, c); }
void gr_draw_textured_rect(int x, int y, int w, int h, int u, int v,
uint16_t clut_x, uint16_t clut_y, uint16_t texpage) {
g_b->draw_textured_rect(x, y, w, h, u, v, clut_x, clut_y, texpage);
GR_RASTER_ROWS(y, y + h - 1, g_b->draw_textured_rect(x, y, w, h, u, v, clut_x, clut_y, texpage));
}
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) {
g_b->draw_textured_rect_scaled(x, y, w, h, u0, v0, u1, v1, clut_x, clut_y, texpage);
GR_RASTER_ROWS(y, y + h - 1, g_b->draw_textured_rect_scaled(x, y, w, h, u0, v0, u1, v1, clut_x, clut_y, texpage));
}
void gr_draw_line(int x0, int y0, int x1, int y1, uint16_t c) {
GR_RASTER_ROWS(gr_min(y0, y1), gr_max(y0, y1), g_b->draw_line(x0, y0, x1, y1, c));
}
void gr_draw_line(int x0, int y0, int x1, int y1, uint16_t c) { g_b->draw_line(x0, y0, x1, y1, c); }
void gr_draw_shaded_line(int x0, int y0, uint16_t c0, int x1, int y1, uint16_t c1) {
g_b->draw_shaded_line(x0, y0, c0, x1, y1, c1);
GR_RASTER_ROWS(gr_min(y0, y1), gr_max(y0, y1), g_b->draw_shaded_line(x0, y0, c0, x1, y1, c1));
}
int gr_render_display(uint32_t *o, int p, int dx, int dy, int dw, int dh) {
return g_b->render_display(o, p, dx, dy, dw, dh);
+113
View File
@@ -0,0 +1,113 @@
#include "gpu_render.h"
#include "gpu_interlace.h"
#include "gpu_sw_renderer.h"
#include <stdio.h>
#include <string.h>
static uint16_t vram[1024 * 512];
static uint32_t hires[32*4*32*4];
static int skipped_row;
static int failures;
static int mock_enabled, mock_pc, mock_pq, mock_submissions, mock_bad;
static void mock_precise(int e,int32_t x0,int32_t y0,int32_t x1,int32_t y1,int32_t x2,int32_t y2) {
(void)x0;(void)y0;(void)x1;(void)y1;(void)x2;(void)y2;mock_pc=e;
}
static void mock_perspective(int e,float q0,float q1,float q2) {
(void)q0;(void)q1;(void)q2;mock_pq=e;
}
static void mock_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t c) {
(void)x0;(void)y0;(void)x1;(void)y1;(void)x2;(void)y2;(void)c;
++mock_submissions;if(!mock_pc || !mock_pq)++mock_bad;
mock_pc=mock_pq=0; /* GPU backends consume these per submission. */
}
static const GpuRenderBackend mock_backend={
.name="metadata fixture",.get_draw_area=sw_get_draw_area,.set_draw_area=sw_set_draw_area,
.set_precise_triangle=mock_precise,.set_perspective_triangle=mock_perspective,
.draw_flat_triangle=mock_triangle
};
/* This fixture keeps optional widescreen presentation disabled. */
int g_ws_bd_stretch_pct, g_ws_bd_stretch_on;
int psx_ws_prim_in_backdrop(void) { return 0; }
int gpu_raster_skipped_row(void) { return skipped_row; }
const GpuRenderBackend *gl_backend_get(void) { return mock_enabled ? &mock_backend : NULL; }
const GpuRenderBackend *vk_backend_get(void) { return NULL; }
#define CHECK(c) do { if (!(c)) { \
fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #c); ++failures; \
} } while (0)
static void reset_canvas(void) {
skipped_row = -1;
gr_fill_rect(0, 0, 32, 32, 0);
gr_set_draw_area(0, 0, 31, 31);
gr_set_semi_transparency(0, 0);
gr_set_mask_bits(0, 0);
gr_set_color_modulation(128, 128, 128, 1);
}
static void draw_kind(int kind) {
switch (kind) {
case 0: gr_draw_flat_rect(2, 2, 8, 8, 0x7FFF); break;
case 1: gr_draw_flat_triangle(2, 2, 10, 2, 2, 10, 0x7FFF); break;
case 2: gr_draw_gouraud_triangle(2, 2, 0x7FFF, 10, 2, 0x7FFF, 2, 10, 0x7FFF); break;
case 3: gr_draw_line(2, 2, 2, 9, 0x7FFF); break;
case 4: gr_draw_shaded_line(2, 2, 0x7FFF, 2, 9, 0x7FFF); break;
case 5: gr_draw_textured_rect(2, 2, 8, 8, 0, 0, 0, 0, 0x104); break;
case 6: gr_draw_textured_rect_scaled(2, 2, 8, 8, 0, 0, 8, 8, 0, 0, 0x104); break;
case 7: gr_draw_textured_triangle(2, 2, 0, 0, 10, 2, 8, 0, 2, 10, 0, 8, 0, 0, 0x104); break;
case 8: gr_draw_shaded_textured_triangle(2, 2, 0, 0, 0x808080, 10, 2, 8, 0, 0x808080,
2, 10, 0, 8, 0x808080, 0, 0, 0x104, 1); break;
}
}
int main(void) {
for (unsigned bits = 0; bits < 32; ++bits) {
unsigned i=bits&1, h=(bits>>1)&1, allow=(bits>>2)&1;
unsigned y=(bits>>3)&1, field=(bits>>4)&1;
CHECK(psx_gpu_raster_skipped_row(i,h,allow,y,field) ==
((i && h && !allow) ? (int)(y ^ field) : -1));
}
for (int scale = 1; scale <= 4; scale *= 4) {
gr_init(vram); gr_set_scale(scale);
gr_fill_rect(256, 0, 16, 16, 0x7FFF);
for (int kind = 0; kind < 9; ++kind) {
for (int skip = -1; skip <= 1; ++skip) {
reset_canvas(); skipped_row=skip; draw_kind(kind);
for (int y=2; y<9; ++y) {
CHECK((vram[y*1024+2] != 0) == (skip < 0 || (y&1) != skip));
}
int x1,y1,x2,y2; gr_get_draw_area(&x1,&y1,&x2,&y2);
CHECK(x1==0 && y1==0 && x2==31 && y2==31);
if (kind == 0) {
gr_render_display_hires(hires,32*scale*4,0,0,32,32);
for (int y=2*scale;y<9*scale;++y)
CHECK(((hires[y*32*scale+3*scale]&0xFFFFFF)!=0) ==
(skip<0 || ((y/scale)&1)!=skip));
}
}
}
reset_canvas(); skipped_row=1;
gr_set_draw_area(3,3,8,8); gr_draw_flat_rect(0,0,10,10,0x7FFF);
CHECK(vram[4*1024+3]==0x7FFF && vram[3*1024+3]==0 && vram[4*1024+2]==0);
/* Fill/copy/upload/readback must retain BOTH fields. */
gr_fill_rect(0,0,16,16,0x1234);
gr_copy_rect(0,0,16,0,16,16);
uint16_t in[4]={1,2,3,4},out[4]={0};
gr_vram_transfer_in(0,0,2,2,in); gr_vram_transfer_out(0,0,2,2,out);
CHECK(memcmp(in,out,sizeof in)==0 && vram[17]==0x1234 && vram[1024+17]==0x1234);
/* A mode switch restores drawing to both fields immediately. */
reset_canvas(); skipped_row=0; gr_draw_flat_rect(2,2,1,2,0x7FFF);
skipped_row=-1; gr_draw_flat_rect(2,2,1,2,0x4321);
CHECK(vram[2*1024+2]==0x4321 && vram[3*1024+2]==0x4321);
}
/* A single enhanced triangle keeps its metadata through all row clips. */
mock_enabled=1;gr_set_backend(GR_BACKEND_OPENGL);skipped_row=0;
gr_set_draw_area(0,0,31,31);
gr_set_precise_triangle(1,0,0,10*65536,0,0,10*65536);
gr_set_perspective_triangle(1,1.0f,0.5f,0.25f);
gr_draw_flat_triangle(0,0,10,0,0,10,0x7FFF);
CHECK(mock_submissions==16 && mock_bad==0 && !mock_pc && !mock_pq);
gr_set_backend(GR_BACKEND_SOFTWARE);
if (failures) return 1;
puts("PASS: field predicate, all raster primitives, clipping, transfers, mode switch, scales 1/4");
return 0;
}