mirror of
https://github.com/ARMSX2/ARMSX1.git
synced 2026-08-24 16:53:35 -07:00
2489 lines
98 KiB
C
2489 lines
98 KiB
C
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "../psx/dev/gpu.h"
|
|
#include "../psx/dev/gpu_backend.h"
|
|
#include "../psx/perf.h"
|
|
#include "../frontend/gpu_hw.h"
|
|
#include "../frontend/gpu_hw_rt.h"
|
|
|
|
void log_log(int level, const char* file, int line, const char* format, ...) {
|
|
(void)level;
|
|
(void)file;
|
|
(void)line;
|
|
(void)format;
|
|
}
|
|
|
|
void psx_ic_irq(psx_ic_t* ic, int id) {
|
|
(void)ic;
|
|
(void)id;
|
|
}
|
|
|
|
/* psx/dev/gpu.c carries save-state code that pulls in psx/state.c, which in turn wants
|
|
every other device in the machine. None of it is exercised here, so it is stubbed
|
|
rather than linked. */
|
|
void psx_sw_u8(psx_state_writer_t* w, uint8_t v) { (void)w; (void)v; }
|
|
void psx_sw_u16(psx_state_writer_t* w, uint16_t v) { (void)w; (void)v; }
|
|
void psx_sw_u32(psx_state_writer_t* w, uint32_t v) { (void)w; (void)v; }
|
|
void psx_sw_i32(psx_state_writer_t* w, int32_t v) { (void)w; (void)v; }
|
|
void psx_sw_f32(psx_state_writer_t* w, float v) { (void)w; (void)v; }
|
|
void psx_sw_u16_array(psx_state_writer_t* w, const uint16_t* v, size_t n) { (void)w; (void)v; (void)n; }
|
|
uint8_t psx_sr_u8(psx_state_reader_t* r) { (void)r; return 0; }
|
|
uint16_t psx_sr_u16(psx_state_reader_t* r) { (void)r; return 0; }
|
|
uint32_t psx_sr_u32(psx_state_reader_t* r) { (void)r; return 0; }
|
|
int32_t psx_sr_i32(psx_state_reader_t* r) { (void)r; return 0; }
|
|
float psx_sr_f32(psx_state_reader_t* r) { (void)r; return 0.0f; }
|
|
void psx_sr_u16_array(psx_state_reader_t* r, uint16_t* v, size_t n) { (void)r; (void)v; (void)n; }
|
|
|
|
/* Applied to every psx_gpu_t the corpus builds, so the mask-bit and dither-gate fixes are
|
|
exercised in BOTH states: off (the historical behaviour, and the default) and on. */
|
|
static uint32_t g_test_accuracy = 0;
|
|
|
|
/* OR-ed into GPUSTAT immediately before every draw. With g_test_accuracy == 0 (the
|
|
shipping default) GPUSTAT bits 9, 11 and 12 must have NO observable effect, so rendering
|
|
the corpus with these bits forced on and forced off has to produce identical VRAM. That
|
|
is the regression guard for "the software rasterizer stays untouched by default". */
|
|
static uint32_t g_test_force_gpustat = 0;
|
|
|
|
static psx_gpu_t* make_gpu(void) {
|
|
psx_gpu_t* gpu = psx_gpu_create();
|
|
if (!gpu)
|
|
return NULL;
|
|
psx_gpu_init(gpu, NULL);
|
|
gpu->draw_x1 = 0;
|
|
gpu->draw_y1 = 0;
|
|
gpu->draw_x2 = PSX_GPU_FB_WIDTH - 1;
|
|
gpu->draw_y2 = PSX_GPU_FB_HEIGHT - 1;
|
|
psx_gpu_set_accuracy_flags(gpu, g_test_accuracy);
|
|
return gpu;
|
|
}
|
|
|
|
static int run_case(const char* name, vertex_t a, vertex_t b, vertex_t c, poly_data_t data, int edge) {
|
|
psx_gpu_t* reference = make_gpu();
|
|
psx_gpu_t* accelerated = make_gpu();
|
|
if (!reference || !accelerated) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
for (size_t index = 0; index < PSX_GPU_VRAM_SIZE / sizeof(uint16_t); ++index) {
|
|
const uint16_t value = (uint16_t)((index * 1103515245u + 12345u) >> 16);
|
|
reference->vram[index] = value;
|
|
accelerated->vram[index] = value;
|
|
}
|
|
|
|
gpu_render_triangle(reference, a, b, c, data, edge);
|
|
gpu_hw_render_triangle(accelerated, a, b, c, data, edge);
|
|
|
|
const int mismatch = memcmp(reference->vram, accelerated->vram, PSX_GPU_VRAM_SIZE) != 0;
|
|
psx_gpu_destroy(reference);
|
|
psx_gpu_destroy(accelerated);
|
|
if (mismatch) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=vram-mismatch\n", name);
|
|
return 1;
|
|
}
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------------------
|
|
Internal-resolution backend gate (frontend/gpu_hw_rt.c).
|
|
|
|
Two properties are checked, and together they are what makes upscaling trustworthy:
|
|
|
|
1x parity at internal_scale = 1 the backend's render target must be
|
|
BYTE-IDENTICAL to the software rasterizer's VRAM. If it is not, the
|
|
coordinate math is wrong and every higher multiplier is wrong too.
|
|
|
|
scale coherence sampling the internal_scale = N render target at (x*N, y*N) must
|
|
reproduce the 1x render target exactly. The edge functions scale by
|
|
N^2, so coverage, the top-left fill rule, barycentric ratios, UVs and
|
|
the native-indexed dither are all supposed to be scale-invariant at
|
|
those sample points. This catches the half-pixel class of bug that
|
|
the backend warns is invisible at 1x.
|
|
------------------------------------------------------------------------------------ */
|
|
|
|
static void seed_vram(psx_gpu_t* gpu) {
|
|
for (size_t index = 0; index < PSX_GPU_VRAM_SIZE / sizeof(uint16_t); ++index)
|
|
gpu->vram[index] = (uint16_t)((index * 1103515245u + 12345u) >> 16);
|
|
}
|
|
|
|
/* Texture page parked at VRAM (512, 0), CLUT at (0, 400). Encoded the way gpu.c:254-258
|
|
decodes it: texp[3:0] = x/64, texp[4] = y/256, texp[6:5] = blend mode, texp[8:7] = depth.
|
|
clut[5:0] = x/16, clut[14:6] = y. */
|
|
#define TEXP_AT_512(depth, mode) \
|
|
((uint16_t)(8u | ((uint32_t)(mode) << 5) | ((uint32_t)(depth) << 7)))
|
|
#define CLUT_AT_0_400 ((uint16_t)(400u << 6))
|
|
|
|
/* Sprites read the texture page from the latched gpu->texp_* registers (gpu.c:501-503),
|
|
not from a command word, so they have to be set up separately. */
|
|
static void latch_texpage(psx_gpu_t* gpu, int depth) {
|
|
gpu->texp_x = 512;
|
|
gpu->texp_y = 0;
|
|
gpu->texp_d = (uint32_t)depth;
|
|
}
|
|
|
|
/* Mirrors the dispatch order in gpu.c: backend first, software second. */
|
|
static void emit_poly(psx_gpu_t* gpu, const poly_data_t* poly) {
|
|
gpu->gpustat |= g_test_force_gpustat;
|
|
|
|
if (gpu->backend && gpu->backend->draw_poly)
|
|
gpu->backend->draw_poly(gpu->backend, gpu, poly);
|
|
|
|
if (poly->attrib & PA_QUAD) {
|
|
gpu_render_triangle(gpu, poly->v[0], poly->v[1], poly->v[2], *poly, 1);
|
|
gpu_render_triangle(gpu, poly->v[1], poly->v[2], poly->v[3], *poly, 1);
|
|
} else {
|
|
gpu_render_triangle(gpu, poly->v[0], poly->v[1], poly->v[2], *poly, 0);
|
|
}
|
|
}
|
|
|
|
static void emit_rect(psx_gpu_t* gpu, const rect_data_t* rect) {
|
|
gpu->gpustat |= g_test_force_gpustat;
|
|
|
|
if (gpu->backend && gpu->backend->draw_rect)
|
|
gpu->backend->draw_rect(gpu->backend, gpu, rect);
|
|
|
|
gpu_render_rect(gpu, *rect);
|
|
}
|
|
|
|
static void emit_line(psx_gpu_t* gpu, vertex_t v0, vertex_t v1, uint16_t color) {
|
|
gpu->gpustat |= g_test_force_gpustat;
|
|
if (gpu->backend && gpu->backend->draw_line)
|
|
gpu->backend->draw_line(gpu->backend, gpu, &v0, &v1, color);
|
|
|
|
gpu_render_flat_line(gpu, v0, v1, color);
|
|
}
|
|
|
|
/* The corpus every backend case runs. Deliberately covers the fill-rule and clipping
|
|
edges: shared-edge pairs, a degenerate triangle, negative drawing offsets, primitives
|
|
straddling each drawing-area boundary, and all four semi-transparency modes. */
|
|
static void draw_corpus(psx_gpu_t* gpu) {
|
|
poly_data_t flat = {.attrib = 0};
|
|
flat.v[0].c = flat.v[1].c = flat.v[2].c = 0x40a0f0;
|
|
flat.v[0] = (vertex_t){.x = 10, .y = 12, .c = 0x40a0f0};
|
|
flat.v[1] = (vertex_t){.x = 92, .y = 24, .c = 0x40a0f0};
|
|
flat.v[2] = (vertex_t){.x = 38, .y = 105, .c = 0x40a0f0};
|
|
emit_poly(gpu, &flat);
|
|
|
|
/* Shared-edge pair: no gap and no double-cover at any scale. */
|
|
poly_data_t quad = {.attrib = PA_QUAD | PA_SHADED};
|
|
quad.v[0] = (vertex_t){.x = 140, .y = 30, .c = 0x0000ff};
|
|
quad.v[1] = (vertex_t){.x = 260, .y = 44, .c = 0x00ff00};
|
|
quad.v[2] = (vertex_t){.x = 132, .y = 150, .c = 0xff0000};
|
|
quad.v[3] = (vertex_t){.x = 255, .y = 165, .c = 0xffff00};
|
|
emit_poly(gpu, &quad);
|
|
|
|
/* Degenerate / zero-area. */
|
|
poly_data_t degenerate = {.attrib = 0};
|
|
degenerate.v[0] = (vertex_t){.x = 400, .y = 400, .c = 0x00ff00};
|
|
degenerate.v[1] = (vertex_t){.x = 400, .y = 400, .c = 0x00ff00};
|
|
degenerate.v[2] = (vertex_t){.x = 460, .y = 400, .c = 0x00ff00};
|
|
emit_poly(gpu, °enerate);
|
|
|
|
/* Clipped against every drawing-area boundary, with a negative offset applied. */
|
|
gpu->draw_x1 = 64;
|
|
gpu->draw_y1 = 48;
|
|
gpu->draw_x2 = 320;
|
|
gpu->draw_y2 = 240;
|
|
gpu->off_x = -12;
|
|
gpu->off_y = -9;
|
|
|
|
poly_data_t clipped = {.attrib = PA_SHADED};
|
|
clipped.v[0] = (vertex_t){.x = 40, .y = 20, .c = 0x3080c0};
|
|
clipped.v[1] = (vertex_t){.x = 360, .y = 96, .c = 0xc08030};
|
|
clipped.v[2] = (vertex_t){.x = 100, .y = 300, .c = 0x20f0a0};
|
|
emit_poly(gpu, &clipped);
|
|
|
|
/* All four blend modes, against live destination content. */
|
|
for (int mode = 0; mode < 4; ++mode) {
|
|
gpu->gpustat = (gpu->gpustat & ~(3u << 5)) | ((uint32_t)mode << 5);
|
|
|
|
poly_data_t blended = {.attrib = PA_TRANSP};
|
|
blended.v[0].c = blended.v[1].c = blended.v[2].c = 0xffffff;
|
|
blended.v[0] = (vertex_t){.x = (int16_t)(90 + mode * 34), .y = 70, .c = 0xffffff};
|
|
blended.v[1] = (vertex_t){.x = (int16_t)(126 + mode * 34), .y = 96, .c = 0xffffff};
|
|
blended.v[2] = (vertex_t){.x = (int16_t)(96 + mode * 34), .y = 150, .c = 0xffffff};
|
|
emit_poly(gpu, &blended);
|
|
}
|
|
|
|
gpu->off_x = 0;
|
|
gpu->off_y = 0;
|
|
|
|
/* Sprites: each size class, opaque and semi-transparent. */
|
|
for (int size = 0; size < 4; ++size) {
|
|
rect_data_t rect = {0};
|
|
rect.attrib = (uint8_t)((size << 3) | (size == 1 ? RA_TRANSP : 0));
|
|
rect.v0 = (vertex_t){.x = (int16_t)(80 + size * 40), .y = 190, .c = 0x60c0a0};
|
|
rect.width = 23;
|
|
rect.height = 17;
|
|
emit_rect(gpu, &rect);
|
|
}
|
|
|
|
/* Textured polygons: every depth, plus the raw and semi-transparent variants. The
|
|
texel-0 discard, the CLUT decode and the (t*m)/128 modulation all live here. */
|
|
for (int depth = 0; depth < 3; ++depth) {
|
|
const uint8_t variants[] = {
|
|
PA_TEXTURED,
|
|
PA_TEXTURED | PA_SHADED,
|
|
PA_TEXTURED | PA_RAW,
|
|
PA_TEXTURED | PA_TRANSP
|
|
};
|
|
|
|
for (size_t i = 0; i < sizeof(variants) / sizeof(variants[0]); ++i) {
|
|
poly_data_t tex = {.attrib = variants[i]};
|
|
|
|
tex.texp = TEXP_AT_512(depth, (int)i & 3);
|
|
tex.clut = CLUT_AT_0_400;
|
|
tex.v[0] = (vertex_t){.x = (int16_t)(70 + depth * 60), .y = 260, .c = 0x9070b0, .tx = 4, .ty = 6};
|
|
tex.v[1] = (vertex_t){.x = (int16_t)(126 + depth * 60), .y = 272, .c = 0x30c060, .tx = 60, .ty = 12};
|
|
tex.v[2] = (vertex_t){.x = (int16_t)(84 + depth * 60), .y = 330, .c = 0xd0a020, .tx = 12, .ty = 58};
|
|
emit_poly(gpu, &tex);
|
|
}
|
|
}
|
|
|
|
/* Textured sprites, which point-sample and inherit the latched texture page. */
|
|
for (int depth = 0; depth < 3; ++depth) {
|
|
latch_texpage(gpu, depth);
|
|
|
|
rect_data_t tex_rect = {0};
|
|
tex_rect.attrib = RA_TEXTURED | (depth == 1 ? RA_TRANSP : 0);
|
|
tex_rect.clut = CLUT_AT_0_400;
|
|
tex_rect.v0 = (vertex_t){.x = (int16_t)(70 + depth * 50), .y = 350, .c = 0x808080, .tx = 8, .ty = 8};
|
|
tex_rect.width = 21;
|
|
tex_rect.height = 19;
|
|
emit_rect(gpu, &tex_rect);
|
|
}
|
|
|
|
/* Mask bit: lay down a run of primitives with "set mask while drawing" on, then draw
|
|
over them with "check mask before draw" on. With PSX_GPU_ACCURACY_MASK_BIT enabled
|
|
the second set must be occluded; with it off both sets draw normally. GPUSTAT bit 9
|
|
is toggled too so the dither gate is exercised in both states. */
|
|
gpu->gpustat |= 0x0800; /* set mask while drawing */
|
|
gpu->gpustat &= ~0x0200u; /* dither disabled */
|
|
|
|
poly_data_t masker = {.attrib = PA_SHADED};
|
|
masker.v[0] = (vertex_t){.x = 230, .y = 198, .c = 0x40f080};
|
|
masker.v[1] = (vertex_t){.x = 306, .y = 210, .c = 0x8040f0};
|
|
masker.v[2] = (vertex_t){.x = 250, .y = 236, .c = 0xf08040};
|
|
emit_poly(gpu, &masker);
|
|
|
|
gpu->gpustat &= ~0x0800u;
|
|
gpu->gpustat |= 0x1000; /* check mask before draw */
|
|
gpu->gpustat |= 0x0200; /* dither enabled */
|
|
|
|
poly_data_t maskee = {.attrib = PA_SHADED};
|
|
maskee.v[0] = (vertex_t){.x = 236, .y = 200, .c = 0xf0f0f0};
|
|
maskee.v[1] = (vertex_t){.x = 312, .y = 222, .c = 0x101010};
|
|
maskee.v[2] = (vertex_t){.x = 256, .y = 238, .c = 0x8080f0};
|
|
emit_poly(gpu, &maskee);
|
|
|
|
rect_data_t maskee_rect = {0};
|
|
maskee_rect.attrib = 0;
|
|
maskee_rect.v0 = (vertex_t){.x = 232, .y = 202, .c = 0x2080d0};
|
|
maskee_rect.width = 40;
|
|
maskee_rect.height = 30;
|
|
emit_rect(gpu, &maskee_rect);
|
|
|
|
gpu->gpustat &= ~0x1800u;
|
|
|
|
/* Lines: shallow, steep and reversed, so both Bresenham branches run. */
|
|
emit_line(gpu, (vertex_t){.x = 70, .y = 60}, (vertex_t){.x = 300, .y = 130}, 0x7c1f);
|
|
emit_line(gpu, (vertex_t){.x = 290, .y = 220}, (vertex_t){.x = 110, .y = 55}, 0x03ff);
|
|
|
|
/* Transfers. fill and copy go through the same hooks gpu.c calls. */
|
|
if (gpu->backend && gpu->backend->fill_vram)
|
|
gpu->backend->fill_vram(gpu->backend, 512, 300, 64, 32, 0x1f00);
|
|
|
|
for (int y = 300; y < 332; y++)
|
|
for (int x = 512; x < 576; x++)
|
|
gpu->vram[x + (y * 1024)] = 0x1f00;
|
|
|
|
if (gpu->backend && gpu->backend->copy_vram)
|
|
gpu->backend->copy_vram(gpu->backend, 512, 300, 600, 360, 64, 32);
|
|
|
|
for (uint32_t y = 0; y < 32; y++) {
|
|
for (uint32_t x = 0; x < 64; x++) {
|
|
gpu->vram[(600 + x) + ((360 + y) * 1024)] =
|
|
gpu->vram[(512 + x) + ((300 + y) * 1024)];
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Reads the backend's render target at native coordinate (x, y): the top-left texel of
|
|
that native pixel's SxS block. */
|
|
static uint16_t rt_texel(psx_gpu_backend_t* be, int scale, int x, int y) {
|
|
uint32_t stride_bytes = 0;
|
|
const uint16_t* base = (const uint16_t*)be->display_buffer(be, 0, 0, &stride_bytes);
|
|
const size_t stride_px = stride_bytes / sizeof(uint16_t);
|
|
|
|
return base[(size_t)x * scale + ((size_t)y * scale * stride_px)];
|
|
}
|
|
|
|
/* Renders the corpus twice at the shipping defaults, once with GPUSTAT bits 9/11/12
|
|
forced high and once with them left alone, and requires the two VRAMs to be identical. */
|
|
static int run_default_inertness_case(const char* name) {
|
|
psx_gpu_t* a = make_gpu();
|
|
psx_gpu_t* b = make_gpu();
|
|
|
|
if (!a || !b) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
seed_vram(a);
|
|
seed_vram(b);
|
|
|
|
g_test_force_gpustat = 0;
|
|
draw_corpus(a);
|
|
|
|
g_test_force_gpustat = 0x1a00; /* dither-enable + set-mask + check-mask */
|
|
draw_corpus(b);
|
|
g_test_force_gpustat = 0;
|
|
|
|
const int mismatch = memcmp(a->vram, b->vram, PSX_GPU_VRAM_SIZE) != 0;
|
|
|
|
psx_gpu_destroy(a);
|
|
psx_gpu_destroy(b);
|
|
|
|
if (mismatch) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=default-path-changed "
|
|
"(GPUSTAT 9/11/12 altered output with accuracy flags off)\n", name);
|
|
return 1;
|
|
}
|
|
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
return 0;
|
|
}
|
|
|
|
static int run_backend_case(const char* name, int scale) {
|
|
psx_gpu_t* gpu = make_gpu();
|
|
|
|
if (!gpu) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
seed_vram(gpu);
|
|
|
|
psx_gpu_backend_t* be = armsx_hw_rt_create(gpu, scale);
|
|
|
|
if (!be) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=backend-create\n", name);
|
|
psx_gpu_destroy(gpu);
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_set_backend(gpu, be);
|
|
draw_corpus(gpu);
|
|
|
|
/* At scale 1 the render target must equal the software rasterizer's VRAM exactly. */
|
|
int mismatch = 0;
|
|
int first_x = -1, first_y = -1;
|
|
|
|
if (scale == 1) {
|
|
for (int y = 0; y < PSX_GPU_FB_HEIGHT && !mismatch; y++) {
|
|
for (int x = 0; x < PSX_GPU_FB_WIDTH; x++) {
|
|
if (rt_texel(be, 1, x, y) != gpu->vram[x + (y * 1024)]) {
|
|
mismatch = 1;
|
|
first_x = x;
|
|
first_y = y;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
psx_gpu_set_backend(gpu, NULL);
|
|
|
|
if (mismatch) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=rt-vram-mismatch at (%d,%d) rt=%04x vram=%04x\n",
|
|
name, first_x, first_y,
|
|
rt_texel(be, 1, first_x, first_y),
|
|
gpu->vram[first_x + (first_y * 1024)]);
|
|
}
|
|
|
|
armsx_hw_rt_destroy(be);
|
|
psx_gpu_destroy(gpu);
|
|
|
|
if (mismatch)
|
|
return 1;
|
|
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
return 0;
|
|
}
|
|
|
|
/* Renders the corpus at 1x and at `scale`, then requires the scaled target sampled every
|
|
`scale` pixels to reproduce the 1x target. */
|
|
static int run_scale_case(const char* name, int scale) {
|
|
psx_gpu_t* a = make_gpu();
|
|
psx_gpu_t* b = make_gpu();
|
|
|
|
if (!a || !b) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
seed_vram(a);
|
|
seed_vram(b);
|
|
|
|
psx_gpu_backend_t* be1 = armsx_hw_rt_create(a, 1);
|
|
psx_gpu_backend_t* ben = armsx_hw_rt_create(b, scale);
|
|
|
|
if (!be1 || !ben) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=backend-create\n", name);
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_set_backend(a, be1);
|
|
psx_gpu_set_backend(b, ben);
|
|
|
|
draw_corpus(a);
|
|
draw_corpus(b);
|
|
|
|
int mismatch = 0;
|
|
int first_x = -1, first_y = -1;
|
|
|
|
for (int y = 0; y < PSX_GPU_FB_HEIGHT && !mismatch; y++) {
|
|
for (int x = 0; x < PSX_GPU_FB_WIDTH; x++) {
|
|
if (rt_texel(be1, 1, x, y) != rt_texel(ben, scale, x, y)) {
|
|
mismatch = 1;
|
|
first_x = x;
|
|
first_y = y;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mismatch) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=scale-incoherent at native (%d,%d) 1x=%04x %dx=%04x\n",
|
|
name, first_x, first_y,
|
|
rt_texel(be1, 1, first_x, first_y), scale,
|
|
rt_texel(ben, scale, first_x, first_y));
|
|
}
|
|
|
|
psx_gpu_set_backend(a, NULL);
|
|
psx_gpu_set_backend(b, NULL);
|
|
armsx_hw_rt_destroy(be1);
|
|
armsx_hw_rt_destroy(ben);
|
|
psx_gpu_destroy(a);
|
|
psx_gpu_destroy(b);
|
|
|
|
if (mismatch)
|
|
return 1;
|
|
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------------------
|
|
GP0(E6) bit 0 == 0 means "write the SOURCE TEXEL's bit 15 into the destination mask
|
|
bit", not "write 0". Only the force-to-1 half used to be implemented, so every textured
|
|
write cleared bit 15.
|
|
|
|
This reproduces the idiom Silent Hill uses, which is what that broke (gpu_prim_dump
|
|
capture #2, primitives 00002 / 00833):
|
|
|
|
1. prime the frame with a "set mask while drawing" quad, so bit 15 is 1 everywhere;
|
|
2. draw the world — geometry whose texels carry STP=1 must KEEP bit 15;
|
|
3. per object, draw one flat semi-transparent quad over its screen bounding box with
|
|
"check mask before draw" on, to fog it.
|
|
|
|
With step 2 clearing bit 15, step 3 is never masked and paints its whole bounding box —
|
|
a lighter rectangle around every character. The corpus-based cases cannot catch this:
|
|
they compare the two rasterizers against each other, and both were wrong the same way.
|
|
This one asserts the behaviour outright.
|
|
------------------------------------------------------------------------------------ */
|
|
static int check_mask_from_texel(const char* name, uint32_t accuracy,
|
|
uint16_t texel, int expect_masked) {
|
|
psx_gpu_t* gpu = psx_gpu_create();
|
|
|
|
if (!gpu) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_init(gpu, NULL);
|
|
gpu->draw_x1 = 0;
|
|
gpu->draw_y1 = 0;
|
|
gpu->draw_x2 = PSX_GPU_FB_WIDTH - 1;
|
|
gpu->draw_y2 = PSX_GPU_FB_HEIGHT - 1;
|
|
gpu->off_x = 0;
|
|
gpu->off_y = 0;
|
|
psx_gpu_set_accuracy_flags(gpu, accuracy);
|
|
|
|
memset(gpu->vram, 0, PSX_GPU_VRAM_SIZE);
|
|
|
|
/* 15bpp texture page at (512, 0) so `texel` reaches the rasterizer verbatim, bit 15
|
|
included. Every bilinear tap of UV (8,8) lands inside this block. */
|
|
for (int y = 0; y < 16; y++)
|
|
for (int x = 512; x < 528; x++)
|
|
gpu->vram[x + (y * 1024)] = texel;
|
|
|
|
/* Additive blend, dither off, mask bits clear. */
|
|
gpu->gpustat = (gpu->gpustat & ~0x1a60u) | (1u << 5);
|
|
|
|
/* Step 2: an opaque, non-raw textured quad. Constant UVs on all four vertices, so the
|
|
interpolated coordinate is exactly (8,8) at every covered pixel. */
|
|
poly_data_t tex = {.attrib = PA_TEXTURED | PA_QUAD};
|
|
tex.texp = TEXP_AT_512(2, 0);
|
|
tex.clut = CLUT_AT_0_400;
|
|
tex.v[0] = (vertex_t){.x = 100, .y = 100, .c = 0x808080, .tx = 8, .ty = 8};
|
|
tex.v[1] = (vertex_t){.x = 140, .y = 100, .c = 0x808080, .tx = 8, .ty = 8};
|
|
tex.v[2] = (vertex_t){.x = 100, .y = 140, .c = 0x808080, .tx = 8, .ty = 8};
|
|
tex.v[3] = (vertex_t){.x = 140, .y = 140, .c = 0x808080, .tx = 8, .ty = 8};
|
|
gpu_render_triangle(gpu, tex.v[0], tex.v[1], tex.v[2], tex, 1);
|
|
gpu_render_triangle(gpu, tex.v[1], tex.v[2], tex.v[3], tex, 1);
|
|
|
|
/* Off the shared diagonal, so the fill rule cannot make this ambiguous. */
|
|
const int px = 110, py = 112;
|
|
const uint16_t after_texture = gpu->vram[px + (py * 1024)];
|
|
|
|
if (after_texture == 0) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=probe-not-covered\n", name);
|
|
psx_gpu_destroy(gpu);
|
|
return 1;
|
|
}
|
|
|
|
const int mask_written = (after_texture & 0x8000) != 0;
|
|
|
|
if (mask_written != expect_masked) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=mask-bit-not-taken-from-texel "
|
|
"texel=%04x vram=%04x expected bit15=%d\n",
|
|
name, texel, after_texture, expect_masked);
|
|
psx_gpu_destroy(gpu);
|
|
return 1;
|
|
}
|
|
|
|
/* Step 3: the fog quad. Untextured, semi-transparent, "check mask before draw" on. */
|
|
gpu->gpustat |= 0x1000u;
|
|
|
|
poly_data_t fog = {.attrib = PA_QUAD | PA_TRANSP | PA_SHADED};
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
fog.v[i] = (vertex_t){.x = (int16_t)((i & 1) ? 150 : 90),
|
|
.y = (int16_t)((i & 2) ? 150 : 90),
|
|
.c = 0xf0f0f0};
|
|
}
|
|
|
|
gpu_render_triangle(gpu, fog.v[0], fog.v[1], fog.v[2], fog, 1);
|
|
gpu_render_triangle(gpu, fog.v[1], fog.v[2], fog.v[3], fog, 1);
|
|
|
|
const uint16_t after_fog = gpu->vram[px + (py * 1024)];
|
|
const int fogged = (after_fog != after_texture);
|
|
|
|
psx_gpu_destroy(gpu);
|
|
|
|
/* Masked pixels must survive the fog quad untouched; unmasked ones must not. */
|
|
if (fogged == expect_masked) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=%s texel=%04x before=%04x after=%04x\n",
|
|
name,
|
|
expect_masked ? "fog-quad-overwrote-masked-pixel"
|
|
: "fog-quad-skipped-unmasked-pixel",
|
|
texel, after_texture, after_fog);
|
|
return 1;
|
|
}
|
|
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------------------
|
|
The mask stage as a CONTRACT, over its whole input matrix.
|
|
|
|
PSX_GPU_MASK_WRITE / PSX_GPU_MASK_SKIP (psx/dev/gpu.h) are not a tidier way to spell what
|
|
gpu.c already does — they are the text the GLES rasterizer's shader is COMPILED FROM
|
|
(frontend/gpu_hw_gl.c stringifies them into GLSL, which accepts `||` and `&&` unchanged).
|
|
A fourth hand-written copy of the rule, in a second language, is exactly how ,
|
|
, and the nearest-filter bug all happened: every rasterizer wrong the same
|
|
way, so every comparison between them passed.
|
|
|
|
This pins the two expressions to psx/dev/gpu.c's OBSERVED output over all sixteen
|
|
combinations of (set-mask, check-mask, texel STP, destination bit 15), in both accuracy
|
|
states. It is deliberately not tautological: the prediction comes from the macros, the
|
|
observation comes from rendering.
|
|
|
|
The GL side of the same contract is measured on device — gl_mask_selftest() at attach and
|
|
the `maskbit` bucket in the 1x parity gate.
|
|
------------------------------------------------------------------------------------ */
|
|
static int check_mask_contract(const char* name, uint32_t accuracy) {
|
|
psx_gpu_t* gpu = psx_gpu_create();
|
|
int failed = 0;
|
|
int row;
|
|
|
|
if (!gpu) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_init(gpu, NULL);
|
|
psx_gpu_set_accuracy_flags(gpu, accuracy);
|
|
|
|
for (row = 0; row < 16; ++row) {
|
|
const int force = (row >> 0) & 1;
|
|
const int check = (row >> 1) & 1;
|
|
const int texel_stp = (row >> 2) & 1;
|
|
const int dst_bit15 = (row >> 3) & 1;
|
|
|
|
/* 15bpp page at (512,0), one uniform texel so every tap agrees. Non-RAW, modulated
|
|
by 0x80 — the identity modulator — so the drawn colour is the texel's low 15 bits
|
|
and the ONLY thing that can set bit 15 in the result is the mask stage. */
|
|
const uint16_t texel = (uint16_t)(0x0421u | (texel_stp ? 0x8000u : 0x0000u));
|
|
const uint16_t primed = (uint16_t)(dst_bit15 ? 0x8000u : 0x0000u);
|
|
const int px = 110, py = 112;
|
|
|
|
uint16_t got;
|
|
int expect_skip, expect_mask;
|
|
int x, y;
|
|
|
|
gpu->draw_x1 = 0;
|
|
gpu->draw_y1 = 0;
|
|
gpu->draw_x2 = PSX_GPU_FB_WIDTH - 1;
|
|
gpu->draw_y2 = PSX_GPU_FB_HEIGHT - 1;
|
|
gpu->off_x = 0;
|
|
gpu->off_y = 0;
|
|
|
|
memset(gpu->vram, 0, PSX_GPU_VRAM_SIZE);
|
|
|
|
for (y = 0; y < 16; y++)
|
|
for (x = 512; x < 528; x++)
|
|
gpu->vram[x + (y * 1024)] = texel;
|
|
|
|
gpu->vram[px + (py * 1024)] = primed;
|
|
|
|
/* Opaque, dither off, GP0(E6) as this row asks. */
|
|
gpu->gpustat = (gpu->gpustat & ~0x1a60u) |
|
|
(force ? 0x0800u : 0u) | (check ? 0x1000u : 0u);
|
|
|
|
{
|
|
poly_data_t tex = {.attrib = PA_TEXTURED | PA_QUAD};
|
|
|
|
tex.texp = TEXP_AT_512(2, 0);
|
|
tex.clut = CLUT_AT_0_400;
|
|
tex.v[0] = (vertex_t){.x = 100, .y = 100, .c = 0x808080, .tx = 8, .ty = 8};
|
|
tex.v[1] = (vertex_t){.x = 140, .y = 100, .c = 0x808080, .tx = 8, .ty = 8};
|
|
tex.v[2] = (vertex_t){.x = 100, .y = 140, .c = 0x808080, .tx = 8, .ty = 8};
|
|
tex.v[3] = (vertex_t){.x = 140, .y = 140, .c = 0x808080, .tx = 8, .ty = 8};
|
|
gpu_render_triangle(gpu, tex.v[0], tex.v[1], tex.v[2], tex, 1);
|
|
gpu_render_triangle(gpu, tex.v[1], tex.v[2], tex.v[3], tex, 1);
|
|
}
|
|
|
|
got = gpu->vram[px + (py * 1024)];
|
|
|
|
/* THE PREDICTION, from the shared macros and the shared helpers — nothing else. */
|
|
expect_skip = PSX_GPU_MASK_SKIP(psx_gpu_mask_check(gpu), dst_bit15);
|
|
expect_mask = PSX_GPU_MASK_WRITE(psx_gpu_mask_set(gpu),
|
|
psx_gpu_mask_from_texel(gpu) != 0, texel_stp);
|
|
|
|
if (expect_skip) {
|
|
if (got != primed) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=masked-pixel-was-written "
|
|
"force=%d check=%d stp=%d dst15=%d primed=%04x got=%04x\n",
|
|
name, force, check, texel_stp, dst_bit15, primed, got);
|
|
failed = 1;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if ((got & 0x7fffu) != 0x0421u) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=write-did-not-land "
|
|
"force=%d check=%d stp=%d dst15=%d got=%04x want colour 0421\n",
|
|
name, force, check, texel_stp, dst_bit15, got);
|
|
failed = 1;
|
|
continue;
|
|
}
|
|
|
|
if ((((got & 0x8000u) != 0u) ? 1 : 0) != (expect_mask ? 1 : 0)) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=mask-bit-disagrees-with-contract "
|
|
"force=%d check=%d stp=%d dst15=%d got=%04x contract says bit15=%d\n",
|
|
name, force, check, texel_stp, dst_bit15, got, expect_mask ? 1 : 0);
|
|
failed = 1;
|
|
}
|
|
}
|
|
|
|
psx_gpu_destroy(gpu);
|
|
|
|
if (failed)
|
|
return 1;
|
|
|
|
printf("GPU_PARITY passed case=%s (16 rows)\n", name);
|
|
return 0;
|
|
}
|
|
|
|
static int run_mask_contract_case(void) {
|
|
int failed = 0;
|
|
|
|
/* Flag on: the rule hardware implements, and the one the GL shader compiles. */
|
|
failed |= check_mask_contract("mask-contract-accurate", PSX_GPU_ACCURACY_MASK_BIT);
|
|
|
|
/* Flag off: both expressions must collapse to "never skip, never set", because
|
|
psx_gpu_mask_check/set/from_texel all return 0 there. Same macros, same test — this is
|
|
what stops the contract from being written to fit only the accurate path. */
|
|
failed |= check_mask_contract("mask-contract-inert-by-default", 0);
|
|
|
|
return failed;
|
|
}
|
|
|
|
static int run_mask_from_texel_case(void) {
|
|
const uint32_t on = PSX_GPU_ACCURACY_MASK_BIT;
|
|
int failed = 0;
|
|
|
|
/* STP=1 texel: bit 15 must be written, and the fog quad must be masked out. */
|
|
failed |= check_mask_from_texel("mask-from-texel-stp-set", on, 0x8421, 1);
|
|
|
|
/* STP=0 texel: bit 15 stays clear and the fog quad draws. Without this control the
|
|
case above would also pass if bit 15 were simply forced on for every write. */
|
|
failed |= check_mask_from_texel("mask-from-texel-stp-clear", on, 0x0421, 0);
|
|
|
|
/* Accuracy flag off: the texel half must be inert, like the rest of the mask bit. */
|
|
failed |= check_mask_from_texel("mask-from-texel-inert-by-default", 0, 0x8421, 0);
|
|
|
|
return failed;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------------------
|
|
Hardware's polygon size cull, asserted behaviourally.
|
|
|
|
psx-spx: the maximum distance between two vertices is 1023 horizontally and 511
|
|
vertically, and polygons exceeding that are NOT rendered. This core rejected at
|
|
2048x1024 in all three rasterizers, so a triangle whose projected vertex the GTE
|
|
saturated to +1023 was drawn as a wedge stretching to the screen edge instead of being
|
|
dropped. Comparative cases cannot catch it: all three rasterizers were over-permissive
|
|
the same way, which is why GPU_PARITY passed throughout.
|
|
|
|
The geometry below is the Silent Hill shape from gpu_prim_dump_capture.txt #00876:
|
|
two vertices at sane screen positions and a third at the saturation value.
|
|
------------------------------------------------------------------------------------ */
|
|
static int check_prim_size(const char* name, uint32_t accuracy, int16_t apex_y,
|
|
int expect_drawn) {
|
|
psx_gpu_t* gpu = psx_gpu_create();
|
|
|
|
if (!gpu) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_init(gpu, NULL);
|
|
gpu->draw_x1 = 0;
|
|
gpu->draw_y1 = 0;
|
|
gpu->draw_x2 = PSX_GPU_FB_WIDTH - 1;
|
|
gpu->draw_y2 = PSX_GPU_FB_HEIGHT - 1;
|
|
gpu->off_x = 0;
|
|
gpu->off_y = 0;
|
|
psx_gpu_set_accuracy_flags(gpu, accuracy);
|
|
|
|
memset(gpu->vram, 0, PSX_GPU_VRAM_SIZE);
|
|
|
|
/* Opaque, untextured, flat. Nothing here depends on blending or the mask bit. */
|
|
poly_data_t tri = {.attrib = 0};
|
|
tri.v[0] = (vertex_t){.x = 60, .y = 100, .c = 0xf0f0f0};
|
|
tri.v[1] = (vertex_t){.x = 200, .y = 100, .c = 0xf0f0f0};
|
|
tri.v[2] = (vertex_t){.x = 130, .y = apex_y, .c = 0xf0f0f0};
|
|
|
|
gpu_render_triangle(gpu, tri.v[0], tri.v[1], tri.v[2], tri, 1);
|
|
|
|
/* Just inside the triangle a few rows below the flat top edge: covered whenever the
|
|
primitive is rasterized at all, at either apex height. */
|
|
const uint16_t probe = gpu->vram[130 + (110 * 1024)];
|
|
const int drawn = (probe != 0);
|
|
|
|
psx_gpu_destroy(gpu);
|
|
|
|
if (drawn != expect_drawn) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=%s yspan=%d vram=%04x\n",
|
|
name,
|
|
expect_drawn ? "in-range-primitive-was-culled"
|
|
: "oversize-primitive-was-rasterized",
|
|
(int)apex_y - 100, probe);
|
|
return 1;
|
|
}
|
|
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
return 0;
|
|
}
|
|
|
|
static int run_prim_size_case(void) {
|
|
const uint32_t on = PSX_GPU_ACCURACY_PRIM_SIZE;
|
|
int failed = 0;
|
|
|
|
/* yspan 923 > 511: hardware drops it, so with the flag on nothing may be written. */
|
|
failed |= check_prim_size("prim-size-oversize-culled", on, 1023, 0);
|
|
|
|
/* yspan 400 <= 511: the control. Without it the case above would also pass if the
|
|
rasterizer had simply stopped drawing anything. */
|
|
failed |= check_prim_size("prim-size-in-range-drawn", on, 500, 1);
|
|
|
|
/* Flag off restores the historical 2048x1024, i.e. the same oversize triangle draws.
|
|
This is what makes the setting a usable A/B rather than a one-way change. */
|
|
failed |= check_prim_size("prim-size-inert-by-default", 0, 1023, 1);
|
|
|
|
return failed;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------------------
|
|
Texture modulation must TRUNCATE, asserted through a frame-feedback loop.
|
|
|
|
Hardware: `(tex5 << 3) * mod8 >> 7`, integer, truncating (psx-spx). This core rounded.
|
|
The difference is one level on 4.9% of inputs and nothing on the identity modulator, so
|
|
no single-primitive comparison can see it -- and all three rasterizers rounded the same
|
|
way, so the corpus cases could not either.
|
|
|
|
A feedback loop makes it a permanent artifact. Silent Hill's loading screen blits the
|
|
previous frame back over itself every frame through this multiply, alternating an
|
|
identity pass (mod 0x80) with a decay pass (mod 0x7f); that is how the running
|
|
character's motion blur fades. Rounding turns `round(t5 * 7.9375)` back into `t5 * 8`
|
|
for every t5 <= 8, so levels 1..8 are fixed points: the trail decays to 8/31 and stays
|
|
there forever. This case runs that exact loop.
|
|
------------------------------------------------------------------------------------ */
|
|
static int check_tex_modulate_decay(const char* name, uint32_t accuracy, int expect_black) {
|
|
psx_gpu_t* gpu = psx_gpu_create();
|
|
|
|
if (!gpu) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_init(gpu, NULL);
|
|
gpu->draw_x1 = 0;
|
|
gpu->draw_y1 = 0;
|
|
gpu->draw_x2 = PSX_GPU_FB_WIDTH - 1;
|
|
gpu->draw_y2 = PSX_GPU_FB_HEIGHT - 1;
|
|
gpu->off_x = 0;
|
|
gpu->off_y = 0;
|
|
psx_gpu_set_accuracy_flags(gpu, accuracy);
|
|
memset(gpu->vram, 0, PSX_GPU_VRAM_SIZE);
|
|
|
|
/* 15bpp-direct page at (512,0), exactly like the game's framebuffer-as-texture blit. */
|
|
gpu->texp_x = 512;
|
|
gpu->texp_y = 0;
|
|
gpu->texp_d = 2;
|
|
gpu->gpustat &= ~0x1a60u;
|
|
|
|
const int dst_x = 300, dst_y = 300;
|
|
int level = 31; /* full white trail */
|
|
int iterations = 0;
|
|
|
|
for (int frame = 0; frame < 400 && level > 0; frame++) {
|
|
const uint16_t src = (uint16_t)(level | (level << 5) | (level << 10));
|
|
|
|
gpu->vram[512] = src; /* the texel we sample */
|
|
gpu->vram[dst_x + (dst_y * 1024)] = 0; /* the pixel we blit into */
|
|
|
|
rect_data_t r = {0};
|
|
r.attrib = RA_TEXTURED; /* opaque, blended, variable */
|
|
r.width = 1;
|
|
r.height = 1;
|
|
r.clut = 0;
|
|
/* Alternating identity / decay pass, as captured from the game. */
|
|
r.v0 = (vertex_t){.x = (int16_t)dst_x, .y = (int16_t)dst_y,
|
|
.c = (frame & 1) ? 0x808080u : 0x7f7f7fu, .tx = 0, .ty = 0};
|
|
|
|
gpu_render_rect(gpu, r);
|
|
|
|
level = gpu->vram[dst_x + (dst_y * 1024)] & 0x1f;
|
|
iterations = frame + 1;
|
|
}
|
|
|
|
psx_gpu_destroy(gpu);
|
|
|
|
const int reached_black = (level == 0);
|
|
|
|
if (reached_black != expect_black) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=%s level=%d after %d frames\n",
|
|
name,
|
|
expect_black ? "feedback-trail-never-fades"
|
|
: "feedback-trail-faded-with-the-flag-off",
|
|
level, iterations);
|
|
return 1;
|
|
}
|
|
|
|
printf("GPU_PARITY passed case=%s (level=%d after %d frames)\n", name, level, iterations);
|
|
return 0;
|
|
}
|
|
|
|
static int run_tex_modulate_case(void) {
|
|
const uint32_t on = PSX_GPU_ACCURACY_TEX_MODULATE;
|
|
int failed = 0;
|
|
|
|
/* With truncation the trail must reach black, as it does on hardware. */
|
|
failed |= check_tex_modulate_decay("tex-modulate-trail-fades", on, 1);
|
|
|
|
/* Flag off restores rounding, and the trail sticks forever. This is the control that
|
|
proves the case above is actually measuring the rounding and not something else. */
|
|
failed |= check_tex_modulate_decay("tex-modulate-inert-by-default", 0, 0);
|
|
|
|
return failed;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------------------
|
|
GP0 argument intake must not run off the end of gpu->buf[].
|
|
|
|
A polyline (GP0 bit 27) is the one command with no argument count: gpu_line() parks
|
|
cmd_args_remaining at -1 and only the game's 0x50005000 terminator ends it, so
|
|
psx_gpu_write32()'s RECV_ARGS branch takes every following word. Unbounded, a polyline
|
|
longer than the 16-slot buffer -- or a desynced stream where the terminator never
|
|
arrives -- walked buf[] straight into the struct members that follow it: recv_data,
|
|
then buf_index, at which point the command stream owns the index and the writes go
|
|
wherever it says, and on through the command counters into the drawing area and the
|
|
drawing offset.
|
|
|
|
The drawing offset and drawing area are the assertion because they sit at the far end
|
|
of that walk (buf slots 78-83 on a 64-bit build). If they still hold what this test set,
|
|
nothing between buf[16] and them was reached either.
|
|
------------------------------------------------------------------------------------ */
|
|
#define OVERRUN_OFF_X (-77)
|
|
#define OVERRUN_OFF_Y (123)
|
|
#define OVERRUN_DRAW_X1 (5u)
|
|
#define OVERRUN_DRAW_Y1 (9u)
|
|
#define OVERRUN_DRAW_X2 (601u)
|
|
#define OVERRUN_DRAW_Y2 (411u)
|
|
|
|
/* GP0(48): monochrome opaque polyline. Bit 27 (0x08 of the command byte) is what makes it
|
|
a polyline; bit 28 (shaded) is clear. */
|
|
#define OVERRUN_POLYLINE_CMD 0x48808080u
|
|
|
|
/* Slot in gpu->buf[] a member of psx_gpu_t occupies once the write walks past the end of
|
|
the buffer. Derived rather than hardcoded so the aimed case below still points at the
|
|
drawing area if the struct is reordered. */
|
|
#define OVERRUN_SLOT(field) \
|
|
((uint32_t)((offsetof(psx_gpu_t, field) - offsetof(psx_gpu_t, buf)) / sizeof(uint32_t)))
|
|
|
|
static psx_gpu_t* make_overrun_gpu(void) {
|
|
psx_gpu_t* gpu = psx_gpu_create();
|
|
|
|
if (!gpu)
|
|
return NULL;
|
|
|
|
psx_gpu_init(gpu, NULL);
|
|
|
|
/* Distinctive and non-zero, so a stray word landing on any of them shows up. */
|
|
gpu->off_x = OVERRUN_OFF_X;
|
|
gpu->off_y = OVERRUN_OFF_Y;
|
|
gpu->draw_x1 = OVERRUN_DRAW_X1;
|
|
gpu->draw_y1 = OVERRUN_DRAW_Y1;
|
|
gpu->draw_x2 = OVERRUN_DRAW_X2;
|
|
gpu->draw_y2 = OVERRUN_DRAW_Y2;
|
|
|
|
return gpu;
|
|
}
|
|
|
|
static int check_overrun_state(const char* name, psx_gpu_t* gpu) {
|
|
const int buf_slots = (int)(sizeof(gpu->buf) / sizeof(gpu->buf[0]));
|
|
|
|
if (gpu->off_x != OVERRUN_OFF_X || gpu->off_y != OVERRUN_OFF_Y) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=drawing-offset-clobbered offset=(%d,%d)\n",
|
|
name, gpu->off_x, gpu->off_y);
|
|
return 1;
|
|
}
|
|
|
|
if (gpu->draw_x1 != OVERRUN_DRAW_X1 || gpu->draw_y1 != OVERRUN_DRAW_Y1 ||
|
|
gpu->draw_x2 != OVERRUN_DRAW_X2 || gpu->draw_y2 != OVERRUN_DRAW_Y2) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=drawing-area-clobbered area=(%u,%u)-(%u,%u)\n",
|
|
name, gpu->draw_x1, gpu->draw_y1, gpu->draw_x2, gpu->draw_y2);
|
|
return 1;
|
|
}
|
|
|
|
/* The first thing the overrun reaches, and the one that turns a bounded overflow into
|
|
an arbitrary write: once a stream word lands on buf_index, every later word goes
|
|
wherever that word said. */
|
|
if (gpu->buf_index < 0 || gpu->buf_index > buf_slots) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=buf-index-out-of-range index=%d slots=%d\n",
|
|
name, gpu->buf_index, buf_slots);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* The realistic shape: a stream that opens a polyline and never terminates it. */
|
|
static int check_polyline_unterminated(void) {
|
|
const char* name = "polyline-unterminated-overrun";
|
|
psx_gpu_t* gpu = make_overrun_gpu();
|
|
int failed;
|
|
int i;
|
|
|
|
if (!gpu) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_write32(gpu, 0x00, OVERRUN_POLYLINE_CMD);
|
|
|
|
/* Far more words than buf[] holds. Every one reads as a plausible vertex (x=20, y=0)
|
|
and none matches the 0x50005000 terminator, so the command never ends on its own. */
|
|
for (i = 0; i < 4096; i++)
|
|
psx_gpu_write32(gpu, 0x00, 0x00000014u);
|
|
|
|
failed = check_overrun_state(name, gpu);
|
|
psx_gpu_destroy(gpu);
|
|
|
|
if (!failed)
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
|
|
return failed;
|
|
}
|
|
|
|
/* The same overrun, aimed. The case above proves the buffer holds; this one proves the
|
|
drawing state is genuinely out of reach, by sending the stream that would otherwise land
|
|
on it: 16 words to fill buf[] and reach recv_data, one word onto buf_index carrying the
|
|
drawing area's slot, then six words that would be draw_x1..off_y. Without the clamp the
|
|
seventeenth word redirects the index and the last six overwrite the drawing state; with
|
|
it, none of them leave buf[]. */
|
|
static int check_polyline_aimed_at_draw_state(void) {
|
|
const char* name = "polyline-unterminated-aimed-at-draw-state";
|
|
psx_gpu_t* gpu = make_overrun_gpu();
|
|
int failed;
|
|
int i;
|
|
|
|
if (!gpu) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_write32(gpu, 0x00, OVERRUN_POLYLINE_CMD);
|
|
|
|
/* buf[1..16]: the rest of the buffer, then recv_data. */
|
|
for (i = 0; i < 16; i++)
|
|
psx_gpu_write32(gpu, 0x00, 0x00000014u);
|
|
|
|
/* buf[17] is buf_index itself. */
|
|
psx_gpu_write32(gpu, 0x00, OVERRUN_SLOT(draw_x1));
|
|
|
|
/* Six words for the six slots the redirected index now points at. Deliberately not
|
|
more: two slots past off_y is an owned pointer that psx_gpu_destroy() frees. */
|
|
for (i = 0; i < 6; i++)
|
|
psx_gpu_write32(gpu, 0x00, 0x00000014u);
|
|
|
|
failed = check_overrun_state(name, gpu);
|
|
psx_gpu_destroy(gpu);
|
|
|
|
if (!failed)
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
|
|
return failed;
|
|
}
|
|
|
|
/* The control, and the reason the bound SATURATES instead of dropping the word. gpu_line()
|
|
looks for the terminator in the last slot written, so a clamp that stopped storing once
|
|
buf[] filled would leave a long-but-perfectly-legal polyline stuck in RECV_ARGS until the
|
|
next GP1 reset -- trading an overrun for a hang. Parking overflow words in the last slot
|
|
keeps the terminator visible, and this case is what says so. */
|
|
static int check_polyline_long_still_terminates(void) {
|
|
const char* name = "polyline-long-still-terminates";
|
|
psx_gpu_t* gpu = make_overrun_gpu();
|
|
int failed;
|
|
int i;
|
|
|
|
if (!gpu) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_write32(gpu, 0x00, OVERRUN_POLYLINE_CMD);
|
|
|
|
/* Comfortably past the 16 slots, but properly terminated -- a 40-vertex polyline is
|
|
nothing unusual for a wireframe or a starfield. */
|
|
for (i = 0; i < 40; i++)
|
|
psx_gpu_write32(gpu, 0x00, 0x00000014u);
|
|
|
|
psx_gpu_write32(gpu, 0x00, 0x55555555u);
|
|
|
|
failed = check_overrun_state(name, gpu);
|
|
|
|
if (!failed && gpu->state != GPU_STATE_RECV_CMD) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=terminator-missed state=%u\n",
|
|
name, gpu->state);
|
|
failed = 1;
|
|
}
|
|
|
|
psx_gpu_destroy(gpu);
|
|
|
|
if (!failed)
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
|
|
return failed;
|
|
}
|
|
|
|
static int run_polyline_overrun_case(void) {
|
|
int failed = 0;
|
|
|
|
failed |= check_polyline_unterminated();
|
|
failed |= check_polyline_aimed_at_draw_state();
|
|
failed |= check_polyline_long_still_terminates();
|
|
|
|
return failed;
|
|
}
|
|
|
|
/* Axis-aligned corpus: sprites, fills, copies and uploads only. Every one of these lands
|
|
on integer native pixel boundaries, so at scale N each native pixel MUST come out as a
|
|
uniform NxN block. Polygons and lines are excluded because sub-pixel variation inside a
|
|
block is exactly what upscaling is supposed to produce for them. */
|
|
static void draw_axis_aligned_corpus(psx_gpu_t* gpu) {
|
|
gpu->draw_x1 = 64;
|
|
gpu->draw_y1 = 48;
|
|
gpu->draw_x2 = 320;
|
|
gpu->draw_y2 = 240;
|
|
|
|
/* Straddles all four drawing-area edges, so a scissor that is off by a single
|
|
render-target column leaves a partially-filled block behind. */
|
|
const int probe[][2] = { {50, 40}, {310, 40}, {50, 232}, {310, 232}, {100, 100} };
|
|
|
|
for (size_t i = 0; i < sizeof(probe) / sizeof(probe[0]); ++i) {
|
|
rect_data_t rect = {0};
|
|
rect.attrib = 0;
|
|
rect.v0 = (vertex_t){.x = (int16_t)probe[i][0], .y = (int16_t)probe[i][1], .c = 0x60c0a0};
|
|
rect.width = 24;
|
|
rect.height = 20;
|
|
emit_rect(gpu, &rect);
|
|
}
|
|
|
|
/* A 1x1 sprite has to become a solid NxN block (the backend). */
|
|
rect_data_t tiny = {0};
|
|
tiny.attrib = (uint8_t)(RS_1X1 << 3);
|
|
tiny.v0 = (vertex_t){.x = 200, .y = 120, .c = 0xf0308f};
|
|
emit_rect(gpu, &tiny);
|
|
|
|
/* Textured sprites point-sample, so each native pixel must still be one texel across
|
|
its whole NxN block. This is what fails if the texel counter advances per
|
|
render-target pixel instead of per native pixel. */
|
|
for (int depth = 0; depth < 3; ++depth) {
|
|
latch_texpage(gpu, depth);
|
|
|
|
rect_data_t tex_rect = {0};
|
|
tex_rect.attrib = RA_TEXTURED;
|
|
tex_rect.clut = CLUT_AT_0_400;
|
|
tex_rect.v0 = (vertex_t){.x = (int16_t)(120 + depth * 44), .y = 160, .c = 0x808080, .tx = 5, .ty = 9};
|
|
tex_rect.width = 26;
|
|
tex_rect.height = 22;
|
|
emit_rect(gpu, &tex_rect);
|
|
}
|
|
|
|
if (gpu->backend && gpu->backend->fill_vram)
|
|
gpu->backend->fill_vram(gpu->backend, 512, 300, 64, 32, 0x1f00);
|
|
|
|
for (int y = 300; y < 332; y++)
|
|
for (int x = 512; x < 576; x++)
|
|
gpu->vram[x + (y * 1024)] = 0x1f00;
|
|
|
|
if (gpu->backend && gpu->backend->copy_vram)
|
|
gpu->backend->copy_vram(gpu->backend, 512, 300, 600, 360, 64, 32);
|
|
|
|
for (uint32_t y = 0; y < 32; y++)
|
|
for (uint32_t x = 0; x < 64; x++)
|
|
gpu->vram[(600 + x) + ((360 + y) * 1024)] = gpu->vram[(512 + x) + ((300 + y) * 1024)];
|
|
|
|
/* Kept clear of the texture page at (512,0)-(767,255) so the sprites above sample
|
|
stable data. */
|
|
for (uint32_t y = 0; y < 16; y++)
|
|
for (uint32_t x = 0; x < 16; x++)
|
|
gpu->vram[(700 + x) + ((420 + y) * 1024)] = (uint16_t)(0x0421 * (x + y));
|
|
|
|
if (gpu->backend && gpu->backend->upload_vram)
|
|
gpu->backend->upload_vram(gpu->backend, 700, 420, 16, 16, gpu->vram, 1024);
|
|
}
|
|
|
|
static int run_block_uniformity_case(const char* name, int scale) {
|
|
psx_gpu_t* gpu = make_gpu();
|
|
|
|
if (!gpu) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
seed_vram(gpu);
|
|
|
|
psx_gpu_backend_t* be = armsx_hw_rt_create(gpu, scale);
|
|
|
|
if (!be) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=backend-create\n", name);
|
|
psx_gpu_destroy(gpu);
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_set_backend(gpu, be);
|
|
draw_axis_aligned_corpus(gpu);
|
|
|
|
uint32_t stride_bytes = 0;
|
|
const uint16_t* base = (const uint16_t*)be->display_buffer(be, 0, 0, &stride_bytes);
|
|
const size_t stride_px = stride_bytes / sizeof(uint16_t);
|
|
|
|
int failed = 0;
|
|
|
|
for (int y = 0; y < PSX_GPU_FB_HEIGHT && !failed; y++) {
|
|
for (int x = 0; x < PSX_GPU_FB_WIDTH && !failed; x++) {
|
|
const uint16_t want = gpu->vram[x + (y * 1024)];
|
|
|
|
for (int by = 0; by < scale && !failed; by++) {
|
|
for (int bx = 0; bx < scale; bx++) {
|
|
const uint16_t got = base[((size_t)x * scale + bx) +
|
|
(((size_t)y * scale + by) * stride_px)];
|
|
|
|
if (got != want) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=block-not-uniform at native (%d,%d) "
|
|
"sub (%d,%d) got=%04x want=%04x\n",
|
|
name, x, y, bx, by, got, want);
|
|
failed = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
psx_gpu_set_backend(gpu, NULL);
|
|
armsx_hw_rt_destroy(be);
|
|
psx_gpu_destroy(gpu);
|
|
|
|
if (failed)
|
|
return 1;
|
|
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------------------
|
|
GP0 COMMAND INTAKE — sprites must survive the trip from a command word to a pixel.
|
|
|
|
WHY THIS EXISTS, AND WHY 23 GREEN CASES DID NOT CATCH IT
|
|
|
|
Every other case in this file calls gpu_render_triangle() / gpu_render_rect() DIRECTLY.
|
|
That is a rasterizer test, and it is structurally blind to the half of the GPU that
|
|
decides whether the rasterizer is ever called: psx_gpu_write32()'s state machine, the
|
|
per-command argument count in gpu_rect(), the type dispatch in psx_gpu_update_cmd(),
|
|
and whatever else sits between a GP0 word and a primitive.
|
|
|
|
The failure that motivated this gate was "the PS1 BIOS draws its logo but not the
|
|
SONY COMPUTER ENTERTAINMENT text under it", with the performance overlay reporting
|
|
`0 rect` on a frame that reported non-zero triangles. Text is textured sprites. A
|
|
rect that is never SUBMITTED and a rect that is submitted and rasterized wrongly look
|
|
nothing alike from inside gpu.c, and only one of them moves that counter — so the
|
|
counter is asserted here too, as the machine-readable form of "the primitive happened".
|
|
|
|
WHAT IS PINNED, AND AGAINST WHAT
|
|
|
|
Not against the other rasterizer: shared formulas can be identically wrong in every
|
|
implementation while comparative gates still pass. Expected VRAM is computed from the
|
|
contract:
|
|
|
|
* a RAW sprite writes the texel unmodified, so the expectation is the texel this
|
|
test itself placed in the texture page — no emulator code participates in it;
|
|
* a modulated sprite at the identity modulator 0x808080 also writes the texel
|
|
unchanged (tex5*8 * 0x80 >> 7 == tex5*8), so the same literal expectation holds
|
|
across the blend path without re-deriving the blend;
|
|
* an untextured rect writes BGR555 of its own command colour.
|
|
|
|
The 4-bit CLUT case is the one PS1 text actually uses, and it is the only case that
|
|
exercises the CLUT indirection through the intake.
|
|
------------------------------------------------------------------------------------ */
|
|
|
|
/* Texture page at VRAM (512, 0); CLUT for the 4-bit case at (0, 400). Same layout the
|
|
backend cases use, so a reader only has to learn it once. */
|
|
#define GP0_TEX_PAGE_X 512
|
|
#define GP0_TEX_PAGE_Y 0
|
|
#define GP0_CLUT_X 0
|
|
#define GP0_CLUT_Y 400
|
|
|
|
/* A deterministic, always-non-zero BGR555 texel. Zero is special-cased by the rasterizer
|
|
as "fully transparent, skip the pixel", so a texture of zeroes would let a completely
|
|
dead rect path pass by writing nothing and being asked for nothing. */
|
|
static uint16_t gp0_texel_at(int x, int y) {
|
|
return (uint16_t)(0x0421u * (unsigned)(((x * 7 + y * 13) % 30) + 1));
|
|
}
|
|
|
|
static void gp0_seed_texture(psx_gpu_t* gpu) {
|
|
for (int y = 0; y < 256; y++)
|
|
for (int x = 0; x < 256; x++)
|
|
gpu->vram[(GP0_TEX_PAGE_X + x) + ((GP0_TEX_PAGE_Y + y) * 1024)] = gp0_texel_at(x, y);
|
|
}
|
|
|
|
/* 16 CLUT entries, and a 4-bit page whose every nibble is a known index. */
|
|
static void gp0_seed_clut4(psx_gpu_t* gpu) {
|
|
for (int i = 0; i < 16; i++)
|
|
gpu->vram[(GP0_CLUT_X + i) + (GP0_CLUT_Y * 1024)] = (uint16_t)(0x0421u * (unsigned)(i + 1));
|
|
|
|
/* Halfword h of row y holds indices for texels 4h..4h+3. Index = (tx + ty) & 0xf. */
|
|
for (int y = 0; y < 256; y++) {
|
|
for (int h = 0; h < 64; h++) {
|
|
uint16_t word = 0;
|
|
|
|
for (int n = 0; n < 4; n++) {
|
|
const int tx = h * 4 + n;
|
|
word |= (uint16_t)(((unsigned)((tx + y) & 0xf)) << (n * 4));
|
|
}
|
|
|
|
gpu->vram[(GP0_TEX_PAGE_X + h) + ((GP0_TEX_PAGE_Y + y) * 1024)] = word;
|
|
}
|
|
}
|
|
}
|
|
|
|
static uint16_t gp0_clut4_expected(psx_gpu_t* gpu, int tx, int ty) {
|
|
return gpu->vram[(GP0_CLUT_X + ((tx + ty) & 0xf)) + (GP0_CLUT_Y * 1024)];
|
|
}
|
|
|
|
static void gp0(psx_gpu_t* gpu, uint32_t word) {
|
|
psx_gpu_write32(gpu, 0x00, word);
|
|
}
|
|
|
|
/* GP0(E1): texpage x/64, y/256, blend mode, colour depth. */
|
|
static void gp0_texpage(psx_gpu_t* gpu, int depth) {
|
|
gp0(gpu, 0xe1000000u | (uint32_t)(GP0_TEX_PAGE_X / 64) |
|
|
((uint32_t)(GP0_TEX_PAGE_Y / 256) << 4) | ((uint32_t)depth << 7));
|
|
}
|
|
|
|
static int gp0_check_pixels(const char* name, const char* what, psx_gpu_t* gpu,
|
|
int x0, int y0, int w, int h,
|
|
const uint16_t* expect) {
|
|
for (int dy = 0; dy < h; dy++) {
|
|
for (int dx = 0; dx < w; dx++) {
|
|
const uint16_t got = gpu->vram[(x0 + dx) + ((y0 + dy) * 1024)];
|
|
const uint16_t want = expect[dy * w + dx];
|
|
|
|
if (got != want) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=%s at (%d,%d) got=%04x want=%04x\n",
|
|
name, what, x0 + dx, y0 + dy, got, want);
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
Drives four sprite/rect forms through psx_gpu_write32() and checks that each one
|
|
(a) leaves the command state machine ready for the next command, (b) moves the
|
|
PSX_PERF_PRIM_RECT counter, and (c) writes the exact pixels the contract calls for.
|
|
|
|
The final step submits a TRIANGLE after all of them. A rect whose argument count is
|
|
wrong does not merely fail to draw — it eats the following command's words, so the
|
|
triangle is the guard against a rect desynchronising the stream behind it, which is
|
|
the failure mode that takes a whole frame down rather than one primitive.
|
|
*/
|
|
/*
|
|
GP1(10h) GPUINFO round-trip: what a game writes into a draw-environment register is
|
|
exactly what the readback must return. psx-spx: index 2 echoes GP0(E2) (20 bit), 3 and 4
|
|
echo GP0(E3)/GP0(E4) (19 bit), 5 echoes GP0(E5) (22 bit) — as written. Exhaustive over
|
|
every payload each register retains, because the bug this pins was a one-bit layout error
|
|
— the E5 readback repacked Y at bit 10 (E3's layout) instead of bit 11 — and no sampled
|
|
or differential case had a reason to catch it: both halves of a differential pair would
|
|
happily agree on the wrong layout. Xenogears reads its own offset back through
|
|
GPUINFO(5), ORs the command byte on, and resubmits it as a GP0(E5); on hardware that
|
|
round trip is identity, and with the bit-10 repack it halved the Y offset and clipped
|
|
~13% of battle frames.
|
|
|
|
Sign matters: off_x/off_y are stored sign-extended, so payloads with bit 10 or bit 21
|
|
set (negative fields) must echo verbatim rather than smearing sign bits over the word.
|
|
The sweeps below include every such value.
|
|
*/
|
|
/*
|
|
GP1(08) -> GPUSTAT mirror. psx-spx: mode bits 0-5 land in GPUSTAT 17-22, bit 6 (hres2)
|
|
in 16, bit 7 (reverse) in 14. These were never written at all, so a game reading its
|
|
video mode back saw 256x240/15bpp/progressive/NTSC forever — for Crash (512-wide,
|
|
GP1(08)=0x02) the readback claimed a screen half the real width, and an engine that
|
|
sizes its culling viewport from GPUSTAT drops exactly the outer flank geometry.
|
|
Exhaustive over all 256 mode bytes, and the texpage/mask bits E1/E6 own must survive.
|
|
*/
|
|
static int run_gp1_mode_mirror_case(void) {
|
|
const char* name = "gp1-mode-mirror";
|
|
psx_gpu_t* gpu = make_gpu();
|
|
int failed = 0;
|
|
|
|
/* Seed the E1/E6-owned low bits so a clobber is visible. */
|
|
psx_gpu_write32(gpu, 0, 0xe1000000 | 0x2ff);
|
|
psx_gpu_write32(gpu, 0, 0xe6000003);
|
|
|
|
for (uint32_t m = 0; m < 0x100 && !failed; m++) {
|
|
psx_gpu_write32(gpu, 4, 0x08000000 | m);
|
|
|
|
uint32_t stat = psx_gpu_read32(gpu, 4);
|
|
uint32_t want = ((m & 0x3fu) << 17) | ((m & 0x40u) << 10) | ((m & 0x80u) << 7);
|
|
|
|
if ((stat & 0x007f4000u) != want) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=mode-mirror mode=%02x stat=%08x "
|
|
"mirror=%06x want=%06x\n",
|
|
name, m, stat, stat & 0x007f4000u, want);
|
|
failed = 1;
|
|
}
|
|
|
|
/* E1's texpage bits 0-10 and E6's mask bits 11-12 must be untouched. */
|
|
if ((stat & 0x1fffu) != 0x1aff) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=low-bits-clobbered mode=%02x "
|
|
"stat=%08x low=%04x want=1aff\n",
|
|
name, m, stat, stat & 0x1fffu);
|
|
failed = 1;
|
|
}
|
|
}
|
|
|
|
psx_gpu_destroy(gpu);
|
|
|
|
if (!failed)
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
|
|
return failed;
|
|
}
|
|
|
|
/*
|
|
GP1(07) - Display range on screen (vertical): Y1 in bits 0-9, Y2 in bits 10-19.
|
|
Both fields are TEN bits wide. The decode used 0x1ff for each, so bit 9 of Y1
|
|
wrapped and bit 9 of Y2 was dropped outright — a 480i title whose range ends
|
|
past scanline 511 got a picture 512 lines short. Sweep every 10-bit value in
|
|
both positions plus the full 20-bit payload at the boundaries so a re-narrowed
|
|
mask cannot pass.
|
|
*/
|
|
static int run_gp1_vrange_case(void) {
|
|
const char* name = "gp1-vrange-10bit";
|
|
psx_gpu_t* gpu = make_gpu();
|
|
int failed = 0;
|
|
uint32_t v;
|
|
|
|
for (v = 0; v < 0x400 && !failed; v++) {
|
|
/* Y1 swept, Y2 held at a value whose own bit 9 is set. */
|
|
psx_gpu_write32(gpu, 4, 0x07000000u | v | (0x2aau << 10));
|
|
|
|
if (gpu->disp_y1 != v || gpu->disp_y2 != 0x2aau) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=y1-sweep wrote=%05x "
|
|
"y1=%u want=%u y2=%u want=682\n",
|
|
name, v | (0x2aau << 10), gpu->disp_y1, v, gpu->disp_y2);
|
|
failed = 1;
|
|
}
|
|
|
|
/* Y2 swept, Y1 held. */
|
|
psx_gpu_write32(gpu, 4, 0x07000000u | 0x155u | (v << 10));
|
|
|
|
if (gpu->disp_y1 != 0x155u || gpu->disp_y2 != v) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=y2-sweep wrote=%05x "
|
|
"y1=%u want=341 y2=%u want=%u\n",
|
|
name, 0x155u | (v << 10), gpu->disp_y1, gpu->disp_y2, v);
|
|
failed = 1;
|
|
}
|
|
}
|
|
|
|
/* Bits 20-23 of the payload belong to no field and must not leak in. */
|
|
if (!failed) {
|
|
psx_gpu_write32(gpu, 4, 0x07000000u | 0xfffffu | (0xfu << 20));
|
|
|
|
if (gpu->disp_y1 != 0x3ffu || gpu->disp_y2 != 0x3ffu) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=upper-bits y1=%u y2=%u want=1023,1023\n",
|
|
name, gpu->disp_y1, gpu->disp_y2);
|
|
failed = 1;
|
|
}
|
|
}
|
|
|
|
psx_gpu_destroy(gpu);
|
|
|
|
if (!failed)
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
|
|
return failed;
|
|
}
|
|
|
|
static int run_gpuinfo_roundtrip_case(void) {
|
|
const char* name = "gpuinfo-roundtrip";
|
|
psx_gpu_t* gpu = make_gpu();
|
|
int failed = 0;
|
|
|
|
/* GP0(E5): X bits 0-10, Y bits 11-21, both signed 11-bit; all 22 bits retained. */
|
|
for (uint32_t p = 0; p < (1u << 22) && !failed; p++) {
|
|
psx_gpu_write32(gpu, 0, 0xe5000000 | p);
|
|
psx_gpu_write32(gpu, 4, 0x10000005);
|
|
|
|
uint32_t got = psx_gpu_read32(gpu, 0) & 0x3fffff;
|
|
|
|
if (got != p) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=e5-roundtrip wrote=%06x read=%06x\n",
|
|
name, p, got);
|
|
failed = 1;
|
|
}
|
|
}
|
|
|
|
/* GP0(E3)/GP0(E4): X 10 bits, Y 9 bits at bit 10. Bits above 18 are dropped at decode,
|
|
so the identity is over what the GPU retains. Sweep one bit past retention to pin the
|
|
drop as well as the echo. */
|
|
for (uint32_t p = 0; p < (1u << 20) && !failed; p++) {
|
|
psx_gpu_write32(gpu, 0, 0xe3000000 | p);
|
|
psx_gpu_write32(gpu, 4, 0x10000003);
|
|
|
|
uint32_t got3 = psx_gpu_read32(gpu, 0) & 0xfffff;
|
|
|
|
psx_gpu_write32(gpu, 0, 0xe4000000 | p);
|
|
psx_gpu_write32(gpu, 4, 0x10000004);
|
|
|
|
uint32_t got4 = psx_gpu_read32(gpu, 0) & 0xfffff;
|
|
|
|
if (got3 != (p & 0x7ffff) || got4 != (p & 0x7ffff)) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=e3e4-roundtrip wrote=%06x "
|
|
"read3=%06x read4=%06x want=%06x\n",
|
|
name, p, got3, got4, p & 0x7ffff);
|
|
failed = 1;
|
|
}
|
|
}
|
|
|
|
/* GP0(E2): four 5-bit fields, each stored x8 and divided back on readback. */
|
|
for (uint32_t p = 0; p < (1u << 20) && !failed; p++) {
|
|
psx_gpu_write32(gpu, 0, 0xe2000000 | p);
|
|
psx_gpu_write32(gpu, 4, 0x10000002);
|
|
|
|
uint32_t got = psx_gpu_read32(gpu, 0) & 0xfffff;
|
|
|
|
if (got != p) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=e2-roundtrip wrote=%06x read=%06x\n",
|
|
name, p, got);
|
|
failed = 1;
|
|
}
|
|
}
|
|
|
|
/*
|
|
GP1(10h).7 - Read GPU Type. Retail silicon answers 2; 0 is the pre-production
|
|
160-pin part. Indices 0, 1 and 6 answer nothing at all and must leave the previous
|
|
GPUREAD word standing, so seed a known value through index 5 and check that only
|
|
7 changes it. Index 8+ mirrors low indices, so 0x0f re-selects 7.
|
|
*/
|
|
{
|
|
psx_gpu_write32(gpu, 0, 0xe5000000 | 0x1234); /* a distinctive latched word */
|
|
psx_gpu_write32(gpu, 4, 0x10000005);
|
|
|
|
uint32_t seed = psx_gpu_read32(gpu, 0);
|
|
|
|
static const uint32_t quiet[] = { 0, 1, 6 };
|
|
|
|
for (unsigned i = 0; i < sizeof(quiet) / sizeof(quiet[0]) && !failed; i++) {
|
|
psx_gpu_write32(gpu, 4, 0x10000000 | quiet[i]);
|
|
|
|
uint32_t got = psx_gpu_read32(gpu, 0);
|
|
|
|
if (got != seed) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=gpuinfo-quiet-index index=%u "
|
|
"read=%08x want-unchanged=%08x\n",
|
|
name, quiet[i], got, seed);
|
|
failed = 1;
|
|
}
|
|
}
|
|
|
|
for (unsigned i = 0; i < 2 && !failed; i++) {
|
|
psx_gpu_write32(gpu, 4, 0x10000007 | (i ? 8u : 0u));
|
|
|
|
uint32_t got = psx_gpu_read32(gpu, 0);
|
|
|
|
if (got != 2) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=gpu-type req=%u read=%08x want=00000002\n",
|
|
name, 7u | (i ? 8u : 0u), got);
|
|
failed = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
psx_gpu_destroy(gpu);
|
|
|
|
if (!failed)
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
|
|
return failed;
|
|
}
|
|
|
|
static int run_gp0_rect_intake_case(void) {
|
|
const char* name = "gp0-rect-intake";
|
|
static uint16_t expect[32 * 16];
|
|
psx_gpu_t* gpu = make_gpu();
|
|
psx_perf_counters_t counters;
|
|
int failed = 0;
|
|
|
|
if (!gpu) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
psx_perf_set_enabled(1);
|
|
psx_perf_take_frame(&counters); /* zero the window */
|
|
|
|
memset(gpu->vram, 0, PSX_GPU_VRAM_SIZE);
|
|
gp0_seed_texture(gpu);
|
|
gp0_texpage(gpu, 2); /* 15-bit, so a texel is itself */
|
|
|
|
if (gpu->texp_x != GP0_TEX_PAGE_X || gpu->texp_y != GP0_TEX_PAGE_Y || gpu->texp_d != 2) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=texpage-not-latched "
|
|
"got=(%u,%u,%u)\n", name, gpu->texp_x, gpu->texp_y, gpu->texp_d);
|
|
psx_gpu_destroy(gpu);
|
|
return 1;
|
|
}
|
|
|
|
/* ---- 1. GP0(65): RAW textured variable-size sprite, 32x16 at (20,10), UV (0,0).
|
|
RAW means the texel is written unmodified, so the expectation is literally the
|
|
texture this test wrote. ---- */
|
|
for (int dy = 0; dy < 16; dy++)
|
|
for (int dx = 0; dx < 32; dx++)
|
|
expect[dy * 32 + dx] = gp0_texel_at(dx, dy);
|
|
|
|
gp0(gpu, 0x65000000u);
|
|
gp0(gpu, (10u << 16) | 20u);
|
|
gp0(gpu, 0u); /* clut = 0, uv = (0,0) */
|
|
gp0(gpu, (16u << 16) | 32u);
|
|
|
|
if (gpu->state != GPU_STATE_RECV_CMD || gpu->cmd_args_remaining != 0) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=raw-sprite-left-command-open "
|
|
"state=%d remaining=%d\n",
|
|
name, gpu->state, gpu->cmd_args_remaining);
|
|
failed = 1;
|
|
}
|
|
|
|
failed |= gp0_check_pixels(name, "raw-sprite-pixels", gpu, 20, 10, 32, 16, expect);
|
|
|
|
/* ---- 2. GP0(64) at the identity modulator: hardware's blend returns the texel
|
|
unchanged at 0x80 per channel, so the SAME expectation must hold through the
|
|
modulate path. A modulation formula that stops being the identity at 0x80 fails
|
|
here, which catches modulation defects shared by multiple rasterizers. ---- */
|
|
gp0(gpu, 0x64808080u);
|
|
gp0(gpu, (60u << 16) | 20u);
|
|
gp0(gpu, 0u);
|
|
gp0(gpu, (16u << 16) | 32u);
|
|
|
|
failed |= gp0_check_pixels(name, "modulated-sprite-pixels", gpu, 20, 60, 32, 16, expect);
|
|
|
|
/* ---- 3. GP0(60): untextured variable-size rect. Three words, not four — the arm of
|
|
gpu_rect()'s argument count that a textured-only test never reaches. ---- */
|
|
for (int i = 0; i < 8 * 8; i++)
|
|
expect[i] = (uint16_t)(((0x7fu >> 3) << 10) | ((0x7fu >> 3) << 5) | (0x7fu >> 3));
|
|
|
|
gp0(gpu, 0x607f7f7fu);
|
|
gp0(gpu, (100u << 16) | 100u);
|
|
gp0(gpu, (8u << 16) | 8u);
|
|
|
|
if (gpu->state != GPU_STATE_RECV_CMD || gpu->cmd_args_remaining != 0) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=mono-rect-left-command-open "
|
|
"state=%d remaining=%d\n",
|
|
name, gpu->state, gpu->cmd_args_remaining);
|
|
failed = 1;
|
|
}
|
|
|
|
failed |= gp0_check_pixels(name, "mono-rect-pixels", gpu, 100, 100, 8, 8, expect);
|
|
|
|
/* ---- 4. GP0(7D): RAW 8x8 FIXED-size sprite in 4-bit CLUT mode — what PS1 font glyphs
|
|
actually are. Fixed size is a THREE-word command (no size word), so this is also the
|
|
size-arm of the argument count, and it goes through the CLUT indirection. ---- */
|
|
gp0_seed_clut4(gpu);
|
|
gp0_texpage(gpu, 0);
|
|
|
|
for (int dy = 0; dy < 8; dy++)
|
|
for (int dx = 0; dx < 8; dx++)
|
|
expect[dy * 8 + dx] = gp0_clut4_expected(gpu, 4 + dx, 4 + dy);
|
|
|
|
gp0(gpu, 0x7d000000u);
|
|
gp0(gpu, (200u << 16) | 200u);
|
|
gp0(gpu, ((uint32_t)((GP0_CLUT_Y << 6) | (GP0_CLUT_X / 16)) << 16) | (4u << 8) | 4u);
|
|
|
|
if (gpu->state != GPU_STATE_RECV_CMD || gpu->cmd_args_remaining != 0) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=fixed-sprite-left-command-open "
|
|
"state=%d remaining=%d\n",
|
|
name, gpu->state, gpu->cmd_args_remaining);
|
|
failed = 1;
|
|
}
|
|
|
|
failed |= gp0_check_pixels(name, "clut4-sprite-pixels", gpu, 200, 200, 8, 8, expect);
|
|
|
|
/* ---- 5. The counter contract. Four rects went in; the overlay must be able to say so.
|
|
`0 rect` on a frame that drew rects is how this regression was reported, and a
|
|
counter that cannot be trusted turns the next report into a guess. ---- */
|
|
psx_perf_take_frame(&counters);
|
|
|
|
if (counters.gpu_primitives[PSX_PERF_PRIM_RECT] != 4) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=rect-counter got=%u want=4\n",
|
|
name, counters.gpu_primitives[PSX_PERF_PRIM_RECT]);
|
|
failed = 1;
|
|
}
|
|
|
|
/* ---- 6. Stream integrity: a triangle submitted AFTER the rects must still land. If a
|
|
rect consumed the wrong number of words, this is what notices. ---- */
|
|
gp0(gpu, 0x20ffffffu);
|
|
gp0(gpu, (300u << 16) | 300u);
|
|
gp0(gpu, (300u << 16) | 340u);
|
|
gp0(gpu, (340u << 16) | 300u);
|
|
|
|
psx_perf_take_frame(&counters);
|
|
|
|
if (counters.gpu_primitives[PSX_PERF_PRIM_TRIANGLE] != 1) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=stream-desync-after-rects "
|
|
"triangles=%u want=1\n",
|
|
name, counters.gpu_primitives[PSX_PERF_PRIM_TRIANGLE]);
|
|
failed = 1;
|
|
}
|
|
|
|
if (!gpu->vram[305 + (305 * 1024)]) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=triangle-after-rects-not-drawn\n",
|
|
name);
|
|
failed = 1;
|
|
}
|
|
|
|
psx_perf_set_enabled(0);
|
|
psx_gpu_destroy(gpu);
|
|
|
|
if (failed)
|
|
return 1;
|
|
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
MASK BIT vs TEXTURED CONTENT — "turning accurate_mask_bit on must not make textures
|
|
disappear."
|
|
|
|
WHY THIS EXISTS
|
|
|
|
The reported symptom was that with accurate_mask_bit = true the PS1 BIOS drew its logo
|
|
but not the SONY COMPUTER ENTERTAINMENT text under it, and Xenogears lost its menus,
|
|
while untextured geometry kept drawing. Turning the setting off restored everything.
|
|
|
|
The existing mask cases (mask-from-texel-*, mask-contract-*) pin the WRITE and SKIP rules
|
|
over their input matrix, but they do it a pixel at a time against hand-built inputs. None
|
|
of them draws a textured SPRITE and asks the blunt question a user would: did the thing
|
|
appear? These do, and they do it over repeated draws, because the field report was of
|
|
degradation across a session rather than a wrong first frame.
|
|
|
|
THE INVARIANT
|
|
|
|
The accuracy flag enables the mask rules; GP0(E6) decides whether the game is USING them.
|
|
A game that never sends GP0(E6) must therefore render byte-identically whether the flag
|
|
is on or off — measured directly against the real BIOS, which never sets those bits at
|
|
all. And a sprite drawn into a clean destination must write every pixel it covers under
|
|
every GP0(E6) setting, because "check mask" only skips where bit 15 is ALREADY set.
|
|
*/
|
|
static int run_mask_bit_textured_sprite_case(void) {
|
|
const char* name = "mask-bit-textured-sprite";
|
|
int failed = 0;
|
|
|
|
/* ---- 1. Inert unless the game asks. Draw the same sprite with the flag off and on,
|
|
with GP0(E6) never sent, and require identical VRAM. This is the property the field
|
|
failure violated, and the one the real BIOS depends on. ---- */
|
|
{
|
|
psx_gpu_t* off = make_gpu();
|
|
psx_gpu_t* on = make_gpu();
|
|
|
|
psx_gpu_set_accuracy_flags(off, 0);
|
|
psx_gpu_set_accuracy_flags(on, PSX_GPU_ACCURACY_MASK_BIT);
|
|
|
|
for (psx_gpu_t** it = (psx_gpu_t*[]){off, on, NULL}; *it; it++) {
|
|
memset((*it)->vram, 0, PSX_GPU_VRAM_SIZE);
|
|
gp0_seed_texture(*it);
|
|
gp0_texpage(*it, 2);
|
|
|
|
/* Half the page carries STP, so the texel half of the write rule is exercised
|
|
rather than sitting at zero. */
|
|
for (int y = 0; y < 64; y++)
|
|
for (int x = 0; x < 64; x++)
|
|
if ((x + y) & 1)
|
|
(*it)->vram[(GP0_TEX_PAGE_X + x) + (y * 1024)] |= 0x8000u;
|
|
|
|
gp0(*it, 0x64808080u);
|
|
gp0(*it, (40u << 16) | 40u);
|
|
gp0(*it, 0u);
|
|
gp0(*it, (16u << 16) | 32u);
|
|
}
|
|
|
|
/* Bits 0-14 only. Bit 15 is EXPECTED to differ: with the flag on, a textured write
|
|
carries the texel's bit 15 into the destination, which is what hardware does and
|
|
what mask-from-texel exists to restore. Bit 15 is not displayed, so the
|
|
picture must be identical even though the halfwords are not — and "the picture is
|
|
identical" is precisely the claim the field failure disproved. */
|
|
for (size_t i = 0; i < PSX_GPU_VRAM_SIZE / sizeof(uint16_t); i++) {
|
|
if ((off->vram[i] & 0x7fff) != (on->vram[i] & 0x7fff)) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=flag-changed-visible-colour "
|
|
"at vram[%zu] off=%04x on=%04x\n",
|
|
name, i, off->vram[i], on->vram[i]);
|
|
failed = 1;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
psx_gpu_destroy(off);
|
|
psx_gpu_destroy(on);
|
|
}
|
|
|
|
/* ---- 2. A sprite into a CLEAN destination must appear in full, for every GP0(E6).
|
|
"Check mask" skips only where bit 15 is already set; a cleared buffer has none, so
|
|
every setting must draw all 512 pixels. ---- */
|
|
for (int e6 = 0; e6 < 4; e6++) {
|
|
psx_gpu_t* gpu = make_gpu();
|
|
int painted = 0;
|
|
|
|
psx_gpu_set_accuracy_flags(gpu, PSX_GPU_ACCURACY_MASK_BIT);
|
|
memset(gpu->vram, 0, PSX_GPU_VRAM_SIZE);
|
|
gp0_seed_texture(gpu);
|
|
gp0_texpage(gpu, 2);
|
|
|
|
for (int y = 0; y < 64; y++)
|
|
for (int x = 0; x < 64; x++)
|
|
if ((x + y) & 1)
|
|
gpu->vram[(GP0_TEX_PAGE_X + x) + (y * 1024)] |= 0x8000u;
|
|
|
|
gp0(gpu, 0xe6000000u | (uint32_t)e6);
|
|
|
|
gp0(gpu, 0x64808080u);
|
|
gp0(gpu, (40u << 16) | 40u);
|
|
gp0(gpu, 0u);
|
|
gp0(gpu, (16u << 16) | 32u);
|
|
|
|
for (int y = 40; y < 56; y++)
|
|
for (int x = 40; x < 72; x++)
|
|
if (gpu->vram[x + y * 1024])
|
|
painted++;
|
|
|
|
if (painted != 32 * 16) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=sprite-vanished E6=%d "
|
|
"painted=%d/512\n", name, e6, painted);
|
|
failed = 1;
|
|
}
|
|
|
|
psx_gpu_destroy(gpu);
|
|
}
|
|
|
|
/* ---- 2b. OVERDRAW without a clear, with the game asking for NOTHING (E6 = 0).
|
|
This is the case that actually discriminates, and the first version of this test
|
|
missed it: a sprite drawn into a CLEAN buffer draws in full even if the mask check is
|
|
spuriously active, because a cleared buffer has no bit 15 anywhere to trip it. The
|
|
failure only shows when a previous textured draw has already written bit 15 from its
|
|
texels and a SECOND, different draw has to overwrite it.
|
|
|
|
So: draw, then draw again with a different modulator, and require the second draw to
|
|
win everywhere — checked against the same two draws with the flag off. With the flag
|
|
on and no GP0(E6) sent, nothing may be skipped. ---- */
|
|
{
|
|
psx_gpu_t* off = make_gpu();
|
|
psx_gpu_t* on = make_gpu();
|
|
|
|
psx_gpu_set_accuracy_flags(off, 0);
|
|
psx_gpu_set_accuracy_flags(on, PSX_GPU_ACCURACY_MASK_BIT);
|
|
|
|
for (psx_gpu_t** it = (psx_gpu_t*[]){off, on, NULL}; *it; it++) {
|
|
memset((*it)->vram, 0, PSX_GPU_VRAM_SIZE);
|
|
gp0_seed_texture(*it);
|
|
gp0_texpage(*it, 2);
|
|
|
|
for (int y = 0; y < 64; y++)
|
|
for (int x = 0; x < 64; x++)
|
|
if ((x + y) & 1)
|
|
(*it)->vram[(GP0_TEX_PAGE_X + x) + (y * 1024)] |= 0x8000u;
|
|
|
|
/* First draw lays down bit 15 from the texels. */
|
|
gp0(*it, 0x64808080u);
|
|
gp0(*it, (40u << 16) | 40u);
|
|
gp0(*it, 0u);
|
|
gp0(*it, (16u << 16) | 32u);
|
|
|
|
/* Second draw, different modulator, same footprint. It must win everywhere. */
|
|
gp0(*it, 0x64204080u);
|
|
gp0(*it, (40u << 16) | 40u);
|
|
gp0(*it, 0u);
|
|
gp0(*it, (16u << 16) | 32u);
|
|
}
|
|
|
|
for (int y = 40; y < 56; y++) {
|
|
for (int x = 40; x < 72; x++) {
|
|
const size_t i = (size_t)x + (size_t)y * 1024u;
|
|
|
|
if ((off->vram[i] & 0x7fff) != (on->vram[i] & 0x7fff)) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=overdraw-blocked-by-mask "
|
|
"at (%d,%d) off=%04x on=%04x\n",
|
|
name, x, y, off->vram[i], on->vram[i]);
|
|
failed = 1;
|
|
y = 56;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
psx_gpu_destroy(off);
|
|
psx_gpu_destroy(on);
|
|
}
|
|
|
|
/* ---- 3. No accumulation. Redrawing a sprite over a destination that a previous draw
|
|
already mask-marked must keep producing the same picture, as long as the buffer is
|
|
cleared between frames the way a game clears it. A rule that degrades frame over frame
|
|
is the shape of the reported failure, so it gets its own assertion. ---- */
|
|
{
|
|
psx_gpu_t* gpu = make_gpu();
|
|
int first = -1;
|
|
|
|
psx_gpu_set_accuracy_flags(gpu, PSX_GPU_ACCURACY_MASK_BIT);
|
|
memset(gpu->vram, 0, PSX_GPU_VRAM_SIZE);
|
|
gp0_seed_texture(gpu);
|
|
gp0_texpage(gpu, 2);
|
|
|
|
for (int y = 0; y < 64; y++)
|
|
for (int x = 0; x < 64; x++)
|
|
if ((x + y) & 1)
|
|
gpu->vram[(GP0_TEX_PAGE_X + x) + (y * 1024)] |= 0x8000u;
|
|
|
|
/* set + check, the combination Silent Hill fog uses. */
|
|
gp0(gpu, 0xe6000003u);
|
|
|
|
for (int frame = 0; frame < 8; frame++) {
|
|
int painted = 0;
|
|
|
|
/* GP0(02) fill: how a game clears, and it ignores the mask bit on hardware.
|
|
If this stops clearing bit 15 the buffer silently becomes unwritable, which
|
|
is exactly the accumulation this case is here to catch. */
|
|
gp0(gpu, 0x02000000u);
|
|
gp0(gpu, (40u << 16) | 40u);
|
|
gp0(gpu, (16u << 16) | 32u);
|
|
|
|
gp0(gpu, 0x64808080u);
|
|
gp0(gpu, (40u << 16) | 40u);
|
|
gp0(gpu, 0u);
|
|
gp0(gpu, (16u << 16) | 32u);
|
|
|
|
for (int y = 40; y < 56; y++)
|
|
for (int x = 40; x < 72; x++)
|
|
if (gpu->vram[x + y * 1024] & 0x7fff)
|
|
painted++;
|
|
|
|
if (first < 0)
|
|
first = painted;
|
|
|
|
if (painted != first) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=degrades-over-frames "
|
|
"frame=%d painted=%d first=%d\n", name, frame, painted, first);
|
|
failed = 1;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (first <= 0) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=nothing-drawn-at-all\n", name);
|
|
failed = 1;
|
|
}
|
|
|
|
psx_gpu_destroy(gpu);
|
|
}
|
|
|
|
if (failed)
|
|
return 1;
|
|
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------------------
|
|
Semi-transparency, asserted against the HARDWARE RULE over its whole input matrix.
|
|
|
|
WHY THIS EXISTS, AND WHY PARITY CANNOT REPLACE IT
|
|
|
|
A Xenogears battle submits ~124 semi-transparent primitives per drawn frame, and the
|
|
only ones covering the whole screen are flat GP0(62) rectangles in mode 2 (B-F) — the
|
|
fade primitive. When the reported symptom is a whole-screen colour that strobes, the
|
|
blend rule is the first thing that has to be either convicted or cleared, and comparing
|
|
the three rasterizers cannot do it: they were all cloned from the same expression, so a
|
|
wrong rule reads green in every comparative case (the same blindness size cull
|
|
and the mask-from-texel bug were found through).
|
|
|
|
So this asserts the rule itself, from psx-spx, per 5-bit channel with saturation:
|
|
|
|
mode 0 (B + F) / 2 integer division, truncating
|
|
mode 1 B + F saturating at 31
|
|
mode 2 B - F saturating at 0
|
|
mode 3 B + F/4 F/4 truncating, saturating at 31
|
|
|
|
All 32x32 background/foreground pairs, all four modes, both rasterizers — 4096 pairs
|
|
per rasterizer. The channels are loaded with DIFFERENT values on purpose: a blend that
|
|
is right on grey and wrong across channels (a swapped shift, a mask that spans two
|
|
fields) passes every uniform-colour test ever written.
|
|
|
|
Driven through gpu_render_rect() rather than a synthesised expression because the point
|
|
is to test the shipping code path, including its float round-trip: the rasterizers hold
|
|
the channels as floats scaled by 8 and quantise back through BGR555(), and whether that
|
|
is exactly the integer rule at every one of the 1024 corners is precisely the question.
|
|
------------------------------------------------------------------------------------ */
|
|
static int blend_ref(int mode, int b, int f) {
|
|
int v;
|
|
|
|
switch (mode) {
|
|
case 0: v = (b + f) / 2; break;
|
|
case 1: v = b + f; break;
|
|
case 2: v = b - f; break;
|
|
default: v = b + (f / 4); break;
|
|
}
|
|
|
|
return (v < 0) ? 0 : ((v > 31) ? 31 : v);
|
|
}
|
|
|
|
/* 5-bit channels -> the packed BGR555 halfword the rasterizers read out of VRAM. */
|
|
static uint16_t blend_pack(int r, int g, int b) {
|
|
return (uint16_t)((r & 0x1f) | ((g & 0x1f) << 5) | ((b & 0x1f) << 10));
|
|
}
|
|
|
|
/* 5-bit channels -> the 24-bit command colour whose BGR555() is exactly those channels.
|
|
BGR555 keeps the top 5 bits of each byte, so << 3 is the exact inverse. */
|
|
static uint32_t blend_cmd_colour(int r, int g, int b) {
|
|
return (uint32_t)((r << 3) | ((g << 3) << 8) | ((b << 3) << 16));
|
|
}
|
|
|
|
static int check_blend_matrix(const char* name, int use_backend) {
|
|
psx_gpu_backend_t* backend = NULL;
|
|
psx_gpu_t* gpu = psx_gpu_create();
|
|
int failed = 0;
|
|
int mode;
|
|
|
|
if (!gpu) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_init(gpu, NULL);
|
|
gpu->draw_x1 = 0;
|
|
gpu->draw_y1 = 0;
|
|
gpu->draw_x2 = PSX_GPU_FB_WIDTH - 1;
|
|
gpu->draw_y2 = PSX_GPU_FB_HEIGHT - 1;
|
|
gpu->off_x = 0;
|
|
gpu->off_y = 0;
|
|
psx_gpu_set_accuracy_flags(gpu, 0);
|
|
|
|
if (use_backend) {
|
|
backend = armsx_hw_rt_create(gpu, 1);
|
|
|
|
if (!backend) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=backend-create\n", name);
|
|
psx_gpu_destroy(gpu);
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_set_backend(gpu, backend);
|
|
}
|
|
|
|
for (mode = 0; mode < 4 && !failed; mode++) {
|
|
int bi;
|
|
|
|
/* A sprite takes its blend mode from GPUSTAT bits 5-6, which is where GP0(E1) and
|
|
the last textured polygon leave it. Nothing else in GPUSTAT matters here. */
|
|
gpu->gpustat = (uint32_t)(mode << 5);
|
|
|
|
for (bi = 0; bi < 32 && !failed; bi++) {
|
|
int fi;
|
|
|
|
for (fi = 0; fi < 32; fi++) {
|
|
/* Three different pairs in one probe, so a channel that borrowed its
|
|
neighbour's value cannot come out equal by coincidence. */
|
|
const int br = bi, bg = (bi + 11) & 0x1f, bb = (bi + 23) & 0x1f;
|
|
const int fr = fi, fg = (fi + 7) & 0x1f, fb = (fi + 19) & 0x1f;
|
|
const uint16_t back = blend_pack(br, bg, bb);
|
|
const int px = 40, py = 40;
|
|
rect_data_t rect;
|
|
uint16_t got;
|
|
int wr, wg, wb;
|
|
|
|
memset(gpu->vram, 0, PSX_GPU_VRAM_SIZE);
|
|
|
|
if (use_backend) {
|
|
/* Seed the backend's own target through the hook the core uses, so the
|
|
destination it blends against is the same value the software path
|
|
sees in gpu->vram. */
|
|
gpu->vram[px + (py * PSX_GPU_FB_WIDTH)] = back;
|
|
backend->upload_vram(backend, 0, 0, PSX_GPU_FB_WIDTH, PSX_GPU_FB_HEIGHT,
|
|
gpu->vram, PSX_GPU_FB_WIDTH);
|
|
} else {
|
|
gpu->vram[px + (py * PSX_GPU_FB_WIDTH)] = back;
|
|
}
|
|
|
|
memset(&rect, 0, sizeof(rect));
|
|
rect.attrib = RA_TRANSP; /* flat, untextured, variable size */
|
|
rect.width = 4;
|
|
rect.height = 4;
|
|
rect.v0.x = (int16_t)(px - 1);
|
|
rect.v0.y = (int16_t)(py - 1);
|
|
rect.v0.c = blend_cmd_colour(fr, fg, fb);
|
|
|
|
/* The core dispatches to the backend at the COMMAND level, not inside
|
|
gpu_render_rect(), so a backend run has to be driven the same way the
|
|
GP0(6x) handler drives it. */
|
|
if (use_backend) {
|
|
backend->draw_rect(backend, gpu, &rect);
|
|
} else {
|
|
gpu_render_rect(gpu, rect);
|
|
}
|
|
|
|
if (use_backend) {
|
|
uint32_t stride = 0;
|
|
const uint16_t* surface = (const uint16_t*)backend->display_buffer(
|
|
backend, 0, 0, &stride);
|
|
|
|
got = surface[px + (py * (stride / 2u))];
|
|
} else {
|
|
got = gpu->vram[px + (py * PSX_GPU_FB_WIDTH)];
|
|
}
|
|
|
|
wr = blend_ref(mode, br, fr);
|
|
wg = blend_ref(mode, bg, fg);
|
|
wb = blend_ref(mode, bb, fb);
|
|
|
|
if ((got & 0x7fff) != blend_pack(wr, wg, wb)) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=blend-rule mode=%d "
|
|
"back=(%d,%d,%d) fore=(%d,%d,%d) want=(%d,%d,%d) "
|
|
"got=(%d,%d,%d) [%04x vs %04x]\n",
|
|
name, mode, br, bg, bb, fr, fg, fb, wr, wg, wb,
|
|
got & 0x1f, (got >> 5) & 0x1f, (got >> 10) & 0x1f,
|
|
got & 0x7fff, blend_pack(wr, wg, wb));
|
|
failed = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (use_backend) {
|
|
psx_gpu_set_backend(gpu, NULL);
|
|
armsx_hw_rt_destroy(backend);
|
|
}
|
|
|
|
psx_gpu_destroy(gpu);
|
|
|
|
if (failed)
|
|
return 1;
|
|
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
return 0;
|
|
}
|
|
|
|
static int run_blend_matrix_case(void) {
|
|
int failed = 0;
|
|
|
|
failed |= check_blend_matrix("blend-rule-software-4096-pairs", 0);
|
|
failed |= check_blend_matrix("blend-rule-hw-rt-1x-4096-pairs", 1);
|
|
|
|
return failed;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------------------
|
|
The drawing offset in force during rasterization is the one the command stream set.
|
|
|
|
WHY THIS EXISTS
|
|
|
|
A Xenogears battle frame draws 1800 primitives. On 10% of frames, 1799 of them rasterize
|
|
at drawing offset y=112 against a drawing area of (0,224)-(319,447), so the whole scene is
|
|
displaced above the clip rectangle and thrown away. Measured on device, the frame issues
|
|
THREE GP0(E5) writes where a good frame issues one:
|
|
|
|
good e5seq=070000@0 offhist=224:1806
|
|
bad e5seq=070000@0,000000@0,038000@1 offhist=0:1,112:1799
|
|
|
|
0x070000 is offset y=224 (E5 packs Y at bit 11). 0x038000 is offset y=112 under that same
|
|
layout -- and is byte-identical to that frame's GP0(E3) payload, where the identical bits
|
|
correctly mean row 224 because E3 packs Y at bit 10. So the third write carries a
|
|
draw-area-shaped payload into the offset register, and it arrives after exactly one
|
|
primitive has drawn, i.e. interleaved with drawing rather than during frame setup.
|
|
|
|
Two mechanisms remain and they need opposite fixes: either those three words genuinely
|
|
reach the GP0 port (and the defect is upstream, in what produces them), or this GPU
|
|
synthesises/mis-slots one of them (and the defect is the intake). This gate pins the
|
|
second half down. It feeds a stream through psx_gpu_write32() -- the real MMIO intake, not
|
|
a handler called directly -- shaped like the observed frame, and asserts:
|
|
|
|
* after every GP0(E5) the offset is EXACTLY what that word encoded, and
|
|
* the number of E5 executions equals the number of E5 words fed (nothing synthesised
|
|
or replayed), and
|
|
* every primitive rasterized under the offset in force when it was submitted.
|
|
|
|
The interleaved multi-word commands are the adversarial part. A handler that reads the
|
|
wrong buf[] slot, or an argument counter that lets a following word fall through into the
|
|
command dispatch, shows up here and nowhere else -- the existing cases all call
|
|
gpu_render_*() directly and never exercise the intake's state machine at all.
|
|
|
|
Deliberately NOT asserted: off_y == draw_y1. That is not a hardware invariant. This
|
|
repo's own gpu_prim_dump_capture.txt shows Silent Hill running off=(160,368) against
|
|
draw=(0,256)-(319,479) on every frame -- a centre-origin convention -- so a gate on that
|
|
rule would fire on correct games.
|
|
------------------------------------------------------------------------------------ */
|
|
#define GP0W(gpu, w) psx_gpu_write32((gpu), 0, (uint32_t)(w))
|
|
|
|
static uint32_t e5_word(int x, int y) {
|
|
return 0xe5000000u | (((uint32_t)y & 0x7ffu) << 11) | ((uint32_t)x & 0x7ffu);
|
|
}
|
|
|
|
static uint32_t e3_word(int x, int y) {
|
|
return 0xe3000000u | (((uint32_t)y & 0x1ffu) << 10) | ((uint32_t)x & 0x3ffu);
|
|
}
|
|
|
|
static uint32_t e4_word(int x, int y) {
|
|
return 0xe4000000u | (((uint32_t)y & 0x1ffu) << 10) | ((uint32_t)x & 0x3ffu);
|
|
}
|
|
|
|
/* One flat variable-size sprite: GP0(60) plus two argument words. */
|
|
static void feed_rect(psx_gpu_t* gpu, int x, int y, int w, int h) {
|
|
GP0W(gpu, 0x60808080u);
|
|
GP0W(gpu, ((uint32_t)(y & 0xffff) << 16) | (uint32_t)(x & 0xffff));
|
|
GP0W(gpu, ((uint32_t)(h & 0xffff) << 16) | (uint32_t)(w & 0xffff));
|
|
}
|
|
|
|
static int check_offset_stream(void) {
|
|
const char* name = "offset-stream-integrity";
|
|
psx_gpu_t* gpu = psx_gpu_create();
|
|
int failed = 0;
|
|
unsigned i;
|
|
|
|
if (!gpu) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=allocation\n", name);
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_init(gpu, NULL);
|
|
psx_gpu_set_accuracy_flags(gpu, 0);
|
|
memset(gpu->vram, 0, PSX_GPU_VRAM_SIZE);
|
|
|
|
/* The real frame's draw area: the second 320x224 buffer. */
|
|
GP0W(gpu, e3_word(0, 224));
|
|
GP0W(gpu, e4_word(319, 447));
|
|
|
|
/* --- offset #1, the value a good frame uses ------------------------------------- */
|
|
GP0W(gpu, e5_word(0, 224));
|
|
|
|
if (gpu->off_x != 0 || gpu->off_y != 224) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=e5-1 want=(0,224) got=(%d,%d)\n",
|
|
name, gpu->off_x, gpu->off_y);
|
|
failed = 1;
|
|
}
|
|
|
|
feed_rect(gpu, 8, 8, 4, 4);
|
|
|
|
/* A texpage latch and a full opaque quad: five words, the longest ordinary command. */
|
|
GP0W(gpu, 0xe1000000u);
|
|
GP0W(gpu, 0x28404040u);
|
|
GP0W(gpu, 0x00100010u);
|
|
GP0W(gpu, 0x00100030u);
|
|
GP0W(gpu, 0x00300010u);
|
|
GP0W(gpu, 0x00300030u);
|
|
|
|
/* --- offset #2 ------------------------------------------------------------------- */
|
|
GP0W(gpu, e5_word(0, 0));
|
|
|
|
if (gpu->off_x != 0 || gpu->off_y != 0) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=e5-2 want=(0,0) got=(%d,%d)\n",
|
|
name, gpu->off_x, gpu->off_y);
|
|
failed = 1;
|
|
}
|
|
|
|
feed_rect(gpu, 8, 8, 4, 4);
|
|
|
|
/* A VRAM fill (3 words, no data phase) and an upload (3 words plus a data phase) --
|
|
the two commands that drive the intake through RECV_DATA. An upload that consumes one
|
|
word too few or too many would spill the next command word into the pixel stream, or
|
|
leave a pixel word to be dispatched as a command; either shows up as a wrong offset or
|
|
a wrong E5 count below. */
|
|
GP0W(gpu, 0x02102030u);
|
|
GP0W(gpu, 0x00000000u);
|
|
GP0W(gpu, 0x00100010u);
|
|
|
|
GP0W(gpu, 0xa0000000u);
|
|
GP0W(gpu, 0x01000000u); /* dst (0,256) */
|
|
GP0W(gpu, 0x00010004u); /* 4x1 -> 4 halfwords -> 2 words */
|
|
GP0W(gpu, 0x11112222u);
|
|
GP0W(gpu, 0x33334444u);
|
|
|
|
/* --- offset #3, the one the device gets wrong ------------------------------------ */
|
|
GP0W(gpu, e5_word(0, 224));
|
|
|
|
if (gpu->off_x != 0 || gpu->off_y != 224) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=e5-3-after-upload want=(0,224) got=(%d,%d) "
|
|
"raw=%06x\n",
|
|
name, gpu->off_x, gpu->off_y, gpu->gp0_e5_raw);
|
|
failed = 1;
|
|
}
|
|
|
|
for (i = 0; i < 5; i++) {
|
|
feed_rect(gpu, 8, 8, 4, 4);
|
|
}
|
|
|
|
/* Nothing may have invented, replayed or swallowed an offset write. */
|
|
if (gpu->gp0_e5_count != 3) {
|
|
fprintf(stderr, "GPU_PARITY failed case=%s reason=e5-count want=3 got=%u\n",
|
|
name, gpu->gp0_e5_count);
|
|
failed = 1;
|
|
}
|
|
|
|
/* And every primitive must have drawn under the offset that was in force when it was
|
|
submitted: 1 sprite at y=224, then 1 at y=0, then 5 back at y=224. The quad is two
|
|
triangles, so it contributes 2 at y=224. */
|
|
{
|
|
uint32_t at224 = 0, at0 = 0, other = 0;
|
|
|
|
for (i = 0; i < gpu->off_hist_used && i < 4u; i++) {
|
|
if (gpu->off_hist_y[i] == 224) {
|
|
at224 += gpu->off_hist_n[i];
|
|
} else if (gpu->off_hist_y[i] == 0) {
|
|
at0 += gpu->off_hist_n[i];
|
|
} else {
|
|
other += gpu->off_hist_n[i];
|
|
}
|
|
}
|
|
|
|
if (at224 != 8 || at0 != 1 || other != 0) {
|
|
fprintf(stderr,
|
|
"GPU_PARITY failed case=%s reason=offset-in-force want=(224:8,0:1,other:0) "
|
|
"got=(224:%u,0:%u,other:%u) prims=%u\n",
|
|
name, at224, at0, other, gpu->frame_prims);
|
|
failed = 1;
|
|
}
|
|
}
|
|
|
|
psx_gpu_destroy(gpu);
|
|
|
|
if (failed)
|
|
return 1;
|
|
|
|
printf("GPU_PARITY passed case=%s\n", name);
|
|
return 0;
|
|
}
|
|
|
|
int main(void) {
|
|
int failed = 0;
|
|
poly_data_t flat = {.attrib = 0};
|
|
flat.v[0].c = flat.v[1].c = flat.v[2].c = 0x40a0f0;
|
|
failed |= run_case(
|
|
"flat",
|
|
(vertex_t){.x = 10, .y = 12, .c = 0x40a0f0},
|
|
(vertex_t){.x = 92, .y = 24, .c = 0x40a0f0},
|
|
(vertex_t){.x = 38, .y = 105, .c = 0x40a0f0},
|
|
flat,
|
|
0
|
|
);
|
|
|
|
poly_data_t shaded = {.attrib = PA_SHADED};
|
|
failed |= run_case(
|
|
"shaded-dithered",
|
|
(vertex_t){.x = 120, .y = 80, .c = 0x0000ff},
|
|
(vertex_t){.x = 220, .y = 100, .c = 0x00ff00},
|
|
(vertex_t){.x = 160, .y = 210, .c = 0xff0000},
|
|
shaded,
|
|
0
|
|
);
|
|
|
|
poly_data_t transparent = {.attrib = PA_TRANSP};
|
|
transparent.v[0].c = transparent.v[1].c = transparent.v[2].c = 0xffffff;
|
|
failed |= run_case(
|
|
"semi-transparent",
|
|
(vertex_t){.x = 300, .y = 40, .c = 0xffffff},
|
|
(vertex_t){.x = 410, .y = 70, .c = 0xffffff},
|
|
(vertex_t){.x = 340, .y = 170, .c = 0xffffff},
|
|
transparent,
|
|
1
|
|
);
|
|
|
|
/* The internal-resolution backend. hw-1x is the correctness gate the whole upscaling
|
|
feature rests on; the hw-2x/3x/4x cases prove the scaling is coherent with it.
|
|
3x is included on purpose — an erroneous *0.5 term hides at even multipliers. */
|
|
/* Non-negotiable: with the accuracy flags off, the new GP0(E6) latch and the dither
|
|
gate must be completely inert, so the default software path behaves as it always has. */
|
|
failed |= run_default_inertness_case("default-path-unchanged");
|
|
|
|
/* GP0(E6) bit 0 == 0 writes the texel's bit 15, not 0. Behavioural, not comparative:
|
|
both rasterizers used to get this wrong identically, so parity alone passed. */
|
|
failed |= run_mask_from_texel_case();
|
|
|
|
/* The same rule as a contract over its whole input matrix, because the GLES rasterizer's
|
|
shader is compiled from PSX_GPU_MASK_WRITE / PSX_GPU_MASK_SKIP and a shader cannot be
|
|
run here. Pins those two expressions to gpu.c's observed output. */
|
|
failed |= run_mask_contract_case();
|
|
|
|
/* Hardware drops polygons bigger than 1023x511. Behavioural for the same reason: all
|
|
three rasterizers were over-permissive identically. */
|
|
failed |= run_prim_size_case();
|
|
|
|
/* Texture blending truncates on hardware. Behavioural via a feedback loop: the
|
|
difference is one level and invisible on any single primitive. */
|
|
failed |= run_tex_modulate_case();
|
|
|
|
/* Semi-transparency against psx-spx's rule over all 32x32 pairs x 4 modes, per
|
|
rasterizer. Behavioural for the same reason as the two above: the three rasterizers
|
|
share one blend expression, so a wrong rule is invisible to every comparative case. */
|
|
failed |= run_blend_matrix_case();
|
|
|
|
/* The offset in force during rasterization is the one the stream set, through the real
|
|
MMIO intake and with multi-word commands interleaved. See the note above the case. */
|
|
failed |= check_offset_stream();
|
|
|
|
/* A polyline never ends on a count, only on a terminator the game has to send. The GP0
|
|
intake must survive one that never arrives without writing past gpu->buf[]. */
|
|
failed |= run_polyline_overrun_case();
|
|
|
|
/* Sprites, driven through the GP0 intake rather than by calling the rasterizer. Every
|
|
other case in this file starts at gpu_render_*(), so nothing here covered the path
|
|
that decides whether a rect is submitted at all — which is what "the BIOS draws its
|
|
logo but not its text, and the overlay says 0 rect" turned out to be about. */
|
|
failed |= run_gp0_rect_intake_case();
|
|
|
|
/* A game that asks the GPU for its own draw-environment registers must be told the
|
|
truth, in the register's own bit layout. Exhaustive over every retained payload of
|
|
E2/E3/E4/E5 — the Xenogears battle clipping was this readback repacking the offset
|
|
in the wrong layout, and only an against-spec sweep can see a bug both halves of a
|
|
differential pair share. */
|
|
failed |= run_gpuinfo_roundtrip_case();
|
|
|
|
/* GPUSTAT must tell the game the video mode it actually set — a zeroed mirror told
|
|
every title it was on a 256-wide screen. */
|
|
failed |= run_gp1_mode_mirror_case();
|
|
failed |= run_gp1_vrange_case();
|
|
|
|
/* Turning accurate_mask_bit on must not make textured content disappear — the setting
|
|
is inert until a game sends GP0(E6), and a sprite into a clean buffer draws in full
|
|
under every E6. The mask cases above pin the rules per pixel; this asks whether the
|
|
sprite actually shows up, and whether it keeps showing up frame after frame. */
|
|
failed |= run_mask_bit_textured_sprite_case();
|
|
|
|
failed |= run_backend_case("hw-1x-matches-software", 1);
|
|
|
|
/* Same gate with the opt-in accuracy fixes on. This is what stops the mask bit or the
|
|
dither gate being implemented in one rasterizer and not the other. */
|
|
g_test_accuracy = PSX_GPU_ACCURACY_MASK_BIT | PSX_GPU_ACCURACY_DITHER_GATE |
|
|
PSX_GPU_ACCURACY_PRIM_SIZE | PSX_GPU_ACCURACY_TEX_MODULATE;
|
|
failed |= run_backend_case("hw-1x-matches-software-accurate", 1);
|
|
failed |= run_scale_case("hw-2x-scale-coherent-accurate", 2);
|
|
g_test_accuracy = 0;
|
|
failed |= run_scale_case("hw-2x-scale-coherent", 2);
|
|
failed |= run_scale_case("hw-3x-scale-coherent", 3);
|
|
failed |= run_scale_case("hw-4x-scale-coherent", 4);
|
|
|
|
/* Every sub-pixel of an axis-aligned primitive, not just the block's top-left corner.
|
|
This is what catches a half-pixel or off-by-one scissor error: the scale-coherence
|
|
cases above only sample 1 render-target pixel in S^2 and would miss it. */
|
|
failed |= run_block_uniformity_case("hw-2x-blocks-uniform", 2);
|
|
failed |= run_block_uniformity_case("hw-3x-blocks-uniform", 3);
|
|
failed |= run_block_uniformity_case("hw-4x-blocks-uniform", 4);
|
|
|
|
if (failed)
|
|
return 1;
|
|
puts("GPU_PARITY all cases passed");
|
|
return 0;
|
|
}
|