Revert PR #327 interlaced row submission to restore retail BIOS boot

Matched Ape Escape retail SCPH1001 LLE builds identify 0661ce77 as the visible boot regression: parent 155e269b reaches game code, while 0661ce77 stalls near BIOS frame 375. Revert its four dependent follow-ups too; retain PR #326 readback bounds and PR #328 Vulkan aspect changes.

The row-by-row raster dispatcher amplifies an existing malformed DMA list into massive GL work. Keep the broader CPU/DMA arbitration experiment separate for compatibility testing. Document attribution, validation, and the temporarily restored interlace limitation.

Validation: full Ape target reaches title/menu and frame 11669 on retail LLE; OpenBIOS control reaches frame 3701. Eight focused tests pass; retained GL readback fixture passes 71 checks at both 1x and 4x after linking local SDL with iconv. Beads: beads-iisy; DMA follow-up: beads-eio.3.122.
This commit is contained in:
Matthew Stanley
2026-09-07 12:30:30 -07:00
parent 421f58ec8c
commit 24c6c56cc0
12 changed files with 108 additions and 365 deletions
-56
View File
@@ -1,56 +0,0 @@
# 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.
The facade counts each original perspective triangle once; repeated backend
metadata setup does not add diagnostic hits. 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.
Precise vertices use signed 16.16 positions. Row dispatch covers the union of
integer fallback bounds and precise vertex bounds, with one row for rounding.
Tests compare native and enhanced masked pixels with unmasked images when
precise positions cross the integer bounds, including negative coordinates.
A tiny precise triangle must submit three row clips rather than 256.
Flat overlay rectangles also submit one-row geometry while field masking is
active. This keeps the mask when a wide backend intentionally replaces its
scissor. Non-interlaced overlays retain the original geometry and policy.
A backend fixture that ignores the scissor tests both field parities, original
progressive behavior, and clip restoration. This is not Vulkan driver testing.
@@ -0,0 +1,84 @@
# Retail BIOS freeze: PR #327 rollback
Issue: `beads-iisy`. Investigation and validation: 2026-09-07.
## Confirmed introduction
The visible retail SCPH1001 boot regression starts at
`0661ce7773c3b82ea441ac9c82b0df08b4ea1563`, "Preserve active display field
during interlaced raster drawing", authored by Alexbeav in
[PR #327](https://github.com/mstan/psxrecomp/pull/327).
It reached master through [PR #334](https://github.com/mstan/psxrecomp/pull/334),
merge `421f58ec8c2a07f2cd608c1afa507404076e9882`.
Matched historical builds used unchanged runtime sources, the same generated
Ape Escape and SCPH1001 code, BIOS/disc, game config, and OpenGL 2x settings.
A minimal CMake wrapper adapted the current game's build declaration to the
older runtime helper. All runs explicitly set `PSX_BIOS_HLE=0`; logs confirmed
`bios_backend=LLE`, `bios_boot=LLE (real intro)`, and `image=SCPH-1001`.
| Runtime revision | Result |
| --- | --- |
| `47bda817`, parent of original DMA slicing | Game code; normal close at frame 3979 |
| `8af48ae9`, PR #277 DMA slicing | Game code; normal close at frame 2959 |
| `99f775c8`, PR #304 before word-cost change | Game code; normal close at frame 3468 |
| `bc88c06c`, PR #304 word-cost change | Game code; normal close at frame 3845 |
| `155e269b`, exact parent of `0661ce77` | Game code; normal close at frame 2192 |
| `0661ce77`, first PR #327 commit | Severe Sony-screen stall around frames 375-376 |
| `39ebb06f`, master immediately before #334 | Game code; normal close at frame 2652 |
| `421f58ec`, merged #334 | Same severe Sony-screen stall around frames 374-376 |
This rules out naming #277 or #304 as the demonstrated introduction of the
visible freeze merely because they introduced/refined asynchronous DMA.
## Mechanism and scope
`GR_RASTER_ROWS` resubmits a primitive once per permitted scanline, changing
the backend draw area each time. That multiplies backend submissions and
flushes in the retail BIOS's interlaced display mode.
A separate DMA defect makes this especially costly: the CPU can overwrite a
primitive header before the asynchronous DMA walker consumes it. On master,
BIOS memcpy at `BFC188D8` clears packet `00138CA8` before DMA node 1023 reads
its link, sending the walker into a malformed `0 -> 1A0000 -> 0` list. The
per-row renderer magnifies the resulting bogus primitives into massive GL
work. The apparent freeze can advance roughly a frame over many seconds;
headless progress alone misses the user-visible failure.
The immediate fix rolls back only #327's five dependent commits:
`0661ce77`, `43909718`, `7c4b0300`, `5aee9c83`, and `6fcfb8ad`.
PR #326's bounded GL readback and #328's Vulkan aspect-ratio changes remain.
DMA scheduling, BIOS code, generated code, and launcher settings are unchanged.
This restores the previous interlaced raster behavior, including its missing
active-field preservation. A replacement should avoid per-row backend draw
submission and validate real retail BIOS boot as well as field correctness.
CPU/DMA arbitration is a separate follow-up, `beads-eio.3.122`; its prototype
needs Vampire Hunter D/Spot Goes to Hollywood compatibility testing before
integration. Do not mix that wider timing change into this rollback.
## Rollback validation
The full Ape game target (SDL3, launcher compiled in, OpenGL, Vulkan compiled
in) passes retail intro/disc handoff, visibly reaches the title/menu, and
closes normally at frame 11669. The same rollback executable also passes the
OpenBIOS LLE control run and closes normally at frame 3701.
Use the owned SCPH1001 BIOS and Ape disc with:
```powershell
$env:PSX_BIOS_HLE = '0'
& .\ApeEscapeRecomp.exe --no-launcher --bios <SCPH1001.BIN> --disc <Ape.cue>
```
Ensure the game's `game.toml` is staged beside the validation executable.
Judge actual windowed boot, not just a headless frame threshold.
Passed: `gl_readback_runner_test`, `psx_cyc_batch_test`,
`dma_gpu_linked_list_timing_test`, `psx_cycle_event_boundaries_test`,
`ws_prepass_guard_test`, `gpu_primitive_reject_test`, `gpu_sw_edges_test`,
and `gpu_vk_upload_alignment_test`.
The real hidden-GL readback fixture passes `checks=71 failures=0` at both 1x
and 4x on NVIDIA 616.86. Its stock runner initially failed to link because
the local MSYS2 SDL static library depends on iconv; rerunning the recorded
link command with `-liconv` allowed both unmodified fixture runs to pass.
-9
View File
@@ -467,15 +467,6 @@ 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)
-1
View File
@@ -17,7 +17,6 @@ extern "C" {
void gpu_init(void);
uint32_t gpu_read_gpustat(void); /* 0x1F801814 read */
int gpu_raster_skipped_row(void); /* -1: all rows; otherwise preserved field parity */
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
@@ -1,16 +0,0 @@
#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
-2
View File
@@ -54,8 +54,6 @@ void gr_set_precise_triangle(int enabled,
* perspective_texturing). q[i] is the normalized 1/z homogeneous weight at
* vertex i. enabled == 0 restores the PS1's affine UV interpolation. */
void gr_set_perspective_triangle(int enabled, float q0, float q1, float q2);
/* Original perspective-qualified triangles since process startup. */
uint32_t gr_perspective_triangle_count(void);
/* Primitives */
void gr_fill_rect(int x, int y, int w, int h, uint16_t color);
+1
View File
@@ -34,6 +34,7 @@ void sw_set_precise_triangle(int enabled,
* homogeneous interpolation weights associated with each screen vertex. */
void sw_set_perspective_triangle(int enabled,
float q0, float q1, float q2);
uint32_t sw_perspective_triangle_count(void);
/* Texture filtering. 0 = nearest (native PSX look, default), 1 = bilinear
* (smooths textures/2D backgrounds; blends in RGB after the CLUT lookup,
+1 -10
View File
@@ -11,7 +11,6 @@
*/
#include "gpu.h"
#include "gpu_interlace.h"
#include "display_scanout.h"
#include "pgxp.h"
#include "mod_memory.h"
@@ -2733,14 +2732,6 @@ 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.
@@ -3362,7 +3353,7 @@ int gpu_texture_correction_enabled(void) {
}
uint32_t gpu_texture_correction_hits(void) {
return gr_perspective_triangle_count();
return sw_perspective_triangle_count();
}
/* Per-vertex precise positions (PGXP, docs/ENHANCEMENTS.md G1). Each of the three
+13 -92
View File
@@ -7,11 +7,10 @@
* gl_backend_get(); if that returns NULL (GL unavailable / init failed) we
* stay on software so a misconfigured [video] renderer never bricks boot.
*
* Primitive dispatch also applies the GPU's interlaced field mask through
* the backend clip, at native VRAM row granularity. */
* The software backend table points straight at the existing sw_* functions,
* so the software path is byte-for-byte unchanged. */
#include "gpu_render.h"
#include "gpu.h"
#include "gpu_sw_renderer.h"
#include <stdio.h>
@@ -65,11 +64,6 @@ 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) {
@@ -111,121 +105,48 @@ 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);
}
static uint32_t g_perspective_triangles;
uint32_t gr_perspective_triangle_count(void) { return g_perspective_triangles; }
void gr_set_perspective_triangle(int enabled, float q0, float q1, float q2) {
/* Count the original triangle, not per-row backend metadata re-arms. */
if (g_b->set_perspective_triangle && enabled && q0 > 0.0f && q1 > 0.0f && q2 > 0.0f)
++g_perspective_triangles;
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]);
}
/* Keep both integer fallback rows and enhanced fixed-point rows. The one-row
* margin covers edge rounding without submitting a full VRAM-height strip. */
static void gr_precise_row_bounds(int *lo, int *hi) {
for (int i = 1; i < 6; i += 2) {
int y = g_pc[i] / 65536;
int rem = g_pc[i] % 65536;
int floor_y = y - (rem < 0);
int ceil_y = y + (rem > 0);
*lo = gr_min(*lo, floor_y - 1);
*hi = gr_max(*hi, ceil_y + 1);
}
}
#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) gr_precise_row_bounds(&lo, &hi); \
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) {
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));
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) {
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));
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) {
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));
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) {
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));
}
/* Wide flat-overlay backends intentionally replace the clip with a full
* surface scissor. Submit row-height geometry too, so the field mask survives
* that path while ordinary full-screen overlays keep their existing policy. */
static void gr_draw_flat_rect_row(int x, int w, uint16_t c) {
int x1, y1, x2, y2;
g_b->get_draw_area(&x1, &y1, &x2, &y2);
g_b->draw_flat_rect(x, y1, w, 1, c);
}
void gr_draw_flat_rect(int x, int y, int w, int h, uint16_t c) {
if (gpu_raster_skipped_row() < 0) {
g_b->draw_flat_rect(x, y, w, h, c);
return;
}
GR_RASTER_ROWS(y, y + h - 1, gr_draw_flat_rect_row(x, w, c));
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) { 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) {
GR_RASTER_ROWS(y, y + h - 1, g_b->draw_textured_rect(x, y, w, h, u, v, clut_x, clut_y, texpage));
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) {
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));
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) { 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) {
GR_RASTER_ROWS(gr_min(y0, y1), gr_max(y0, y1), g_b->draw_shaded_line(x0, y0, c0, x1, y1, c1));
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);
+6
View File
@@ -56,6 +56,7 @@ static int g_precise_valid = 0;
static int32_t g_precise_x16[3], g_precise_y16[3];
static int g_perspective_valid = 0;
static float g_perspective_q[3];
static uint32_t g_perspective_triangles = 0;
/* ---- Native-wide compositor (separate present surfaces) ------------------
* Canonical VRAM stays faithful. For an opted-in wide game we ADDITIONALLY
@@ -521,6 +522,11 @@ void sw_set_perspective_triangle(int enabled,
g_perspective_q[0] = q0;
g_perspective_q[1] = q1;
g_perspective_q[2] = q2;
if (g_perspective_valid) g_perspective_triangles++;
}
uint32_t sw_perspective_triangle_count(void) {
return g_perspective_triangles;
}
static inline int precise_scaled(int axis, int vertex, int fallback, int scale) {
+3 -1
View File
@@ -2771,7 +2771,9 @@ static void vkb_set_precise_triangle(int enabled,
static void vkb_set_perspective_triangle(int enabled, float q0, float q1, float q2) {
s_pq_valid = (enabled && q0 > 0.0f && q1 > 0.0f && q2 > 0.0f) ? 1 : 0;
s_pq[0] = q0; s_pq[1] = q1; s_pq[2] = q2;
/* Keep fallback software metadata in sync; the facade counts triangles. */
/* The corrected-triangle tally lives in the software rasterizer and backs
* gpu_texture_correction_hits() — the "is this doing anything on this
* title" counter. Forward so it reads the same on every backend. */
sw_set_perspective_triangle(enabled, q0, q1, q2);
}
/* The override describes exactly one triangle; drop it once submitted so a
-178
View File
@@ -1,178 +0,0 @@
#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], reference_hires[32*4*32*4];
static uint16_t reference_vram[32*32];
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. */
}
/* Wide overlay paths can deliberately ignore the draw-area scissor. */
static unsigned overlay_rows[32];
static void mock_flat_overlay(int x,int y,int w,int h,uint16_t c) {
(void)x;(void)w;(void)c;
for (int row=y; row<y+h && row<32; ++row)
if (row>=0) ++overlay_rows[row];
}
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,.draw_flat_rect=mock_flat_overlay
};
/* 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));
}
}
}
/* Enhanced vertices can lie above/below integer fallback vertices.
* Compare each masked surface with its own unmasked pixel oracle. */
for (int shift = -10; shift <= 10; shift += 20) {
reset_canvas();
gr_set_precise_triangle(1,2*65536,(8+shift)*65536+16384,
10*65536,(8+shift)*65536+16384,2*65536,(16+shift)*65536+16384);
gr_draw_flat_triangle(2,8,10,8,2,16,0x7FFF);
for (int y=0; y<32; ++y)
memcpy(reference_vram+y*32,vram+y*1024,32*sizeof(uint16_t));
gr_render_display_hires(reference_hires,32*scale*4,0,0,32,32);
for (int skip=0; skip<=1; ++skip) {
reset_canvas(); skipped_row=skip;
gr_set_precise_triangle(1,2*65536,(8+shift)*65536+16384,
10*65536,(8+shift)*65536+16384,2*65536,(16+shift)*65536+16384);
gr_draw_flat_triangle(2,8,10,8,2,16,0x7FFF);
for (int y=0; y<32; ++y) for (int x=0; x<32; ++x)
CHECK(vram[y*1024+x] == ((y&1)==skip ? 0 : reference_vram[y*32+x]));
gr_render_display_hires(hires,32*scale*4,0,0,32,32);
for (int y=0; y<32*scale; ++y) for (int x=0; x<32*scale; ++x)
CHECK((hires[y*32*scale+x]&0xFFFFFF) == (((y/scale)&1)==skip ? 0 : (reference_hires[y*32*scale+x]&0xFFFFFF)));
}
}
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);
}
/* One logical perspective triangle stays one hit despite row re-arms. */
for (int skip = -1; skip <= 1; ++skip) {
for (int clipped = 0; clipped <= 1; ++clipped) {
reset_canvas(); skipped_row = skip;
if (clipped) gr_set_draw_area(20,20,21,21);
uint32_t before = gr_perspective_triangle_count();
gr_set_perspective_triangle(1,1.0f,0.5f,0.25f);
gr_draw_textured_triangle(2,2,0,0,10,2,8,0,2,10,0,8,0,0,0x104);
CHECK(gr_perspective_triangle_count() == before + 1);
gr_set_perspective_triangle(1,0.5f,0.25f,0.125f);
gr_draw_textured_triangle(2,2,0,0,10,2,8,0,2,10,0,8,0,0,0x104);
CHECK(gr_perspective_triangle_count() == before + 2);
gr_set_perspective_triangle(0,1.0f,1.0f,1.0f);
gr_set_perspective_triangle(1,0.0f,1.0f,1.0f);
gr_draw_textured_triangle(2,2,0,0,10,2,8,0,2,10,0,8,0,0,0x104);
CHECK(gr_perspective_triangle_count() == before + 2);
}
}
/* 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==6 && mock_bad==0 && !mock_pc && !mock_pq);
mock_submissions=0;
gr_set_draw_area(0,0,1023,511);
gr_set_precise_triangle(1,0,8*65536+16384,65536,8*65536+16384,0,9*65536+16384);
gr_set_perspective_triangle(1,1.0f,1.0f,1.0f);
gr_draw_flat_triangle(0,8,1,8,0,9,0x7FFF);
CHECK(mock_submissions==3 && mock_bad==0 && !mock_pc && !mock_pq);
for (int skip=-1; skip<=1; ++skip) {
memset(overlay_rows,0,sizeof overlay_rows); skipped_row=skip;
gr_set_draw_area(0,4,31,15);
gr_draw_flat_rect(0,0,32,20,0x7FFF);
for (int y=0; y<32; ++y) {
unsigned expected = skip<0 ? y<20 : (y>=4 && y<=15 && (y&1)!=skip);
CHECK(overlay_rows[y] == expected);
}
int x1,y1,x2,y2; gr_get_draw_area(&x1,&y1,&x2,&y2);
CHECK(x1==0 && y1==4 && x2==31 && y2==15);
}
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;
}