diff --git a/docs/GPU_INTERLACED_RASTER.md b/docs/GPU_INTERLACED_RASTER.md new file mode 100644 index 00000000..5e33fcf9 --- /dev/null +++ b/docs/GPU_INTERLACED_RASTER.md @@ -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 -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. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 21ae85a5..299aa683 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -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) diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index c831f207..ecc8557d 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -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 */ diff --git a/runtime/include/gpu_interlace.h b/runtime/include/gpu_interlace.h new file mode 100644 index 00000000..6cc4be65 --- /dev/null +++ b/runtime/include/gpu_interlace.h @@ -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 diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 8d91f79a..ea960583 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -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. diff --git a/runtime/src/gpu_render.c b/runtime/src/gpu_render.c index c833f491..715bdda8 100644 --- a/runtime/src/gpu_render.c +++ b/runtime/src/gpu_render.c @@ -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 @@ -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); diff --git a/runtime/tests/test_gpu_interlace_render.c b/runtime/tests/test_gpu_interlace_render.c new file mode 100644 index 00000000..8c58ba08 --- /dev/null +++ b/runtime/tests/test_gpu_interlace_render.c @@ -0,0 +1,113 @@ +#include "gpu_render.h" +#include "gpu_interlace.h" +#include "gpu_sw_renderer.h" +#include +#include + +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; +}