Files
psxrecomp/runtime/src/gpu.c
T
Matthew Stanley dcb63cb570 gpu/a0_history: add per-upload frame stamp (load-vs-upload ordering proof)
a0_history had no timestamp, so an upload's timing couldn't be compared against
the CD-load timeline. Add frame_stamp (s_frame_count at the upload) to the entry,
return it from gpu_get_a0_src, and emit "frame" in the a0_history JSON.

This pinned the MMX5 font bug: the (896/832/960,256) font uploads run at frames
1374/1381/1388 reading zero/0x77 garbage, while the CD delivers that buffer run
(LBA 10473 -> 0x800D5208..0x800DD208, real data) at frame ~1508 — the upload
races ~130 frames ahead of the font's CD load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:51:32 -07:00

3217 lines
140 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* gpu.c — PS1 GPU hardware simulation (Phase 3, Step 1).
*
* Implements:
* - GPUSTAT register with correct bit semantics
* - GP1 commands 00h-08h (reset, display config)
* - VRAM storage (1024x512 x 16-bit)
* - GP0 command write — ABORTS (not yet implemented)
* - GPUREAD — returns last latched value
*
* Reference: nocash PSX specs, DuckStation src/core/gpu.cpp
*/
#include "gpu.h"
#include "gpu_sw_renderer.h"
#include "gpu_render.h"
#include "crash_trace.h"
#include "debug_server.h"
#include "cpu_state.h"
#include "event_ring.h"
#include "color_lut.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---- VRAM ---- */
static uint16_t vram[1024 * 512];
/* ---- GP0 state machine ---- */
typedef enum {
GP0_IDLE,
GP0_COLLECTING,
GP0_VRAM_WRITE,
GP0_POLYLINE_MONO, /* collecting mono polyline vertices */
GP0_POLYLINE_SHADED /* collecting shaded polyline color+vertex pairs */
} Gp0State;
static Gp0State gp0_state;
static uint64_t gp0_write_count;
static uint64_t gp0_nop_count, gp0_fill_count, gp0_draw_count, gp0_env_count, gp0_copy_count;
static uint32_t gp0_cmd_buf[16]; /* max fixed-length command is 12 words */
static int gp0_words_collected;
static int gp0_words_needed;
static uint32_t gp0_next_source_addr = 0xFFFFFFFFu;
static uint32_t gp0_cmd_source_addr = 0xFFFFFFFFu;
/* ---- Widescreen proportion correction --------------------------------------
* Active only when [video] aspect_ratio != 4:3 AND the game's [widescreen]
* block opts in (see config_loader.h). Two mechanisms, both driven from the
* game.toml so nothing here is game-specific:
*
* 1. Tagged character/billboard prims. The recompiler emits a
* psx_ws_sprite_tag(cpu) call at the entry of each configured
* sprite-tag function ($a0 = prim pointer, scratchpad holds the prim's
* GTE-projected anchor). The game computes the prim's screen-pixel
* offsets/widths itself — which our GTE X-squash never sees — so tagged
* prims get every X re-squashed around their own anchor at execution
* time, restoring correct proportions on the stretched present.
*
* 2. Untagged textured rects (SPRT family). These never went through the
* GTE at all (HUD, menus — pure screen space), so they're squashed
* around the display centre, presenting at native proportions.
* Untextured TILEs are never touched: full-screen fades/flashes must
* keep covering the whole frame.
*
* The tag table is keyed by the prim's guest address (the DMA linked-list
* walk reports each word's source address) and stamped with the frame
* counter, so stale entries age out without an explicit clear. */
static int32_t ws_xnum = 1, ws_xden = 1; /* X squash factor; 1/1 = off */
static uint32_t ws_anchor_addr = 0; /* scratchpad addr of anchor SXY */
static int ws_hud_sprt = 0; /* edge-anchor untagged HUD SPRTs */
/* Wide-aspect mode: 0 = off (4:3 identity), 1 = squash (legacy hack — compress
* a wider FOV into the 320 frame, present stretched), 2 = native-wide (render
* the wider FOV into an actually wider frame and present 1:1; the GTE is NOT
* squashed — the frame is widened via draw_offset/draw_area and the display
* read). ws_cfg_num/den hold the configured aspect that drives the wide extent
* in native-wide mode. */
static int ws_mode = 0;
static int ws_cfg_num = 4, ws_cfg_den = 3;
#define WS_TAG_BUCKETS 4096 /* power of two */
#define WS_TAG_PROBES 8
#define WS_FMV_HYSTERESIS 30 /* frames a colour MDEC decode pins 4:3 */
typedef struct { uint32_t key; uint32_t stamp; int32_t anchor_x; } WsTag;
static WsTag ws_tags[WS_TAG_BUCKETS];
static uint32_t ws_last_tag_stamp = (uint32_t)-1000; /* frame of newest tag */
extern uint64_t s_frame_count; /* defined in debug_server.c */
extern int mdec_recently_active(uint32_t within_frames); /* mdec.c */
static int ws_configured(void) { return ws_xnum != ws_xden; }
/* Any wide mode engaged (squash or native-wide). Drives the FMV/menu 4:3
* pillarbox + the game-vs-2D detector, which both modes share. */
static int ws_engaged(void) { return ws_mode != 0; }
/* Forward decls: defined later but used by psx_ws_backdrop_x above them. */
static int32_t ws_scale_about(int32_t x, int32_t ax);
static int32_t ws_disp_w(void);
/* Gameplay vs full-2D screen. Character/billboard prims tag (psx_ws_sprite_tag)
* within the last couple of frames => the actor render funnel is running =>
* gameplay. A menu/title/save screen draws no tagged prims. The 2-frame window
* makes this frame-stable regardless of per-prim ordering within a frame. */
/* Full-2D widescreen ([widescreen] full_2d). A pure 2D sprite game (e.g. MMX6)
* never emits the sprite-tag hook the 3D gameplay detector keys on, so every
* in-game frame would be misclassified as a "full-2D menu" and pillarboxed 4:3.
* When this is set, treat every in-game frame as gameplay so native-wide engages
* and the 2D scene presents widescreen. Set once from game.toml at widescreen-
* engage (gpu_ws_set_full_2d); PSX_WS_FORCE_2D=1 forces it on for testing. */
static int ws_full_2d = 0;
void gpu_ws_set_full_2d(int on) { ws_full_2d = on ? 1 : 0; }
/* Full-2D tile-engine mode (MMX6): config [widescreen] full_2d, or the PSX_WS_FORCE_2D
* test override. Distinct from ws_game_mode (which also fires on the 3D sprite-tag path,
* e.g. Tomba) — only true full-2D games get the BG tile-budget reveal cap. */
static int ws_full_2d_mode(void) {
static int env = -1;
if (env < 0) { const char *e = getenv("PSX_WS_FORCE_2D"); env = (e && e[0] == '1') ? 1 : 0; }
return ws_full_2d || env;
}
static int ws_game_mode(void) {
if (ws_full_2d_mode()) return 1;
return (uint32_t)s_frame_count - ws_last_tag_stamp <= 2;
}
/* True when the current frame is presented at native 4:3 (NOT stretched), so
* ALL squash must be suppressed and the content rendered pixel-native:
* - FMV video (24-bit, or streamed 15-bit colour MDEC), and
* - full-2D screens (menus/title/save — no character billboards this frame).
* Coupling squash to this exact predicate keeps content and present in lock-
* step: we squash IFF we stretch. Per-prim center-squash mangles composite 2D
* UI (dialog boxes built from tiled cap/middle pieces), so such screens get
* zero squash + a 4:3 pillarbox instead. The FMV check is cached per frame;
* game_mode is a cheap live check. */
int gpu_ws_present_native_43(void) {
if (!ws_engaged()) return 0;
if (!ws_game_mode()) return 1; /* full-2D screen */
static uint32_t fmv_frame_cache = 0xFFFFFFFFu;
static int fmv_cached = 0;
uint32_t f = (uint32_t)s_frame_count;
if (f != fmv_frame_cache) {
fmv_frame_cache = f;
GpuDisplayInfo di; gpu_get_display_info(&di);
fmv_cached = di.depth24 || mdec_recently_active(WS_FMV_HYSTERESIS);
}
return fmv_cached;
}
/* Squash applies only when configured AND the frame is being stretched. */
static int ws_active(void) { return ws_configured() && !gpu_ws_present_native_43(); }
/* ----- Native-wide (mode 2) ------------------------------------------------
* Render the wider FOV into an actually wider frame instead of squashing it.
* Engaged on game frames only (FMV/menu frames pillarbox 4:3 like squash). The
* frame grows symmetrically: each side by OFFSET display-pixels, total by
* EXTRA = 2*OFFSET. OFFSET = round(W*(3*num-4*den)/(8*den)) where W is the live
* display width — derived from the aspect so it generalises past 16:9 and
* tracks display-mode changes; EXTRA is forced even so OFFSET is integral
* (e.g. W=320 @ 16:9 -> OFFSET=53, EXTRA=106, frame 320 -> 426). The shift is
* applied via the GPU draw_offset (so 3D and 2D move together and stay
* aligned), the draw-area widens by EXTRA so the shifted content is not clipped
* on the right, and the present widens the display read by EXTRA. 0 when
* native-wide is inactive (4:3 / boot / squash mode / FMV / full-2D). */
int ws_native_wide_active(void) {
return ws_mode == 2 && !gpu_ws_present_native_43();
}
static int ws_nw_offset(void) {
if (!ws_native_wide_active()) return 0;
int numr = 3 * ws_cfg_num - 4 * ws_cfg_den; /* > 0 for aspects wider than 4:3 */
if (numr <= 0) return 0;
int w = (int)ws_disp_w();
int v = (w * numr + 4 * ws_cfg_den) / (8 * ws_cfg_den); /* round-to-nearest */
return v;
}
int ws_nw_extra(void) { return 2 * ws_nw_offset(); }
/* Per-side X cull margin in screen/world units (the game's draw classifier
* works in objX-camX where 1 unit ~= 1 native-4:3 screen pixel). The squash
* shows a half-view of 160/s pixels (s = squash factor = ws_xnum/ws_xden), so
* the visible edge moves out by 160*(1/s - 1) = 160*(xden-xnum)/xnum. Widening
* the cull window by this restores the original off-screen margin at the new
* edge. 0 whenever squash is inactive (4:3/boot/menu/FMV) → original cull. */
/* Diagnostic override (8C): when >= 0, psx_ws_x_margin() returns this value
* unconditionally so a probe can sweep the cull margin live (0 = force 4:3
* cull while still stretching; large = over-draw) at a fixed camera position.
* -1 = normal computed margin. */
static int ws_margin_override = -1;
void gpu_ws_set_margin_override(int v) { ws_margin_override = v; }
int psx_ws_x_margin(void) {
if (ws_margin_override >= 0) return ws_margin_override;
/* Native-wide: widen the world-space draw cull by the per-side reveal
* (== the centering OFFSET in screen px) so the game SUBMITS the geometry
* that previously fell outside the 4:3 cull window; the wide compositor then
* rasterizes it into the revealed margins. Same recompiler emit sites as the
* squash path ([widescreen.cull]); 0 at 4:3 so the cull stays byte-identical. */
if (ws_native_wide_active()) return ws_nw_offset();
if (!ws_active()) return 0;
return (160 * (ws_xden - ws_xnum) + ws_xnum / 2) / ws_xnum;
}
/* ---- MMX6 2D background tile-loop widen ([widescreen.bg2d]) ---------------
* Mega Man X6 is a pure-2D sprite engine that renders only a 4:3 (320px) field
* of view — there is no overscan to "reveal." Its per-layer background renderer
* (FUN_800270d0) draws `count` 16px tile columns × 16 rows from a start tile
* column / start screen-x derived from the camera scroll. To produce a TRUE
* wider FOV we widen that loop so it draws extra columns on BOTH sides of the
* 320 view, filling the 16:9 reveal margins with real adjacent stage. The three
* helpers below are hooked at the renderer's count / start-col / start-x
* instructions by the recompiler (gen-time). They are IDENTITY unless native-
* wide is engaged on a ~320 screen, so 4:3 (and the engine's own 512 hi-res
* mode, which already draws 33 columns) stay byte-identical.
*
* LEFT = per-side reveal in whole 16px tile columns = ceil(nw_offset / 16). The
* loop start moves LEFT columns earlier (tile col -LEFT in the 64-col ring,
* screen-x -LEFT*16) and the count grows by 2*LEFT, so existing columns keep
* their screen positions (stay aligned with objects/player) while new columns
* are prepended left and appended right. The 64-column tile ring (mask 0x3f) is
* far wider than the visible window, so the prepended columns fetch the correct
* already-streamed adjacent tiles. */
static int ws_mmx6_left_cols(void) {
if (!ws_native_wide_active()) return 0;
/* Only the ~320 gameplay mode; the engine's 512 hi-res mode (title) draws
* its own 33 columns and centres itself — never double-shift it. */
if (ws_disp_w() > 384) return 0;
int off = ws_nw_offset(); /* per-side reveal in screen px */
if (off <= 0) return 0;
return (off + 15) / 16; /* ceil to whole tile columns */
}
/* HOST-SIDE reveal mode (elective, default ON): the guest-side widen below overruns
* the engine's 1024-slot BG packet buffer / 999-tile cap in dense stages (void/stale
* columns). When host-side mode is active we keep the GUEST renderer at its NATIVE 21
* columns (these three helpers become IDENTITY → guest buffer/OT/cap byte-identical)
* and draw the reveal margins ourselves into the wide surface (gpu_ws_mmx6_emit_reveal).
* The stream hooks below stay active either way (they widen the tile RING, which the
* host-side decoder reads). Toggle via gpu_ws_mmx6_set_hostside(); 0 = old guest-widen. */
static int g_mmx6_hostside = 0; /* DEFAULT OFF: host-side reveal was the wrong approach
* (garbled tpage + stale early ring read). The buffer-
* relocation fix keeps the engine's correct guest-widen
* render instead. Code kept inert pending removal. */
void gpu_ws_mmx6_set_hostside(int on) { g_mmx6_hostside = on ? 1 : 0; }
static int mmx6_hostside_active(void) {
return g_mmx6_hostside && ws_native_wide_active() && ws_disp_w() <= 384
&& ws_nw_offset() > 0;
}
/* Column count: native (21) under host-side mode; else base + both-side reveal.
* Also drives the once-per-frame ring-freshness refill (mmx6_bg_refill_tick): this
* hook fires at the start of each layer's render, so the first call of a new frame
* re-streams the widened window for all 3 layers right before they are drawn. */
static void mmx6_bg_refill_tick(void); /* defined below (ring-freshness fix) */
int psx_ws_mmx6_bg_cols(int base) { mmx6_bg_refill_tick(); return mmx6_hostside_active() ? base : base + 2 * ws_mmx6_left_cols(); }
/* Start tile column: native under host-side mode; else LEFT earlier (64-col ring). */
int psx_ws_mmx6_bg_startcol(int col) { return mmx6_hostside_active() ? col : ((col - ws_mmx6_left_cols()) & 0x3f); }
/* Start screen-x: native under host-side mode; else LEFT*16 further left. */
int psx_ws_mmx6_bg_startx(int x) { return mmx6_hostside_active() ? x : (x - ws_mmx6_left_cols() * 16); }
/* Tile-RING STREAMER widen (same [widescreen.bg2d], FUN_800273e4). The renderer
* above now draws LEFT extra columns each side, but the engine's 64-column tile
* ring is only refreshed at its leading edge: the streamer re-streams ONE column
* per side per frame at world-X scrollX-16 (left) and scrollX+16+width (right),
* so the columns the widened renderer reaches are never populated (they show
* empty=black or stale=old tiles, the "stale margins"). Push the streamed left/
* right edge out by LEFT*16 so the ring stays valid across the widened window as
* the camera scrolls (the incremental streamer fills each newly-leading column).
* Identity (no extra streaming) at 4:3 / 512 hi-res, so the ring is byte-identical
* there. The 64-col ring has ample slack (visible ~21 cols) for ±LEFT more. */
int psx_ws_mmx6_bg_stream_left(int x) { return x - ws_mmx6_left_cols() * 16; }
int psx_ws_mmx6_bg_stream_right(int x) { return x + ws_mmx6_left_cols() * 16; }
/* ===== MMX6 BG packet-buffer RELOCATION ([widescreen.bg2d] bufbase_site/cap_site) ===
* The widened BG render (29 cols) overruns the engine's BG packet double-buffer (driver
* base 0x800B91C0, stride 0x4000 = 1024 tile slots/buffer) and its per-frame 1000-tile cap
* (FUN_800270d0) in dense stages -> the renderer returns early -> void/garbled columns.
* Fix: when the widen is active, relocate the double-buffer into a larger, proven-free RAM
* region (the 828KB heap->stack gap [0x800EA000,0x801B9000), measured 2026-06-21) and raise
* the cap to match. The packet TEMPLATE (opcode 0x7D at word1) is uniform, so we seed the
* relocated slots once. The OT links point at the relocated packets and the GPU DMA reads
* them from guest RAM exactly as before. IDENTITY (no relocation, cap 1000) at 4:3 / 512 /
* when the widen is off -> 4:3 and every other game byte-identical. */
#define MMX6_RELOC_BASE 0x80140000u /* mid the proven-free heap->stack gap */
#define MMX6_RELOC_STRIDE 0x8000u /* 2048 tile slots/buffer (was 0x4000=1024) */
#define MMX6_RELOC_CAP 1800 /* per-frame BG tile cap (was 1000; <= 2048) */
extern void psx_write_word(uint32_t addr, uint32_t val);
/* PARKED (default 0): relocation moves the BG packets to a larger free buffer and the
* prims DO render from there (census-confirmed), but the engine's OT chain / per-slot
* DR_TPAGE primitives are keyed to the original buffer layout, so relocated BG draws
* BLACK (wrong/absent texpage). Needs the OT-chain + DR_TPAGE relocation REd before it
* can ship. Toggle via mmx6_reveal {reloc:1} for continued debugging. When 0 the bufbase/
* cap hooks are identity -> the committed guest-widen path (correct + fresh, dense-stage
* overflow) is restored byte-for-byte. */
static int g_mmx6_reloc = 0;
void gpu_ws_mmx6_set_reloc(int on) { g_mmx6_reloc = on ? 1 : 0; }
int gpu_ws_mmx6_reloc_get(void) { return g_mmx6_reloc; }
static int mmx6_bg_reloc_active(void) {
return g_mmx6_reloc && !g_mmx6_hostside && ws_mmx6_left_cols() > 0;
}
/* Hook at the driver's buffer-address addu (0x80026DC4): remap base 0x800B91C0 + bufidx*0x4000
* to the relocated buffer (larger stride). Seeds the relocated slots' uniform SPRT template
* (word1 = 0x7D000000) once per buffer. */
int psx_ws_mmx6_bg_bufbase(int addr) {
if (!mmx6_bg_reloc_active()) return addr;
uint32_t a = (uint32_t)addr;
uint32_t bufidx = (a - 0x800B91C0u) >> 14; /* 0 or 1 */
if (bufidx > 1u) return addr; /* unexpected -> leave untouched */
uint32_t newbase = MMX6_RELOC_BASE + bufidx * MMX6_RELOC_STRIDE;
static int seeded[2] = { 0, 0 };
if (!seeded[bufidx]) {
/* Seed BOTH template fields the renderer does NOT fully rewrite:
* word0 = num_words(0x03, the libgpu OT header read by the GPU DMA) | next-link
* (low 24; the renderer rebuilds active links each frame, but the head
* packet's tail-link falls back to this default — terminate per buffer);
* word1 = SPRT16 opcode 0x7D (the renderer only RMWs its semi bit).
* word2(xy)/word3(uv/clut) are written per draw, so they need no template. */
uint32_t nslots = MMX6_RELOC_STRIDE / 0x10u;
for (uint32_t s = 0; s < nslots; s++) {
uint32_t next = (s + 1u < nslots)
? ((newbase + (s + 1u) * 0x10u) & 0xFFFFFFu)
: 0xFFFFFFu; /* terminate the default chain */
psx_write_word(newbase + s * 0x10u + 0u, 0x03000000u | next);
psx_write_word(newbase + s * 0x10u + 4u, 0x7D000000u);
}
seeded[bufidx] = 1;
}
return (int)newbase;
}
/* Hook at the BG cap slti (0x80027278): counter < cap. Raises the cap to MMX6_RELOC_CAP when
* relocated (the bigger buffer can hold them); plain 1000 otherwise (byte-identical). */
int psx_ws_mmx6_bg_undercap(int counter) {
int cap = mmx6_bg_reloc_active() ? MMX6_RELOC_CAP : 1000;
return (counter < cap) ? 1 : 0;
}
/* ===== MMX6 host-side reveal columns ====================================== *
* Keep the guest BG renderer native (21 cols) and draw the ±LEFT reveal-margin
* columns ourselves, straight into the wide compositor surface, by replaying the
* engine's own tile decode over guest RAM (FUN_800270d0). Guest packet buffer / OT /
* 999-cap and the 4:3 image are untouched (no guest mutation; elective; 4:3 identical).
*
* The only field NOT in the tile packet is the texture PAGE (selected indirectly via
* the tile's OT slot). We recover it empirically: as the guest draws its native BG
* tiles we record clut -> {tpage, draw state} (clut<->tpage is 1:1 in this engine).
* Reveal tiles resolve tpage by clut. Engine addresses (SLUS-01395):
* layer struct 0x800971F8 + layer*0x54 (scrollX +0xa, scrollY +0xe, parent +0x52)
* tile ring 0x800A21B8 + layer*0x1000 (u16[(col&0x3f) + (row&0x1f)*0x40])
* tile attr [scratchpad 0x1F80000C] + (tile&0x3fff)*4 (>>0x18 == 0xff -> skip) */
extern uint32_t psx_read_word(uint32_t addr);
extern uint16_t psx_read_half(uint32_t addr);
extern uint8_t psx_read_byte(uint32_t addr);
#define MMX6_CLUT_N 0x8000
#define MMX6_BG_BUF_LO 0x000B91C0u /* BG packet buffer (masked), driver base 0x800B91C0 */
#define MMX6_BG_BUF_HI 0x000C11C0u /* + 2 * 0x4000 */
typedef struct { uint16_t tpage; uint32_t color24; uint8_t semi_mode; uint8_t flags; } Mmx6ClutEnt;
static Mmx6ClutEnt *g_mmx6_clut = NULL; /* flags: bit0 valid, bit1 raw_texture */
/* Record clut -> tpage/state for a native BG SPRT16 (called from the GP0 handler). */
void gpu_ws_mmx6_bg_record(uint32_t src_addr, uint16_t clut, uint16_t tpage,
uint32_t color24, int semi_mode, int raw) {
uint32_t a = src_addr & 0x1FFFFFFFu;
if (a < MMX6_BG_BUF_LO || a >= MMX6_BG_BUF_HI) return; /* only background tiles */
if (!g_mmx6_clut) {
g_mmx6_clut = (Mmx6ClutEnt *)calloc(MMX6_CLUT_N, sizeof(Mmx6ClutEnt));
if (!g_mmx6_clut) return;
}
Mmx6ClutEnt *e = &g_mmx6_clut[clut & (MMX6_CLUT_N - 1)];
e->tpage = tpage; e->color24 = color24;
e->semi_mode = (uint8_t)(semi_mode & 3);
e->flags = (uint8_t)(1u | (raw ? 2u : 0u));
}
int gpu_ws_mmx6_reveal_is_active(void) { return mmx6_hostside_active(); }
/* Diagnostics (debug command mmx6_reveal). */
static long g_mmx6_emit_tiles = 0; /* tiles drawn in the last reveal pass */
static long g_mmx6_emit_calls = 0; /* reveal-pass invocations this run */
int gpu_ws_mmx6_hostside_get(void) { return g_mmx6_hostside; }
long gpu_ws_mmx6_emit_tiles(void) { return g_mmx6_emit_tiles; }
long gpu_ws_mmx6_emit_calls(void) { return g_mmx6_emit_calls; }
int gpu_ws_mmx6_clut_count(void) {
if (!g_mmx6_clut) return 0;
int n = 0;
for (int i = 0; i < MMX6_CLUT_N; i++) if (g_mmx6_clut[i].flags & 1) n++;
return n;
}
/* Draw the ±LEFT reveal columns for all 3 BG layers into the wide surface for the
* vertical band being cleared (band_y / base_x from the GP0 fill). Pure guest reads. */
void gpu_ws_mmx6_emit_reveal(int base_x, int band_y) {
if (!mmx6_hostside_active() || !g_mmx6_clut) return;
int left = ws_mmx6_left_cols();
if (left <= 0) return;
uint32_t attr_base = psx_read_word(0x1F80000Cu);
if (attr_base == 0) return;
g_mmx6_emit_calls++;
g_mmx6_emit_tiles = 0;
sw_wide_set_target(base_x); /* g_wide_cur = this band's surface */
for (int layer = 0; layer < 3; layer++) {
uint32_t ls = 0x800971F8u + (uint32_t)layer * 0x54u;
int sx = (int16_t)psx_read_half(ls + 0xa);
int sy = (int16_t)psx_read_half(ls + 0xe);
int8_t parent = (int8_t)psx_read_byte(ls + 0x52);
if (parent >= 0) { /* linked layer: add parent's scroll */
uint32_t lp = 0x800971F8u + (uint32_t)(uint8_t)parent * 0x54u;
sx += (int16_t)psx_read_half(lp + 0xa);
sy += (int16_t)psx_read_half(lp + 0xe);
}
int sxr = sx; if (sxr < 0) sxr += 0xf; int start_col = sxr >> 4;
int sub_x = -(sx & 0xf);
int syr = sy; if (syr < 0) syr += 0xf; int start_row = syr >> 4;
int sub_y = -(sy & 0xf);
uint32_t ring = 0x800A21B8u + (uint32_t)layer * 0x1000u;
for (int side = 0; side < 2; side++) {
int ci0 = (side == 0) ? -left : 21; /* native draws cols 0..20 */
for (int k = 0; k < left; k++) {
int ci = ci0 + k; /* column index relative to native start */
int col = start_col + ci;
int scr_x = sub_x + ci * 16;
for (int row = 0; row < 16; row++) {
int trow = start_row + row;
uint16_t tile = psx_read_half(ring
+ (uint32_t)((col & 0x3f) * 2)
+ (uint32_t)((trow & 0x1f) * 0x80));
if (tile == 0) continue;
uint32_t attr = psx_read_word(attr_base + (uint32_t)((tile & 0x3fff) * 4));
if ((attr >> 0x18) == 0xff) continue;
uint16_t clut = (uint16_t)(((((attr & 0xf000) >> 6) + 0x7980
+ ((attr >> 0x18) & 0x40) * 0x10)) | ((attr >> 8) & 0xf));
Mmx6ClutEnt *e = &g_mmx6_clut[clut & (MMX6_CLUT_N - 1)];
if (!(e->flags & 1)) continue; /* tpage not learned yet (warmup) */
int u0 = (int)((attr >> 0xc) & 0xf0);
int v0 = (int)((attr >> 0x10) & 0xf0);
uint16_t clut_x = (uint16_t)((clut & 0x3f) * 16);
uint16_t clut_y = (uint16_t)((clut >> 6) & 0x1ff);
int semi = (tile & 0x4000) ? 1 : 0;
int wy = band_y + sub_y + row * 16;
sw_wide_emit_tile(band_y, scr_x, wy, u0, v0, clut_x, clut_y,
e->tpage, e->color24, semi, e->semi_mode,
(e->flags & 2) ? 1 : 0);
g_mmx6_emit_tiles++;
}
}
}
}
}
/* ===== MMX6 BG tile-ring freshness fix ([widescreen.bg2d]) ================== *
* The widened renderer (FUN_800270d0) draws LEFT extra tile columns on each side of
* the 320 view, but the engine's streamer (FUN_800273e4 -> FUN_800274a0) only
* refreshes ONE column per side per frame at the leading edge. On scene entry or
* fast horizontal scroll the inner reveal columns therefore hold STALE tiles from a
* previous scroll position (different scrollY) -> real-but-misplaced tiles spilling
* up-and-left (the "staircase"). 4:3 is unaffected (it draws only the 21 columns the
* engine keeps streamed). Fix: before the renderer draws, re-stream EVERY tile column
* of the widened window for all 3 layers, so the render window and the ring-refresh
* window are the same window. This is a faithful clone of FUN_800274a0's inner
* metatile->ring fill, MINUS the scrollX wrap-bookkeeping writes (those drive the
* engine's own incremental streamer; perturbing them would desync its leading edge).
* Off-map columns fill tile 0 (the renderer skips them) = the engine's own void; at a
* horizontal map-loop seam (engine early-return) we skip the column. Gated on
* native-wide + ~320 mode, so 4:3 and the 512 hi-res title stay byte-identical. */
extern void psx_write_half(uint32_t addr, uint16_t val);
/* Faithful clone of FUN_800274a0's inner fill: stream one tile column at guest pixel
* (worldX, worldY) of `layer` into the 64x32 ring. Compares mirror the engine's sltu
* (UNSIGNED). When write!=0 the tiles are written to the ring; when write==0 they are
* compared against the live ring (validation), counting *cmp_total / *cmp_bad. Returns
* 1 if the column was streamed, 0 if skipped (map-loop seam early-return). */
static int mmx6_fill_column(int layer, int worldX, int worldY, int write,
int *cmp_total, int *cmp_bad) {
uint32_t lbase = 0x800971F8u + (uint32_t)layer * 0x54u;
int rows = 0x12;
if (worldY < 0) { worldY = 0; rows = 0x11; }
int32_t metaCol = ((worldX < 0) ? worldX + 0xff : worldX) >> 8; /* t1 */
int32_t metaRow = ((worldY < 0) ? worldY + 0xff : worldY) >> 8; /* t4 */
int wrapX = worldX - (((worldX < 0) ? worldX + 0x3ff : worldX) >> 10) * 0x400;
int colsh = ((wrapX < 0) ? wrapX + 0xf : wrapX) >> 4; /* t7 = wrapX>>4 */
int ringcol = colsh & 0x3f; /* masked (renderer reads &0x3f) */
int tcolInMeta = colsh & 0xf; /* s0 = t7 & 0xf */
int wrapY = worldY - (((worldY < 0) ? worldY + 0x1ff : worldY) >> 9) * 0x200;
int ringrow = ((wrapY < 0) ? wrapY + 0xf : wrapY) >> 4; /* a2 */
int trowInMeta = ringrow & 0xf; /* t3 */
int mapLeft = psx_read_byte(lbase + 0x4d);
int mapRight = psx_read_byte(lbase + 0x4e);
int scrollX = (int16_t)psx_read_half(lbase + 0xa);
if ((uint32_t)metaCol < (uint32_t)mapLeft) {
metaCol = metaCol + 1 + (mapRight - mapLeft);
if (scrollX <= (mapLeft - 1) * 0x100) return 0; /* engine early-return: skip */
}
if ((uint32_t)mapRight < (uint32_t)metaCol) {
metaCol = (metaCol - 1 - mapRight) + mapLeft;
if ((mapRight + 1) * 0x100 <= scrollX) return 0;
}
int mapW = psx_read_byte(0x800CD338u);
int mapH = psx_read_byte(0x800CD339u);
uint32_t mapBase = psx_read_word(0x1F800004u);
uint32_t metaBase = psx_read_word(0x1F800008u);
int layerStride = (uint16_t)psx_read_half(0x8008EC10u);
int metaIdx = 0;
if ((uint32_t)metaCol < (uint32_t)mapW && (uint32_t)metaRow < (uint32_t)mapH)
metaIdx = psx_read_byte(mapBase + (uint32_t)(layer * layerStride + mapW * metaRow + metaCol));
uint32_t ringbase = 0x800A21B8u + (uint32_t)layer * 0x1000u;
for (int i = 0; i < rows; i++) {
uint32_t cell = ringbase + (uint32_t)(ringcol * 2) + (uint32_t)((ringrow & 0x1f) * 0x80);
uint16_t tile = psx_read_half(metaBase + (uint32_t)metaIdx * 0x200u
+ (uint32_t)(trowInMeta * 0x20) + (uint32_t)(tcolInMeta * 2));
if (write) {
psx_write_half(cell, tile);
} else if (cmp_total) {
(*cmp_total)++;
if (psx_read_half(cell) != tile && cmp_bad) (*cmp_bad)++;
}
ringrow = (ringrow + 1) & 0x1f;
if (++trowInMeta == 0x10) {
trowInMeta = 0;
metaRow++;
metaIdx = 0;
if ((uint32_t)metaCol < (uint32_t)mapW && (uint32_t)metaRow < (uint32_t)mapH)
metaIdx = psx_read_byte(mapBase + (uint32_t)(layer * layerStride + mapW * metaRow + metaCol));
}
}
return 1;
}
static int g_mmx6_freshfix = 1; /* re-stream the widened window each frame (the fix) */
static long g_mmx6_refill_cols = 0; /* columns streamed in the last refill (diag) */
static uint32_t g_mmx6_refill_frame = 0xFFFFFFFFu; /* frame of the last refill (once/frame) */
void gpu_ws_mmx6_set_freshfix(int on) { g_mmx6_freshfix = on ? 1 : 0; }
int gpu_ws_mmx6_freshfix_get(void) { return g_mmx6_freshfix; }
long gpu_ws_mmx6_refill_cols(void) { return g_mmx6_refill_cols; }
/* Re-stream every tile column of the widened window for all 3 BG layers, using each
* layer's OWN scroll (the streamer driver keys the ring on the layer's own scrollX/
* scrollY, not the parent-coupled value the renderer reads with) and worldY = scrollY
* - 0x10 (mirrors the driver). ci spans the renderer's widened span [-LEFT, 20+LEFT].
* No-op at 4:3 / 512 hi-res (left==0). Idempotent over the native columns the engine
* already streamed correctly, so re-streaming them is harmless. */
void psx_ws_mmx6_bg_refill_all(void) {
if (!g_mmx6_freshfix) return;
if (!ws_native_wide_active() || ws_disp_w() > 384) return;
int left = ws_mmx6_left_cols();
if (left <= 0) return;
g_mmx6_refill_cols = 0;
for (int layer = 0; layer < 3; layer++) {
uint32_t lbase = 0x800971F8u + (uint32_t)layer * 0x54u;
int sx = (int16_t)psx_read_half(lbase + 0xa);
int sy = (int16_t)psx_read_half(lbase + 0xe);
for (int ci = -left; ci <= 20 + left; ci++) {
if (mmx6_fill_column(layer, sx + ci * 16, sy - 0x10, 1, NULL, NULL))
g_mmx6_refill_cols++;
}
}
}
/* Trigger the once-per-frame widened-window refill. Called from the count_site hook
* (psx_ws_mmx6_bg_cols), which fires at the start of each layer's render — the first
* call of a new frame refills all 3 layers right before they are drawn. */
static void mmx6_bg_refill_tick(void) {
uint32_t f = (uint32_t)s_frame_count;
if (f == g_mmx6_refill_frame) return;
g_mmx6_refill_frame = f;
psx_ws_mmx6_bg_refill_all();
}
/* Validation harness (debug mmx6_freshfix {validate:1}): for the NATIVE 21 columns —
* which the engine streams correctly — run the clone in compare-only mode against the
* live ring. Proves the clone is byte-exact before the refill (which overwrites the
* ring) is trusted. Returns total cells compared; *bad = mismatches. */
int gpu_ws_mmx6_validate(int *bad_out) {
int total = 0, bad = 0;
for (int layer = 0; layer < 3; layer++) {
uint32_t lbase = 0x800971F8u + (uint32_t)layer * 0x54u;
int sx = (int16_t)psx_read_half(lbase + 0xa);
int sy = (int16_t)psx_read_half(lbase + 0xe);
for (int ci = 0; ci <= 20; ci++)
mmx6_fill_column(layer, sx + ci * 16, sy - 0x10, 0, &total, &bad);
}
if (bad_out) *bad_out = bad;
return total;
}
/* Shared render-funnel screen-X cull widening ([widescreen.cull] auto_screen_x),
* called identically by the gcc emit, the sljit JIT, and the interpreter so every
* overlay execution path widens the same way. sx = the GTE screen-X as loaded by
* the guest's lhu (low 16 bits significant); imm = the original bound (0x140 /
* 0x141). Returns the sltiu verdict (1 = on-screen/keep). Sign-extends sx and
* shifts by +margin so BOTH 16:9 margins pass; at 4:3 margin==0 so it reduces
* bit-for-bit to the vanilla `(uint16)sx < imm`. */
int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm) {
int m = psx_ws_x_margin();
return ((uint32_t)((int32_t)(int16_t)(uint16_t)sx + m)
< (uint32_t)((int32_t)imm + 2 * m)) ? 1 : 0;
}
/* Detect the GTE per-vertex trivial-reject signature in a run of instruction
* words: at least one `sltiu …,0x140/0x141` (width) AND one `sltiu …,0xE0/0xF1`
* (height). Lets the sljit JIT + interpreter gate the cull-widening to real
* render funnels (a lone sltiu 0x140 elsewhere must stay vanilla). Mirrors the
* recompiler's func_has_screen_extent_cull. */
int psx_ws_func_has_screen_cull(const uint32_t *words, int n) {
int has_x = 0, has_y = 0;
for (int i = 0; i < n; i++) {
uint32_t w = words[i];
if ((w & 0xFC000000u) != 0x2C000000u) continue; /* sltiu */
uint32_t imm = w & 0xFFFFu;
if (imm == 0x140 || imm == 0x141) has_x = 1;
else if (imm == 0xE0 || imm == 0xF1) has_y = 1;
if (has_x && has_y) return 1;
}
return 0;
}
/* Widescreen backdrop screen-X correction ([widescreen.backdrop] x_sites).
* The parallax 2D backdrop layer (ocean/cloud/mountain/grass — overlay actor
* handlers e.g. 0x801216BC) computes its screen-X in pure integer math
* (screenX = (worldX - camX) >> parallax) and NEVER goes through the GTE, so
* the GTE X-squash (gte_set_display_aspect) that gives 3D the wider 16:9 FOV
* does not touch it. The recompiler emits this on each handler's final
* screenX store so the backdrop is squashed by the SAME factor around the
* screen centre: a far piece whose 4:3 screenX sat past the 320px edge (and
* was GPU-clipped → the blue void / half-rectangles at the edges) is pulled
* in to cover the revealed FOV. Identity at 4:3 / boot / FMV / full-2D (the
* exact ws_active() predicate the GTE squash uses), so one build serves both.
* x is the int16 screenX the handler was about to store. */
int psx_ws_backdrop_x(int x) {
if (!ws_active()) return (int16_t)x;
int32_t cx = ws_disp_w() / 2; /* screen centre (=160 @ 320) */
return ws_scale_about((int16_t)x, cx);
}
/* Backdrop PRELOAD predicate + value substitution ([widescreen.cull]
* auto_backdrop). The recompiler/sljit/interp detect each scrolling-backdrop
* column-window generator (see ws_backdrop_detect.h) and route its window START
* and END bounds through psx_ws_backdrop_value(). When native-wide is engaged we
* WIDEN the camera-tracked window by the 16:9 reveal: the START (left) bound
* moves left by `margin`, the END (right) bound moves right by `margin`, where
* margin is the per-side reveal in COLUMNS. The window of window_cols columns
* spans (at least) the full display width, so reveal_cols = window_cols *
* reveal_px / display_px = window_cols * nw_offset / disp_w (+slack). This draws
* ONLY the now-visible columns (no whole-row overdraw, no fixed cap), and the
* generator's own low/high clamps still bound it at the level edges. Gated on
* native-wide only (NOT squash: that path uses psx_ws_backdrop_x()). Returns
* `orig` unchanged at 4:3 / squash / boot / FMV, so 4:3 stays byte-identical. */
int psx_ws_backdrop_preload(void) { return ws_native_wide_active(); }
/* Live-tunable widen amount (set via the `ws_backdrop_margin` debug command):
* <0 => WHOLE-ROW preload: START->0, END->extent-1 (max generous)
* 0 => identity: no widening, native-wide still on (A/B the effect)
* >0 => widen the camera-tracked window by N columns each side (bounded)
* Default is a safe-but-generous bounded value; dialed live while debugging so
* the widen strategy can be tuned without a rebuild. */
int g_ws_bd_margin = 0; /* column preload proven irrelevant to the void; default off */
/* Set to 1 by the interpreter around its psx_ws_backdrop_value call so the value
* function does NOT also ring-note: the interp records a richer entry (live
* extent / camera / DL count). Native cache-DLL calls leave it 0, so the value
* function notes them itself -- that is how the ring sees native execution. */
int g_ws_bd_from_interp = 0;
uint32_t psx_ws_backdrop_value(uint32_t orig, int is_end, int window_cols) {
if (!psx_ws_backdrop_preload()) return orig; /* 4:3 -> byte-identical */
if (window_cols < 0) window_cols = -window_cols;
int m = g_ws_bd_margin;
int from_interp = g_ws_bd_from_interp;
g_ws_bd_from_interp = 0; /* consume one-shot flag */
uint32_t finalv;
if (m < 0) {
/* Whole-row: the generator's shared clamps (START<0 -> 0; END>=extent ->
* extent-1) pin the loop to [0, extent-1]. Bounded because `extent` is a
* byte and the DL count is a byte (row <= 255 cols). */
enum { WS_BD_END_SENTINEL = 0x7FFF };
finalv = is_end ? (uint32_t)WS_BD_END_SENTINEL : 0u;
} else if (m == 0) {
finalv = orig; /* effect off (still recorded) */
} else {
finalv = is_end ? (orig + (uint32_t)m) : (orig - (uint32_t)m);
}
/* Single chokepoint for every path (interp hook, native cache-DLL via the
* overlay callback, sljit). The interp records its own richer entry, so skip
* here when it set the flag; native-DLL calls (flag 0) are recorded here with
* pc/extent/camera = 0 -- which is how the ring sees native execution. */
if (!from_interp)
psx_ws_backdrop_ring_note(0u, is_end ? 2 /*WS_BD_END*/ : 1 /*WS_BD_START*/,
window_cols, orig, finalv, 0, 0, 0, 0u, 0u);
return finalv;
}
/* ---- auto_backdrop diagnostic ring (always-on; see BACKDROP_PRELOAD.md) -----
* Every backdrop-window rewrite (interp or native) is recorded here so the live
* tile-row extent, the camera-tracked bounds, and exactly WHICH of a scene's
* windows fire can be read back via the `ws_backdrop_ring` debug command --
* query the ring for the window of interest, never pause/step. The recorder is
* fed by the interpreter (which has the live CPU state: s7=extent, the DL count,
* and the scratchpad camera-X) at the rewrite site. */
typedef struct {
uint32_t frame, pc, orig, finalv, base, dl;
int kind, wcols, extent, camx, count;
} WsBdRingEnt;
#define WS_BD_RING_CAP 512
static WsBdRingEnt s_bd_ring[WS_BD_RING_CAP];
static uint32_t s_bd_ring_seq = 0;
void psx_ws_backdrop_ring_note(uint32_t pc, int kind, int wcols, uint32_t orig,
uint32_t finalv, int extent, int camx, int count,
uint32_t base, uint32_t dl) {
WsBdRingEnt *e = &s_bd_ring[s_bd_ring_seq & (WS_BD_RING_CAP - 1)];
e->frame = (uint32_t)s_frame_count; e->pc = pc; e->kind = kind; e->wcols = wcols;
e->orig = orig; e->finalv = finalv; e->extent = extent; e->camx = camx; e->count = count;
e->base = base; e->dl = dl;
s_bd_ring_seq++;
}
int psx_ws_backdrop_ring_json(char *buf, int cap) {
int n = (int)(s_bd_ring_seq < WS_BD_RING_CAP ? s_bd_ring_seq : WS_BD_RING_CAP);
uint32_t start = s_bd_ring_seq - (uint32_t)n;
int off = snprintf(buf, (size_t)cap, "\"seq\":%u,\"n\":%d,\"ents\":[", s_bd_ring_seq, n);
for (int i = 0; i < n && off < cap - 200; i++) {
WsBdRingEnt *e = &s_bd_ring[(start + (uint32_t)i) & (WS_BD_RING_CAP - 1)];
off += snprintf(buf + off, (size_t)(cap - off),
"%s{\"f\":%u,\"pc\":\"%08x\",\"k\":%d,\"wc\":%d,\"o\":%d,\"fin\":%d,\"ext\":%d,\"cam\":%d,\"cnt\":%d,\"base\":\"%08x\",\"dl\":\"%08x\"}",
i ? "," : "", e->frame, e->pc, e->kind, e->wcols,
(int)(int16_t)e->orig, (int)(int16_t)e->finalv, e->extent, e->camx, e->count,
e->base, e->dl);
}
off += snprintf(buf + off, (size_t)(cap - off), "]");
return off;
}
/* Backdrop store-site registry. The [widescreen.backdrop] x_sites are emitted
* into native cache-DLL code by the recompiler, but overlay code very often
* runs INTERPRETED (no DLL loaded), where the emit can't reach. So the runtime
* also registers the same site PCs here and the dirty-RAM interpreter applies
* psx_ws_backdrop_x() at those `sh` PCs — same transform, both paths. Tiny set;
* a linear scan per matching SH is negligible. */
#define WS_BACKDROP_SITES_MAX 16
static uint32_t ws_backdrop_sites[WS_BACKDROP_SITES_MAX];
static int ws_backdrop_site_n = 0;
void psx_ws_set_backdrop_sites(const uint32_t* pcs, int n) {
ws_backdrop_site_n = 0;
if (!pcs) return;
for (int i = 0; i < n && ws_backdrop_site_n < WS_BACKDROP_SITES_MAX; i++)
ws_backdrop_sites[ws_backdrop_site_n++] = pcs[i] & 0x1FFFFFFFu;
}
int psx_ws_is_backdrop_site(uint32_t pc) {
if (!ws_backdrop_site_n) return 0;
pc &= 0x1FFFFFFFu;
for (int i = 0; i < ws_backdrop_site_n; i++)
if (ws_backdrop_sites[i] == pc) return 1;
return 0;
}
/* Snapshot live widescreen state for the TCP gpu_state diagnostic. Mirrors
* the predicates above so a probe can see, mid-gameplay, whether the squash
* and the cull-margin are actually engaged (8C). */
void gpu_ws_get_debug(GpuWsDebug* out) {
if (!out) return;
out->configured = ws_configured();
out->active = ws_active();
out->game_mode = ws_game_mode();
out->present_native_43 = gpu_ws_present_native_43();
out->x_margin = psx_ws_x_margin();
out->xnum = ws_xnum;
out->xden = ws_xden;
out->mode = ws_mode;
out->nw_extra = ws_nw_extra();
out->cur_frame = s_frame_count;
out->last_tag_frame = ws_last_tag_stamp;
}
void gpu_ws_configure(int aspect_num, int aspect_den,
uint32_t sprite_anchor_addr, int hud_sprt_squash, int mode) {
ws_cfg_num = aspect_num > 0 ? aspect_num : 4;
ws_cfg_den = aspect_den > 0 ? aspect_den : 3;
ws_mode = mode;
if (mode == 1) {
/* Squash factor = (4*den)/(3*num) — the same factor the GTE applies. */
int32_t n = 4 * ws_cfg_den, d = 3 * ws_cfg_num;
int32_t a = n, b = d;
while (b) { int32_t t = a % b; a = b; b = t; }
ws_xnum = n / a;
ws_xden = d / a;
} else {
/* Native-wide (2) and off (0): the GTE is NOT squashed. */
ws_xnum = ws_xden = 1;
}
ws_anchor_addr = sprite_anchor_addr;
ws_hud_sprt = hud_sprt_squash;
}
/* Called from generated code at the entry of each [widescreen]
* sprite_tag_funcs function: record prim pointer ($a0) -> projected anchor.
* Gated on ws_configured (NOT ws_active): ws_active depends on game_mode which
* THIS function sets, so gating it on ws_active would never let gameplay
* start (the first character of a frame would be suppressed forever). */
void psx_ws_sprite_tag(CPUState* cpu) {
if (!ws_engaged() || !ws_anchor_addr) return;
uint32_t key = cpu->gpr[4] & 0x1FFFFCu;
if (!key) return;
uint32_t sxy = cpu->read_word(ws_anchor_addr);
int32_t ax = (int32_t)(int16_t)(sxy & 0xFFFFu);
uint32_t now = (uint32_t)s_frame_count;
uint32_t idx = (key >> 2) & (WS_TAG_BUCKETS - 1);
uint32_t victim = idx;
for (int i = 0; i < WS_TAG_PROBES; i++) {
uint32_t j = (idx + i) & (WS_TAG_BUCKETS - 1);
WsTag *t = &ws_tags[j];
if (t->key == key || t->key == 0) { victim = j; break; }
if (now - t->stamp > 2) victim = j; /* stale — reusable */
}
ws_tags[victim].key = key;
ws_tags[victim].stamp = now;
ws_tags[victim].anchor_x = ax;
ws_last_tag_stamp = now;
}
/* Look up the executing GP0 command's prim in the tag table. The command's
* first word lives at prim+4 (the PsyQ P_TAG header precedes it), but accept
* a direct hit too in case a tag site passes the colour-word address. */
static int ws_tagged_anchor(int32_t *out_ax) {
if (!ws_active() || gp0_cmd_source_addr == 0xFFFFFFFFu) return 0;
uint32_t now = (uint32_t)s_frame_count;
for (int variant = 0; variant < 2; variant++) {
uint32_t key = (gp0_cmd_source_addr - (variant ? 0u : 4u)) & 0x1FFFFCu;
uint32_t idx = (key >> 2) & (WS_TAG_BUCKETS - 1);
for (int i = 0; i < WS_TAG_PROBES; i++) {
WsTag *t = &ws_tags[(idx + i) & (WS_TAG_BUCKETS - 1)];
if (t->key == key && now - t->stamp <= 2) {
*out_ax = t->anchor_x;
return 1;
}
}
}
return 0;
}
/* Native-wide-compatible "is the current GP0 prim sprite-tagged?" — same ws_tags
* lookup as ws_tagged_anchor but gated on ws_engaged() (squash OR native-wide)
* instead of ws_active() (squash only). Used by the GL native-wide 2D-backdrop
* stretch to EXCLUDE foreground sprites: characters / Tomba / HUD are tagged by
* psx_ws_sprite_tag (their prim ptr -> ws_tags), the 2D backdrop tiles are not.
* Reads the live gp0_cmd_source_addr (the source addr of the prim being drawn). */
/* Flower-field backdrop data-structure address range — set by the dirty-RAM
* interpreter when the backdrop generator (overlay 0x80116808) runs; its tile
* packets live in [lo, hi]. The GL native-wide 2D-stretch gate matches the prim
* being drawn (gp0_cmd_source_addr) against this to PRECISELY identify the
* flower-field tiles — the 3D rock/foreground is untagged AND has narrow prims,
* so tag/narrow heuristics alone mis-stretch and tear it. */
uint32_t g_ws_backdrop_lo = 0, g_ws_backdrop_hi = 0;
/* diag: per-frame min/max of the prim source addrs the GL gate sees */
uint32_t g_bdg_src_lo = 0xFFFFFFFFu, g_bdg_src_hi = 0;
static uint32_t bdg_src_frame = 0xFFFFFFFFu;
/* ---- prim<->pixel correlation debug gate (ws_dbg_stretch command) ------------
* To identify WHICH drawn prims constitute the native-wide 2D-backdrop void layer
* (vs Tomba/HUD/3D), this lets a probe stretch an exact SELECTABLE prim set and
* screenshot the result. When g_dbg_mode != 0 the GL stretch gate uses this
* instead of the flower-struct range. Modes (buffer/frame-independent except 1):
* 1 OT-range : gp0_cmd_source_addr in [g_dbg_lo,g_dbg_hi]
* 2 clut : textured prim whose clut == g_dbg_clut
* 3 tpage : textured prim whose tpage == (g_dbg_clut & 0x1FF)
* 4 tagged : prim is sprite-tagged (psx_ws_prim_is_tagged)
* 5 untag-tex : textured AND not sprite-tagged
* 6 all-tex : every textured prim (sanity: should fill void + distort 3D)
* Counters (per-frame, snapshotted by the present hook) report how many prims
* matched and how many of those were sprite-tagged. */
int g_dbg_mode = 0;
uint32_t g_dbg_lo = 0, g_dbg_hi = 0;
uint16_t g_dbg_clut = 0;
int g_dbg_match_n = 0, g_dbg_match_tagged = 0; /* snapshot (reported) */
static int s_dbg_match_n = 0, s_dbg_match_tagged = 0; /* accumulate per frame */
int psx_ws_prim_is_tagged(void); /* defined below */
static int dbg_gate_match(void) {
uint32_t op = (gp0_cmd_buf[0] >> 24) & 0xFFu;
uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16);
uint32_t a = gp0_cmd_source_addr & 0x1FFFFFFFu;
int textured = ((op >= 0x20 && op <= 0x3F) && (op & 0x04)) || (op >= 0x64 && op <= 0x67);
int tagged = psx_ws_prim_is_tagged();
int m = 0;
switch (g_dbg_mode) {
case 1: m = (g_dbg_hi > g_dbg_lo && a >= g_dbg_lo && a <= g_dbg_hi); break;
case 2: m = (textured && clut == g_dbg_clut); break;
case 3: m = (textured && ((uint16_t)(gp0_cmd_buf[4] >> 16) & 0x1FF) == (g_dbg_clut & 0x1FF)); break;
case 4: m = tagged; break;
case 5: m = (textured && !tagged); break;
case 6: m = textured; break;
default: m = 0; break;
}
if (m) { s_dbg_match_n++; if (tagged) s_dbg_match_tagged++; }
return m;
}
/* Called by the present hook each frame to publish the dbg match counters. */
void psx_ws_dbg_gate_frame_snapshot(void) {
g_dbg_match_n = s_dbg_match_n; g_dbg_match_tagged = s_dbg_match_tagged;
s_dbg_match_n = 0; s_dbg_match_tagged = 0;
}
/* Background-phase latch for the native-wide 2D-backdrop stretch. The far 2D
* backdrop (a sprite-tagged tile grid: sky gradient + flower-ball field) draws
* FIRST each frame, before the GTE 3D world. Once a shaded/gouraud prim (the 3D
* world's signature -- the backdrop is sprites/flat) is drawn, the backdrop phase
* is over: later TAGGED prims (HUD, characters) must NOT be stretched. Latched
* per-frame at GP0 decode (ws_bg_phase_note, sees every prim in draw order). */
static uint32_t s_bg_phase_frame = 0xFFFFFFFFu;
static int s_bg_phase_over = 0;
void ws_bg_phase_note(uint32_t op) {
uint32_t f = (uint32_t)s_frame_count;
if (f != s_bg_phase_frame) { s_bg_phase_frame = f; s_bg_phase_over = 0; }
if (op >= 0x30u && op <= 0x3Fu) s_bg_phase_over = 1; /* shaded = 3D world */
}
static int ws_bg_phase_over(void) {
uint32_t f = (uint32_t)s_frame_count;
if (f != s_bg_phase_frame) { s_bg_phase_frame = f; s_bg_phase_over = 0; }
return s_bg_phase_over;
}
int psx_ws_prim_in_backdrop(void) {
if (gp0_cmd_source_addr != 0xFFFFFFFFu) {
uint32_t f = (uint32_t)s_frame_count;
if (f != bdg_src_frame) { bdg_src_frame = f; g_bdg_src_lo = 0xFFFFFFFFu; g_bdg_src_hi = 0; }
if (gp0_cmd_source_addr < g_bdg_src_lo) g_bdg_src_lo = gp0_cmd_source_addr;
if (gp0_cmd_source_addr > g_bdg_src_hi) g_bdg_src_hi = gp0_cmd_source_addr;
}
if (g_dbg_mode != 0) return dbg_gate_match(); /* correlation override */
/* Real gate: stretch the 2D backdrop = sprite-tagged prims drawn in the
* background phase (before the 3D world). Fills the native-wide void for both
* the flower grid and the sky band; HUD/characters (tagged but post-3D) and the
* GTE world (untagged) are excluded. */
if (ws_bg_phase_over()) return 0;
return psx_ws_prim_is_tagged();
}
int psx_ws_prim_is_tagged(void) {
if (!ws_engaged() || gp0_cmd_source_addr == 0xFFFFFFFFu) return 0;
uint32_t now = (uint32_t)s_frame_count;
for (int variant = 0; variant < 2; variant++) {
uint32_t key = (gp0_cmd_source_addr - (variant ? 0u : 4u)) & 0x1FFFFCu;
uint32_t idx = (key >> 2) & (WS_TAG_BUCKETS - 1);
for (int i = 0; i < WS_TAG_PROBES; i++) {
WsTag *t = &ws_tags[(idx + i) & (WS_TAG_BUCKETS - 1)];
if (t->key == key && now - t->stamp <= 2) return 1;
}
}
return 0;
}
/* Squash x around pivot ax by ws_xnum/ws_xden, round to nearest. */
static int32_t ws_scale_about(int32_t x, int32_t ax) {
int32_t d = x - ax;
int32_t s = (d * ws_xnum + (d >= 0 ? ws_xden / 2 : -ws_xden / 2)) / ws_xden;
return ax + s;
}
/* Squash a width; never below 1px so nothing vanishes. */
static int32_t ws_scale_len(int32_t w) {
int32_t s = (w * ws_xnum + ws_xden / 2) / ws_xden;
return s < 1 ? 1 : s;
}
/* Horizontal display width in drawing space (e.g. 320). */
static int32_t ws_disp_w(void) {
GpuDisplayInfo di;
gpu_get_display_info(&di);
return di.width ? (int32_t)di.width : 320;
}
/* In-game HUD pivot for an untagged screen-space SPRT spanning [x, x+w).
* Only reached during gameplay (ws_active true). HUD elements are sparse and
* corner-anchored, so pivot by thirds: outer-third elements anchor to their
* screen edge (keeping the wide-screen corner position at native proportions),
* the middle third to centre. A composite (counter box + digits) sits inside
* one zone, so its pieces share a pivot and stay aligned. (Full-2D menu
* screens never reach here — they get zero squash + 4:3 pillarbox instead.) */
static int32_t ws_hud_pivot(int32_t x, int32_t w) {
int32_t W = ws_disp_w();
int32_t cx = 2 * x + w; /* 2*centre, avoids losing the half */
if (3 * cx < 2 * W) return 0;
if (3 * cx > 4 * W) return W;
return W / 2;
}
/* Shared transform for fixed-size textured sprites (8x8 / 16x16 / 1x1 dot):
* squash *x0 in place (around the tagged anchor, else the HUD pivot) and
* return the squashed draw width, or 0 = no change. */
static int ws_sprt_fixed_transform(int32_t *x0, int w) {
if (!ws_active()) return 0;
int32_t ax;
if (ws_tagged_anchor(&ax)) {
*x0 = ws_scale_about(*x0, ax);
return (int)ws_scale_len(w);
}
if (ws_hud_sprt) {
*x0 = ws_scale_about(*x0, ws_hud_pivot(*x0, w));
return (int)ws_scale_len(w);
}
return 0;
}
/* Polyline state */
static uint16_t polyline_color; /* mono polyline: current color */
static int32_t polyline_prev_x, polyline_prev_y; /* previous vertex */
static uint16_t polyline_prev_c; /* shaded polyline: previous color */
static int polyline_semi_trans; /* semi-transparency flag from command word */
static int polyline_has_prev; /* have we seen at least one vertex? */
/* VRAM write transfer state (CPU→VRAM, command 0xA0) */
static uint16_t vram_write_x, vram_write_y; /* start coords */
static uint16_t vram_write_w, vram_write_h; /* dimensions */
static uint16_t vram_write_col, vram_write_row; /* current offset */
static uint32_t vram_write_remaining; /* words remaining */
/* VRAM read transfer state (VRAM→CPU, command 0xC0) */
static int vram_read_active;
static uint16_t vram_read_x, vram_read_y;
static uint16_t vram_read_w, vram_read_h;
static uint16_t vram_read_col, vram_read_row;
/* ---- GPU internal state ---- */
/* Texture page / draw mode (set by GP0(E1h), reflected in GPUSTAT bits 0-10) */
static uint32_t texpage_x; /* bits 0-3: texture page X base (N*64) */
static uint32_t texpage_y; /* bit 4: texture page Y base (0 or 256) */
static uint32_t semi_transparency; /* bits 5-6 */
static uint32_t texpage_colors; /* bits 7-8: 4bit/8bit/15bit */
static uint32_t dither_enabled; /* bit 9 */
static uint32_t draw_to_display; /* bit 10: drawing to display area allowed */
static uint32_t set_mask_bit; /* bit 11 */
static uint32_t check_mask_bit; /* bit 12 */
static uint32_t interlace_field; /* bit 13 */
static uint32_t reverse_flag; /* bit 14 */
static uint32_t texture_disable; /* bit 15 */
/* Draw area (set by GP0(E3h)/GP0(E4h)) */
static uint32_t draw_area_left, draw_area_top;
static uint32_t draw_area_right, draw_area_bottom;
/* Draw offset (set by GP0(E5h)) */
static int32_t draw_offset_x, draw_offset_y;
/* Texture window raw value (set by GP0(E2h), readback via GP1(10h)) */
static uint32_t texture_window_value;
/* Display mode (set by GP1(08h), reflected in GPUSTAT bits 16-22) */
static uint32_t hres2; /* bit 16: horizontal resolution 2 (368 mode) */
static uint32_t hres1; /* bits 17-18: horizontal resolution 1 */
static uint32_t vres; /* bit 19: vertical resolution (0=240, 1=480) */
static uint32_t video_mode; /* bit 20: 0=NTSC, 1=PAL */
static uint32_t display_depth; /* bit 21: 0=15bit, 1=24bit */
static uint32_t vertical_interlace; /* bit 22 */
/* Display enable (set by GP1(03h)) */
static uint32_t display_disabled; /* bit 23: 0=enabled, 1=disabled */
/* IRQ1 (set by GP0(1Fh), acked by GP1(02h)) */
static uint32_t irq1_flag; /* bit 24 */
/* DMA direction (set by GP1(04h)) */
static uint32_t dma_direction; /* bits 29-30 in GPUSTAT */
/* LCF (even/odd line in interlace, toggles per vblank) */
static uint32_t lcf; /* bit 31 */
/* I_STAT — defined in memory.c; declared early so gpu_read_gpustat can
* raise the VBLANK bit when a synthetic vblank fires from a tight
* BIOS-shell GPUSTAT poll loop, without going through the
* vblank-callback path (which would call SDL_Delay/Sleep). */
extern uint32_t i_stat;
/* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */
extern void psx_irq_raise(uint32_t bit, uint32_t detail);
/* Display area start (GP1(05h)) */
static uint32_t display_area_x;
static uint32_t display_area_y;
/* ----- Native-wide compositor driving (see runtime/src/gpu_sw_renderer.c) ----
* The renderer keeps a separate wide surface per framebuffer; we tell it which
* back buffer each draw targets and present from the displayed buffer's surface.
* A "framebuffer base" is a VRAM x-origin the display has scanned out from — we
* learn that set from GP1(05h) so the mirror only engages for real display
* buffers (not offscreen texture builds). Generic: no per-game constants. */
#define WS_FB_BASES 4
static uint32_t ws_fb_base[WS_FB_BASES];
static int ws_fb_n = 0;
static void ws_note_display_base(uint32_t bx) {
for (int i = 0; i < ws_fb_n; i++) if (ws_fb_base[i] == bx) return;
if (ws_fb_n < WS_FB_BASES) { ws_fb_base[ws_fb_n++] = bx; return; }
for (int i = 1; i < WS_FB_BASES; i++) ws_fb_base[i - 1] = ws_fb_base[i];
ws_fb_base[WS_FB_BASES - 1] = bx;
}
static int ws_is_fb_base(uint32_t bx) {
for (int i = 0; i < ws_fb_n; i++) if (ws_fb_base[i] == bx) return 1;
return 0;
}
/* Point the renderer's wide mirror at the current back buffer (draw_area_left)
* when native-wide is active and that buffer is a known display buffer; else
* disable mirroring for this draw. Called when the draw env changes. */
static void ws_nw_sync_target(void) {
if (!ws_native_wide_active()) { gr_wide_disable_target(); return; }
gr_wide_configure((int)ws_disp_w() + ws_nw_extra(), ws_nw_offset());
uint32_t base = draw_area_left;
if (ws_is_fb_base(base)) gr_wide_set_target((int)base);
else gr_wide_disable_target();
}
/* Horizontal display range (GP1(06h)) */
static uint32_t h_display_x1;
static uint32_t h_display_x2;
/* Vertical display range (GP1(07h)) */
static uint32_t v_display_y1;
static uint32_t v_display_y2;
/* GPUREAD latch (GP1(10h) get-info result, or VRAM read data) */
static uint32_t gpuread_latch;
/* C0 (VRAM→CPU) capture slot — forward declaration for gpu_read_gpuread */
#define C0_HISTORY_CAP_FWD 32
static struct C0HistEntry {
uint16_t x, y, w, h;
uint32_t func_addr, sp_val, s1_val;
uint32_t first_words[4];
int read_count;
} c0_history_fwd[C0_HISTORY_CAP_FWD];
static int c0_history_count_fwd = 0;
static int c0_capture_slot_fwd = -1;
/* Vblank presentation callback */
static gpu_vblank_cb vblank_callback;
/* Shaded quad vertex capture (Phase 4.5 debug) — forward declarations
* so gpu_vblank_tick can reference sq_cap_armed. */
#define SQ_CAP_MAX 32
static GpuSqCapEntry sq_cap_buf[SQ_CAP_MAX];
static int sq_cap_count;
static int sq_cap_armed;
/* GPUSTAT poll counter: in v4, recompiled code runs as native C without
* per-instruction stepping. The BIOS contains tight VSYNC wait loops that
* poll GPUSTAT bit 31 (LCF) waiting for it to toggle. LCF only changes
* via gpu_vblank_tick(), which normally fires from psx_check_interrupts()
* at dispatch boundaries. A polling loop within a single recompiled
* function would spin forever.
*
* Fix: count GPUSTAT reads and trigger a vblank when the BIOS has polled
* enough times, approximating the real hardware timing where the field
* flips every ~33,868 GPU clocks (one NTSC frame). */
#define GPUSTAT_POLL_VBLANK_THRESHOLD 1000
static uint32_t gpustat_poll_count;
/* ---- Initialization ---- */
void gpu_init(void) {
memset(vram, 0, sizeof(vram));
gr_init(vram);
/* Reset GP0 state machine */
gp0_state = GP0_IDLE;
gp0_words_collected = 0;
gp0_words_needed = 0;
vram_write_remaining = 0;
vram_read_active = 0;
/* Reset all state to power-on defaults */
texpage_x = 0;
texpage_y = 0;
semi_transparency = 0;
texpage_colors = 0;
dither_enabled = 0;
draw_to_display = 0;
set_mask_bit = 0;
check_mask_bit = 0;
interlace_field = 0;
reverse_flag = 0;
texture_disable = 0;
draw_area_left = 0;
draw_area_top = 0;
draw_area_right = 0;
draw_area_bottom = 0;
draw_offset_x = 0;
draw_offset_y = 0;
texture_window_value = 0;
hres2 = 0;
hres1 = 0;
vres = 0;
video_mode = 0;
display_depth = 0;
vertical_interlace = 0;
display_disabled = 1; /* display is OFF after reset */
irq1_flag = 0;
dma_direction = 0;
lcf = 0;
display_area_x = 0;
display_area_y = 0;
h_display_x1 = 0x200;
h_display_x2 = 0xC00;
v_display_y1 = 0x010;
v_display_y2 = 0x100;
gpuread_latch = 0;
gpustat_poll_count = 0;
}
/* ---- GPUSTAT read (0x1F801814) ---- */
uint32_t gpu_read_gpustat(void) {
/* Advance vblank when polled enough times from within a single function.
* This handles BIOS VSYNC wait loops that poll LCF in tight loops.
*
* IMPORTANT: only update emulation state here (LCF + I_STAT). Do NOT
* fire the vblank callback — that calls sdl_vblank_present, which
* runs SDL_RenderPresent and SDL_Delay (Sleep). Entering Sleep from
* an MMIO-read code path means recompiled MIPS code spends real wall
* time inside Sleep — wrong context, hard to reason about, and
* accumulates host stack frames inside the recompiled call tree.
* The callback fires from the proper VBLANK trigger in
* psx_check_interrupts (cycle-paced). */
gpustat_poll_count++;
if (gpustat_poll_count >= GPUSTAT_POLL_VBLANK_THRESHOLD) {
gpustat_poll_count = 0;
lcf ^= 1;
psx_irq_raise(0, 0); /* IRQ_VBLANK (GPUSTAT-poll fallback path) */
/* DEQUEUE: VBlank fired via the GPUSTAT-poll fallback path (distinct
* from the cycle-paced VBlank in psx_check_interrupts). */
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_VBLANK, 0xFFFFFFFFu);
}
uint32_t stat = 0;
stat |= (texpage_x & 0xF);
stat |= (texpage_y & 1) << 4;
stat |= (semi_transparency & 3) << 5;
stat |= (texpage_colors & 3) << 7;
stat |= (dither_enabled & 1) << 9;
stat |= (draw_to_display & 1) << 10;
stat |= (set_mask_bit & 1) << 11;
stat |= (check_mask_bit & 1) << 12;
stat |= (interlace_field & 1) << 13;
stat |= (reverse_flag & 1) << 14;
stat |= (texture_disable & 1) << 15;
stat |= (hres2 & 1) << 16;
stat |= (hres1 & 3) << 17;
stat |= (vres & 1) << 19;
stat |= (video_mode & 1) << 20;
stat |= (display_depth & 1) << 21;
stat |= (vertical_interlace & 1) << 22;
stat |= (display_disabled & 1) << 23;
stat |= (irq1_flag & 1) << 24;
/* Bit 25: DMA request — depends on DMA direction.
* Direction 0: always 0
* Direction 1: FIFO not full (always 1 for now — we process instantly)
* Direction 2: same as bit 28 (ready to receive DMA block)
* Direction 3: same as bit 27 (ready to send VRAM to CPU)
*/
switch (dma_direction) {
case 0: break; /* bit 25 = 0 */
case 1: stat |= (1u << 25); break;
case 2: stat |= (1u << 25); break; /* mirrors ready-to-receive */
case 3: stat |= (1u << 25); break; /* mirrors ready-to-send */
}
/* Bit 26: ready to receive cmd word — 1 when not busy */
stat |= (1u << 26);
/* Bit 27: ready to send VRAM to CPU — 1 when VRAM read is active */
if (vram_read_active)
stat |= (1u << 27);
/* Bit 28: ready to receive DMA block — 1 when not busy */
stat |= (1u << 28);
/* Bits 29-30: DMA direction */
stat |= (dma_direction & 3) << 29;
/* Bit 31: LCF — drawing even/odd lines in interlace mode */
stat |= (lcf & 1) << 31;
return stat;
}
/* ---- GPUREAD (0x1F801810 read) ---- */
uint32_t gpu_read_gpuread(void) {
if (!vram_read_active)
return gpuread_latch;
/* Read two 16-bit pixels from VRAM and pack into one 32-bit word.
* Routed through the renderer facade: under the GL backend the GPU-side
* framebuffer is authoritative and must be synced down before the CPU
* array is read (no-op cost on the software backend). */
uint32_t value = 0;
for (int i = 0; i < 2; i++) {
uint16_t rx = (vram_read_x + vram_read_col) % 1024;
uint16_t ry = (vram_read_y + vram_read_row) % 512;
value |= (uint32_t)gr_vram_read((int)rx, (int)ry) << (i * 16);
if (++vram_read_col == vram_read_w) {
vram_read_col = 0;
if (++vram_read_row == vram_read_h) {
/* Transfer complete */
vram_read_active = 0;
break;
}
}
}
/* Capture first words for C0 debug */
if (c0_capture_slot_fwd >= 0 && c0_capture_slot_fwd < C0_HISTORY_CAP_FWD) {
int rc = c0_history_fwd[c0_capture_slot_fwd].read_count++;
if (rc < 4)
c0_history_fwd[c0_capture_slot_fwd].first_words[rc] = value;
if (!vram_read_active)
c0_capture_slot_fwd = -1; /* transfer complete */
}
gpuread_latch = value;
return value;
}
/* ---- GP0 command helpers ---- */
/* Convert 24-bit RGB888 to 16-bit RGB555 (PS1 VRAM format) */
static uint16_t rgb888_to_rgb555(uint32_t color24) {
uint32_t r = (color24 >> 0) & 0xFF;
uint32_t g = (color24 >> 8) & 0xFF;
uint32_t b = (color24 >> 16) & 0xFF;
return (uint16_t)((r >> 3) | ((g >> 3) << 5) | ((b >> 3) << 10));
}
/* i_stat extern declared earlier in this file (above gpu_read_gpustat). */
void gpu_vblank_tick(void) {
lcf ^= 1;
gpustat_poll_count = 0;
psx_irq_raise(0, 0); /* IRQ_VBLANK (gpu_vblank_tick) */
if (vblank_callback) vblank_callback();
}
const uint16_t* gpu_get_vram(void) {
return vram;
}
static uint8_t gpu_vram_byte(uint32_t byte_x, uint32_t y) {
uint16_t hw = vram[((y & 511u) * 1024u) + ((byte_x >> 1) & 1023u)];
return (byte_x & 1u) ? (uint8_t)(hw >> 8) : (uint8_t)hw;
}
/* ---- Present-time screen-colour LUT (verified-enhancement, opt-in) -------
*
* PRESENT-TIME ONLY. This sits on the 15-bit-scanout -> RGB888 conversion that
* feeds the SDL/GL present path. It never touches VRAM and never runs on the
* depth24 (FMV) scanout (see gpu_display_pixel_rgb). It defaults to SCREEN_RAW
* (the original exact 5->3-replicated expansion below), so with the feature off
* the conversion — and therefore every hashed/oracle-diffed frame — is
* byte-identical to upstream. Opt in via PSX_SCREEN={crt,composite,trinitron};
* any other value (or unset) keeps the raw path.
*
* Built lazily on first use because env is read once and the GPU has no
* settings plumb yet. The raw fast-path below is preserved verbatim so the
* default never even consults the LUT. */
static ColorLut* s_screen_lut = NULL;
static int s_screen_lut_init = 0;
static int s_screen_kind_cfg = SCREEN_RAW; /* config/launcher-set; env overrides */
void gpu_set_screen_kind(int kind) {
if (kind < SCREEN_RAW || kind > SCREEN_TRINITRON) kind = SCREEN_RAW;
if (kind == s_screen_kind_cfg) return;
s_screen_kind_cfg = kind;
s_screen_lut_init = 0; /* rebuild on next scanout */
}
static void screen_lut_ensure(void) {
if (s_screen_lut_init) return;
s_screen_lut_init = 1;
if (s_screen_lut) { color_lut_destroy(s_screen_lut); s_screen_lut = NULL; }
/* Precedence: PSX_SCREEN env (debug override) wins if set+valid; otherwise
* the config/launcher value. Default (no env, raw config) = passthrough. */
ScreenKind kind = (ScreenKind)s_screen_kind_cfg;
const char* name = getenv("PSX_SCREEN");
ScreenKind envk;
if (name && screen_kind_from_name(name, &envk)) kind = envk;
if (kind == SCREEN_RAW) {
s_screen_lut = NULL; /* raw fast-path; passthrough */
return;
}
ColorSettings s;
s.screen = kind;
s.darken = -1.0; /* per-screen default */
s.target = DISPLAY_SRGB;
s_screen_lut = color_lut_create(&s);
if (s_screen_lut && color_lut_is_passthrough(s_screen_lut)) {
color_lut_destroy(s_screen_lut);
s_screen_lut = NULL;
}
}
static void gpu_rgb555_to_rgb888(uint16_t c, uint8_t* r, uint8_t* g, uint8_t* b) {
screen_lut_ensure();
if (s_screen_lut) {
color_lut_map555(s_screen_lut, c, r, g, b);
return;
}
/* Default raw path — byte-identical to upstream. */
*r = (uint8_t)((c & 0x1Fu) << 3);
*g = (uint8_t)(((c >> 5) & 0x1Fu) << 3);
*b = (uint8_t)(((c >> 10) & 0x1Fu) << 3);
}
void gpu_display_pixel_rgb(const GpuDisplayInfo* di, uint32_t x, uint32_t y,
uint8_t* r, uint8_t* g, uint8_t* b) {
if (di->depth24) {
uint32_t byte_x = ((di->display_x & 1023u) * 2u) + x * 3u;
uint32_t vy = (di->display_y + y) & 511u;
*r = gpu_vram_byte(byte_x + 0u, vy);
*g = gpu_vram_byte(byte_x + 1u, vy);
*b = gpu_vram_byte(byte_x + 2u, vy);
return;
}
uint32_t vx = (di->display_x + x) & 1023u;
uint32_t vy = (di->display_y + y) & 511u;
gpu_rgb555_to_rgb888(vram[vy * 1024u + vx], r, g, b);
}
uint32_t gpu_display_pixel_argb(const GpuDisplayInfo* di, uint32_t x, uint32_t y) {
uint8_t r, g, b;
gpu_display_pixel_rgb(di, x, y, &r, &g, &b);
return 0xFF000000u | ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
}
void gpu_get_display_info(GpuDisplayInfo* out) {
out->display_x = display_area_x;
out->display_y = display_area_y;
out->depth24 = (int)(display_depth & 1u);
out->disabled = (int)display_disabled;
/* Derive pixel width from horizontal display range and resolution mode.
* The BIOS typically sets 256x240 or 320x240 NTSC. */
uint32_t dots = (h_display_x2 > h_display_x1) ? (h_display_x2 - h_display_x1) : 0;
/* Horizontal resolution in pixels depends on hres1/hres2 bits.
* Common: hres1=0 → 256, hres1=1 → 320, hres1=2 → 512, hres1=3 → 640
* hres2=1 → 368 */
uint32_t w;
if (hres2) {
w = 368;
} else {
static const uint32_t hres_lut[4] = { 256, 320, 512, 640 };
w = hres_lut[hres1 & 3];
}
uint32_t h = (v_display_y2 > v_display_y1) ? (v_display_y2 - v_display_y1) : 240;
if (vres) h *= 2; /* 480i */
/* Clamp to sane maximums */
if (w > 640) w = 640;
if (h > 512) h = 512;
out->width = w;
out->height = h;
}
void gpu_set_vblank_callback(gpu_vblank_cb cb) {
vblank_callback = cb;
}
/* Sign-extend an N-bit value to int32_t */
static int32_t sign_extend(uint32_t val, int bits) {
uint32_t sign_bit = 1u << (bits - 1);
return (int32_t)((val ^ sign_bit) - sign_bit);
}
/* ---- Polygon rasterizer ---- */
/* Parse a vertex position word: signed 11-bit X and Y */
static void parse_vertex(uint32_t word, int32_t* x, int32_t* y) {
*x = sign_extend(word & 0x7FFu, 11);
*y = sign_extend((word >> 16) & 0x7FFu, 11);
}
/* Write a single pixel to VRAM with draw area clipping and mask bit handling */
static void raster_pixel(int32_t x, int32_t y, uint16_t color) {
if (x < (int32_t)draw_area_left || x > (int32_t)draw_area_right) return;
if (y < (int32_t)draw_area_top || y > (int32_t)draw_area_bottom) return;
uint32_t vx = (uint32_t)x & 1023u;
uint32_t vy = (uint32_t)y & 511u;
uint32_t idx = vy * 1024 + vx;
if (check_mask_bit && (vram[idx] & 0x8000u)) return;
vram[idx] = color | (set_mask_bit ? 0x8000u : 0u);
}
/* Rasterize a flat-shaded triangle using DDA scanline fill.
* Vertices are in screen coordinates (draw offset already applied). */
static void raster_triangle(int32_t x0, int32_t y0,
int32_t x1, int32_t y1,
int32_t x2, int32_t y2,
uint16_t color)
{
/* Sort vertices by Y (ascending). */
int32_t tx, ty;
if (y0 > y1) { tx=x0; ty=y0; x0=x1; y0=y1; x1=tx; y1=ty; }
if (y1 > y2) { tx=x1; ty=y1; x1=x2; y1=y2; x2=tx; y2=ty; }
if (y0 > y1) { tx=x0; ty=y0; x0=x1; y0=y1; x1=tx; y1=ty; }
/* Reject degenerate (zero-height) or oversized triangles. */
if (y0 == y2) return;
if ((x2 - x0) > 1023 || (x0 - x2) > 1023) return;
if ((y2 - y0) > 511) return;
/* 64-bit fixed-point DDA (32.32) matching PS1 hardware behavior.
* Reference: DuckStation gpu_sw_rasterizer.inl makefp_xy / makestep_xy */
#define FP_ONE (1LL << 32)
#define MAKE_FP(v) (((int64_t)(v) << 32) + (FP_ONE - (1 << 11)))
#define MAKE_STEP(dx, dy) \
((((int64_t)(dx) << 32) + ((dx) < 0 ? -((dy)-1) : (((dx) > 0) ? ((dy)-1) : 0))) / (dy))
#define UNFP(fp) ((int32_t)((uint64_t)(fp) >> 32))
/* Upper half: y0 to y1 */
if (y1 > y0) {
int32_t dy_long = y2 - y0;
int32_t dy_short = y1 - y0;
int64_t base = MAKE_FP(x0);
int64_t step_long = MAKE_STEP(x2 - x0, dy_long);
int64_t step_short = MAKE_STEP(x1 - x0, dy_short);
/* Determine which edge is left vs right */
int64_t lx, rx, ls, rs;
if (step_long < step_short) {
lx = base; ls = step_long; rx = base; rs = step_short;
} else {
lx = base; ls = step_short; rx = base; rs = step_long;
}
for (int32_t y = y0; y < y1; y++) {
int32_t xl = UNFP(lx);
int32_t xr = UNFP(rx);
for (int32_t x = xl; x < xr; x++)
raster_pixel(x, y, color);
lx += ls; rx += rs;
}
}
/* Lower half: y1 to y2 */
if (y2 > y1) {
int32_t dy_long = y2 - y0;
int32_t dy_short = y2 - y1;
/* Long edge continues from y0 to y2 */
int64_t step_long = MAKE_STEP(x2 - x0, dy_long);
int64_t long_at_y1 = MAKE_FP(x0) + step_long * (y1 - y0);
int64_t short_start = MAKE_FP(x1);
int64_t step_short = MAKE_STEP(x2 - x1, dy_short);
/* Determine left/right from the upper half's orientation */
int64_t lx, rx, ls, rs;
int64_t full_step_short_upper = (y1 > y0) ? MAKE_STEP(x1 - x0, y1 - y0) : 0;
if (step_long < full_step_short_upper ||
(y1 == y0 && long_at_y1 <= short_start)) {
lx = long_at_y1; ls = step_long; rx = short_start; rs = step_short;
} else {
lx = short_start; ls = step_short; rx = long_at_y1; rs = step_long;
}
for (int32_t y = y1; y < y2; y++) {
int32_t xl = UNFP(lx);
int32_t xr = UNFP(rx);
for (int32_t x = xl; x < xr; x++)
raster_pixel(x, y, color);
lx += ls; rx += rs;
}
}
#undef FP_ONE
#undef MAKE_FP
#undef MAKE_STEP
#undef UNFP
}
/* Execute mono triangle (GP0 0x20-0x23) */
static void gp0_exec_mono_tri(void) {
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu);
int32_t vx[3], vy[3];
for (int i = 0; i < 3; i++) {
parse_vertex(gp0_cmd_buf[1 + i], &vx[i], &vy[i]);
vx[i] += draw_offset_x;
vy[i] += draw_offset_y;
}
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color);
}
/* Execute mono quad (GP0 0x28-0x2B) — two triangles: (0,1,2) and (2,1,3) */
static void gp0_exec_mono_quad(void) {
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu);
int32_t vx[4], vy[4];
for (int i = 0; i < 4; i++) {
parse_vertex(gp0_cmd_buf[1 + i], &vx[i], &vy[i]);
vx[i] += draw_offset_x;
vy[i] += draw_offset_y;
}
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color);
gr_draw_flat_triangle(vx[2], vy[2], vx[1], vy[1], vx[3], vy[3], color);
}
/* Execute shaded triangle (GP0 0x30-0x33) — Gouraud shaded */
static void gp0_exec_shaded_tri(void) {
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
int32_t vx[3], vy[3];
uint16_t c[3];
/* Layout: C0, V0, C1, V1, C2, V2 */
for (int i = 0; i < 3; i++) {
c[i] = rgb888_to_rgb555(gp0_cmd_buf[i * 2] & 0xFFFFFFu);
parse_vertex(gp0_cmd_buf[1 + i * 2], &vx[i], &vy[i]);
vx[i] += draw_offset_x;
vy[i] += draw_offset_y;
}
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
gr_draw_gouraud_triangle(vx[0], vy[0], c[0],
vx[1], vy[1], c[1],
vx[2], vy[2], c[2]);
}
void gpu_arm_shaded_quad_capture(void) { sq_cap_armed = 1; sq_cap_count = 0; }
int gpu_get_shaded_quad_capture(const GpuSqCapEntry** out) {
sq_cap_armed = 0; /* disarm on read */
*out = sq_cap_buf;
return sq_cap_count;
}
/* Execute shaded quad (GP0 0x38-0x3B) */
static void gp0_exec_shaded_quad(void) {
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
int32_t vx[4], vy[4];
uint16_t c[4];
/* Layout: C0, V0, C1, V1, C2, V2, C3, V3 */
for (int i = 0; i < 4; i++) {
c[i] = rgb888_to_rgb555(gp0_cmd_buf[i * 2] & 0xFFFFFFu);
parse_vertex(gp0_cmd_buf[1 + i * 2], &vx[i], &vy[i]);
vx[i] += draw_offset_x;
vy[i] += draw_offset_y;
}
/* Capture vertex data when armed. */
if (sq_cap_armed && sq_cap_count < SQ_CAP_MAX) {
GpuSqCapEntry* e = &sq_cap_buf[sq_cap_count++];
for (int i = 0; i < 4; i++) {
e->vx[i] = vx[i]; e->vy[i] = vy[i];
e->color[i] = gp0_cmd_buf[i * 2] & 0xFFFFFFu;
}
}
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
gr_draw_gouraud_triangle(vx[0], vy[0], c[0],
vx[1], vy[1], c[1],
vx[2], vy[2], c[2]);
gr_draw_gouraud_triangle(vx[2], vy[2], c[2],
vx[1], vy[1], c[1],
vx[3], vy[3], c[3]);
}
/* Helper: build texpage word from GPU state for SW renderer.
* Format: bits 0-3 = X base, bit 4 = Y base, bits 5-6 = semi-trans, bits 7-8 = color depth */
static uint16_t current_texpage(void) {
return (uint16_t)(texpage_x | (texpage_y << 4) |
(semi_transparency << 5) | (texpage_colors << 7));
}
/* Helper: set up SW renderer state before a textured draw.
* semi_trans: whether the primitive has semi-transparency enabled (from opcode bit)
* raw_texture: 1 if this is a raw-texture opcode (bit 0 of GP0 opcode set) */
static void setup_textured_draw(uint32_t color24, int semi_trans, int raw_texture) {
uint32_t r = (color24 >> 0) & 0xFF;
uint32_t g = (color24 >> 8) & 0xFF;
uint32_t b = (color24 >> 16) & 0xFF;
gr_set_color_modulation((int)r, (int)g, (int)b, raw_texture);
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
}
/* Execute textured triangle (GP0 0x24-0x27) */
static void gp0_exec_textured_tri(void) {
uint32_t color24 = gp0_cmd_buf[0] & 0xFFFFFFu;
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
int raw_texture = (gp0_cmd_buf[0] >> 24) & 1;
int32_t vx[3], vy[3];
int u[3], v[3];
/* Layout: color+v0, texcoord0+clut, v1, texcoord1+tpage, v2, texcoord2 */
parse_vertex(gp0_cmd_buf[1], &vx[0], &vy[0]);
parse_vertex(gp0_cmd_buf[3], &vx[1], &vy[1]);
parse_vertex(gp0_cmd_buf[5], &vx[2], &vy[2]);
u[0] = gp0_cmd_buf[2] & 0xFF; v[0] = (gp0_cmd_buf[2] >> 8) & 0xFF;
u[1] = gp0_cmd_buf[4] & 0xFF; v[1] = (gp0_cmd_buf[4] >> 8) & 0xFF;
u[2] = gp0_cmd_buf[6] & 0xFF; v[2] = (gp0_cmd_buf[6] >> 8) & 0xFF;
uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16);
uint16_t clut_x = (clut & 0x3F) * 16;
uint16_t clut_y = (clut >> 6) & 0x1FF;
/* Texpage from word 4 bits 16-31, or use current state */
uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[4] >> 16);
uint16_t tpage = tpage_word & 0x1FF;
for (int i = 0; i < 3; i++) {
vx[i] += draw_offset_x;
vy[i] += draw_offset_y;
}
setup_textured_draw(color24, semi_trans, raw_texture);
gr_draw_textured_triangle(vx[0], vy[0], u[0], v[0],
vx[1], vy[1], u[1], v[1],
vx[2], vy[2], u[2], v[2],
clut_x, clut_y, tpage);
}
/* Execute textured quad (GP0 0x2C-0x2F) */
static void gp0_exec_textured_quad(void) {
uint32_t color24 = gp0_cmd_buf[0] & 0xFFFFFFu;
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
int raw_texture = (gp0_cmd_buf[0] >> 24) & 1;
int32_t vx[4], vy[4];
int u[4], v[4];
/* Layout: color+v0, tc0+clut, v1, tc1+tpage, v2, tc2, v3, tc3 */
parse_vertex(gp0_cmd_buf[1], &vx[0], &vy[0]);
parse_vertex(gp0_cmd_buf[3], &vx[1], &vy[1]);
parse_vertex(gp0_cmd_buf[5], &vx[2], &vy[2]);
parse_vertex(gp0_cmd_buf[7], &vx[3], &vy[3]);
u[0] = gp0_cmd_buf[2] & 0xFF; v[0] = (gp0_cmd_buf[2] >> 8) & 0xFF;
u[1] = gp0_cmd_buf[4] & 0xFF; v[1] = (gp0_cmd_buf[4] >> 8) & 0xFF;
u[2] = gp0_cmd_buf[6] & 0xFF; v[2] = (gp0_cmd_buf[6] >> 8) & 0xFF;
u[3] = gp0_cmd_buf[8] & 0xFF; v[3] = (gp0_cmd_buf[8] >> 8) & 0xFF;
uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16);
uint16_t clut_x = (clut & 0x3F) * 16;
uint16_t clut_y = (clut >> 6) & 0x1FF;
uint16_t tpage = (uint16_t)(gp0_cmd_buf[4] >> 16) & 0x1FF;
/* Widescreen: tagged billboard quads carry CPU-computed pixel offsets the
* GTE squash never saw — re-squash every X around the prim's anchor. */
{
int32_t ws_ax;
if (ws_tagged_anchor(&ws_ax))
for (int i = 0; i < 4; i++) vx[i] = ws_scale_about(vx[i], ws_ax);
}
for (int i = 0; i < 4; i++) {
vx[i] += draw_offset_x;
vy[i] += draw_offset_y;
}
setup_textured_draw(color24, semi_trans, raw_texture);
if (vy[0] == vy[1] && vy[2] == vy[3] &&
vx[0] == vx[2] && vx[1] == vx[3] &&
u[0] == u[2] && u[1] == u[3] &&
v[0] == v[1] && v[2] == v[3]) {
int x = vx[0] < vx[1] ? vx[0] : vx[1];
int y = vy[0] < vy[2] ? vy[0] : vy[2];
int w = vx[0] < vx[1] ? vx[1] - vx[0] : vx[0] - vx[1];
int h = vy[0] < vy[2] ? vy[2] - vy[0] : vy[0] - vy[2];
int left_u = vx[0] < vx[1] ? u[0] : u[1];
int right_u = vx[0] < vx[1] ? u[1] : u[0];
int top_v = vy[0] < vy[2] ? v[0] : v[2];
int bot_v = vy[0] < vy[2] ? v[2] : v[0];
if (w > 0 && h > 0) {
if (right_u - left_u == w && bot_v - top_v == h) {
gr_draw_textured_rect(x, y, w, h, left_u, top_v,
clut_x, clut_y, tpage);
return;
}
gr_draw_textured_rect_scaled(x, y, w, h, left_u, top_v,
right_u, bot_v,
clut_x, clut_y, tpage);
return;
}
}
gr_draw_textured_triangle(vx[0], vy[0], u[0], v[0],
vx[1], vy[1], u[1], v[1],
vx[2], vy[2], u[2], v[2],
clut_x, clut_y, tpage);
gr_draw_textured_triangle(vx[2], vy[2], u[2], v[2],
vx[1], vy[1], u[1], v[1],
vx[3], vy[3], u[3], v[3],
clut_x, clut_y, tpage);
}
/* Execute shaded textured triangle (GP0 0x34-0x37) */
static void gp0_exec_shaded_textured_tri(void) {
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
int raw_texture = (gp0_cmd_buf[0] >> 24) & 1;
int32_t vx[3], vy[3];
int u[3], v[3];
uint32_t c[3];
/* Layout: C0, V0, TC0+clut, C1, V1, TC1+tpage, C2, V2, TC2 */
c[0] = gp0_cmd_buf[0] & 0xFFFFFFu;
c[1] = gp0_cmd_buf[3] & 0xFFFFFFu;
c[2] = gp0_cmd_buf[6] & 0xFFFFFFu;
parse_vertex(gp0_cmd_buf[1], &vx[0], &vy[0]);
parse_vertex(gp0_cmd_buf[4], &vx[1], &vy[1]);
parse_vertex(gp0_cmd_buf[7], &vx[2], &vy[2]);
u[0] = gp0_cmd_buf[2] & 0xFF; v[0] = (gp0_cmd_buf[2] >> 8) & 0xFF;
u[1] = gp0_cmd_buf[5] & 0xFF; v[1] = (gp0_cmd_buf[5] >> 8) & 0xFF;
u[2] = gp0_cmd_buf[8] & 0xFF; v[2] = (gp0_cmd_buf[8] >> 8) & 0xFF;
uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16);
uint16_t clut_x = (clut & 0x3F) * 16;
uint16_t clut_y = (clut >> 6) & 0x1FF;
uint16_t tpage = (uint16_t)(gp0_cmd_buf[5] >> 16) & 0x1FF;
for (int i = 0; i < 3; i++) {
vx[i] += draw_offset_x;
vy[i] += draw_offset_y;
}
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0],
vx[1], vy[1], u[1], v[1], c[1],
vx[2], vy[2], u[2], v[2], c[2],
clut_x, clut_y, tpage, raw_texture);
}
/* Execute shaded textured quad (GP0 0x3C-0x3F) */
static void gp0_exec_shaded_textured_quad(void) {
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
int raw_texture = (gp0_cmd_buf[0] >> 24) & 1;
int32_t vx[4], vy[4];
int u[4], v[4];
uint32_t c[4];
/* Layout: C0, V0, TC0+clut, C1, V1, TC1+tpage, C2, V2, TC2, C3, V3, TC3 */
c[0] = gp0_cmd_buf[0] & 0xFFFFFFu;
c[1] = gp0_cmd_buf[3] & 0xFFFFFFu;
c[2] = gp0_cmd_buf[6] & 0xFFFFFFu;
c[3] = gp0_cmd_buf[9] & 0xFFFFFFu;
parse_vertex(gp0_cmd_buf[1], &vx[0], &vy[0]);
parse_vertex(gp0_cmd_buf[4], &vx[1], &vy[1]);
parse_vertex(gp0_cmd_buf[7], &vx[2], &vy[2]);
parse_vertex(gp0_cmd_buf[10], &vx[3], &vy[3]);
u[0] = gp0_cmd_buf[2] & 0xFF; v[0] = (gp0_cmd_buf[2] >> 8) & 0xFF;
u[1] = gp0_cmd_buf[5] & 0xFF; v[1] = (gp0_cmd_buf[5] >> 8) & 0xFF;
u[2] = gp0_cmd_buf[8] & 0xFF; v[2] = (gp0_cmd_buf[8] >> 8) & 0xFF;
u[3] = gp0_cmd_buf[11] & 0xFF; v[3] = (gp0_cmd_buf[11] >> 8) & 0xFF;
uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16);
uint16_t clut_x = (clut & 0x3F) * 16;
uint16_t clut_y = (clut >> 6) & 0x1FF;
uint16_t tpage = (uint16_t)(gp0_cmd_buf[5] >> 16) & 0x1FF;
for (int i = 0; i < 4; i++) {
vx[i] += draw_offset_x;
vy[i] += draw_offset_y;
}
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0],
vx[1], vy[1], u[1], v[1], c[1],
vx[2], vy[2], u[2], v[2], c[2],
clut_x, clut_y, tpage, raw_texture);
gr_draw_shaded_textured_triangle(vx[2], vy[2], u[2], v[2], c[2],
vx[1], vy[1], u[1], v[1], c[1],
vx[3], vy[3], u[3], v[3], c[3],
clut_x, clut_y, tpage, raw_texture);
}
/* Execute mono line (GP0 0x40-0x47) — Bresenham */
static void gp0_exec_mono_line(void) {
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu);
int32_t x0, y0, x1, y1;
parse_vertex(gp0_cmd_buf[1], &x0, &y0);
parse_vertex(gp0_cmd_buf[2], &x1, &y1);
x0 += draw_offset_x; y0 += draw_offset_y;
x1 += draw_offset_x; y1 += draw_offset_y;
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
gr_draw_line(x0, y0, x1, y1, color);
}
/* Execute shaded line (GP0 0x50-0x57) */
static void gp0_exec_shaded_line(void) {
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
uint16_t c0 = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu);
uint16_t c1 = rgb888_to_rgb555(gp0_cmd_buf[2] & 0xFFFFFFu);
int32_t x0, y0, x1, y1;
parse_vertex(gp0_cmd_buf[1], &x0, &y0);
parse_vertex(gp0_cmd_buf[3], &x1, &y1);
x0 += draw_offset_x; y0 += draw_offset_y;
x1 += draw_offset_x; y1 += draw_offset_y;
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
gr_draw_shaded_line(x0, y0, c0, x1, y1, c1);
}
/* Execute mono rectangle (GP0 0x60-0x63) */
static void gp0_exec_mono_rect(void) {
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu);
int32_t x0, y0;
parse_vertex(gp0_cmd_buf[1], &x0, &y0);
int w = gp0_cmd_buf[2] & 0xFFFFu;
int h = (gp0_cmd_buf[2] >> 16) & 0xFFFFu;
if (w > 1023) w = 1023;
if (h > 511) h = 511;
x0 += draw_offset_x; y0 += draw_offset_y;
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
gr_draw_flat_rect(x0, y0, w, h, color);
}
/* Execute textured rectangle (GP0 0x64-0x67) */
static void gp0_exec_textured_rect(void) {
uint32_t color24 = gp0_cmd_buf[0] & 0xFFFFFFu;
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
int raw_texture = (gp0_cmd_buf[0] >> 24) & 1;
int32_t x0, y0;
parse_vertex(gp0_cmd_buf[1], &x0, &y0);
int u0 = gp0_cmd_buf[2] & 0xFF;
int v0 = (gp0_cmd_buf[2] >> 8) & 0xFF;
uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16);
uint16_t clut_x = (clut & 0x3F) * 16;
uint16_t clut_y = (clut >> 6) & 0x1FF;
int w = gp0_cmd_buf[3] & 0x3FF;
int h = (gp0_cmd_buf[3] >> 16) & 0x1FF;
if (w > 1023) w = 1023;
if (h > 511) h = 511;
/* Widescreen: tagged sprite parts squash around their projected anchor;
* untagged SPRTs are screen-space 2D (HUD/menus) and squash around the
* display centre. Texels keep full coverage via the scaled-rect path. */
int ws_w = 0;
if (ws_active() && w > 0) {
int32_t ws_ax;
if (ws_tagged_anchor(&ws_ax)) {
x0 = ws_scale_about(x0, ws_ax);
ws_w = (int)ws_scale_len(w);
} else if (ws_hud_sprt) {
x0 = ws_scale_about(x0, ws_hud_pivot(x0, w));
ws_w = (int)ws_scale_len(w);
}
}
x0 += draw_offset_x; y0 += draw_offset_y;
setup_textured_draw(color24, semi_trans, raw_texture);
if (ws_w && ws_w != w)
gr_draw_textured_rect_scaled(x0, y0, ws_w, h, u0, v0, u0 + w, v0 + h,
clut_x, clut_y, current_texpage());
else
gr_draw_textured_rect(x0, y0, w, h, u0, v0, clut_x, clut_y, current_texpage());
}
/* Execute 1x1 dot (GP0 0x68-0x6B) */
static void gp0_exec_mono_dot(void) {
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu);
int32_t x, y;
parse_vertex(gp0_cmd_buf[1], &x, &y);
x += draw_offset_x; y += draw_offset_y;
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
gr_draw_flat_rect(x, y, 1, 1, color);
}
/* Execute 8x8 textured sprite (GP0 0x74-0x77) */
static void gp0_exec_textured_8x8(void) {
uint32_t color24 = gp0_cmd_buf[0] & 0xFFFFFFu;
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
int raw_texture = (gp0_cmd_buf[0] >> 24) & 1;
int32_t x0, y0;
parse_vertex(gp0_cmd_buf[1], &x0, &y0);
int ws_w = ws_sprt_fixed_transform(&x0, 8);
x0 += draw_offset_x; y0 += draw_offset_y;
int u0 = gp0_cmd_buf[2] & 0xFF;
int v0 = (gp0_cmd_buf[2] >> 8) & 0xFF;
uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16);
uint16_t clut_x = (clut & 0x3F) * 16;
uint16_t clut_y = (clut >> 6) & 0x1FF;
setup_textured_draw(color24, semi_trans, raw_texture);
if (ws_w && ws_w != 8)
gr_draw_textured_rect_scaled(x0, y0, ws_w, 8, u0, v0, u0 + 8, v0 + 8,
clut_x, clut_y, current_texpage());
else
gr_draw_textured_rect(x0, y0, 8, 8, u0, v0, clut_x, clut_y, current_texpage());
}
/* Execute 8x8 sprite (GP0 0x70-0x73) */
static void gp0_exec_mono_8x8(void) {
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu);
int32_t x0, y0;
parse_vertex(gp0_cmd_buf[1], &x0, &y0);
x0 += draw_offset_x; y0 += draw_offset_y;
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
gr_draw_flat_rect(x0, y0, 8, 8, color);
}
/* Execute 16x16 textured sprite (GP0 0x7C-0x7F) */
static void gp0_exec_textured_16x16(void) {
uint32_t color24 = gp0_cmd_buf[0] & 0xFFFFFFu;
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
int raw_texture = (gp0_cmd_buf[0] >> 24) & 1;
int32_t x0, y0;
parse_vertex(gp0_cmd_buf[1], &x0, &y0);
int ws_w = ws_sprt_fixed_transform(&x0, 16);
x0 += draw_offset_x; y0 += draw_offset_y;
int u0 = gp0_cmd_buf[2] & 0xFF;
int v0 = (gp0_cmd_buf[2] >> 8) & 0xFF;
uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16);
uint16_t clut_x = (clut & 0x3F) * 16;
uint16_t clut_y = (clut >> 6) & 0x1FF;
/* MMX6 host-side reveal: learn clut -> tpage/state from native BG tiles. */
if (gpu_ws_mmx6_reveal_is_active())
gpu_ws_mmx6_bg_record(gp0_cmd_source_addr, clut, current_texpage(),
color24, (int)semi_transparency, raw_texture);
setup_textured_draw(color24, semi_trans, raw_texture);
if (ws_w && ws_w != 16)
gr_draw_textured_rect_scaled(x0, y0, ws_w, 16, u0, v0, u0 + 16, v0 + 16,
clut_x, clut_y, current_texpage());
else
gr_draw_textured_rect(x0, y0, 16, 16, u0, v0, clut_x, clut_y, current_texpage());
}
/* ---- GP0 command execution ---- */
static void gp0_exec_nop(void) {
/* 0x00 / 0x01 — nothing to do */
}
static void gp0_exec_fill_rect(void) {
/* 0x02 — Fill Rectangle in VRAM
* Word 0: 0x02BBGGRR (color)
* Word 1: YstartXstart (X bits 0-9 aligned to 16, Y bits 16-24)
* Word 2: YsizXsiz (W bits 0-9 rounded up to 16, H bits 16-24) */
uint32_t color24 = gp0_cmd_buf[0] & 0x00FFFFFF;
uint16_t color16 = rgb888_to_rgb555(color24);
uint32_t dst_x = gp0_cmd_buf[1] & 0x3F0u; /* X masked to 16-pixel alignment */
uint32_t dst_y = (gp0_cmd_buf[1] >> 16) & 0x1FFu;
uint32_t width = ((gp0_cmd_buf[2] & 0x3FFu) + 0xFu) & ~0xFu; /* round up to 16 */
uint32_t height = (gp0_cmd_buf[2] >> 16) & 0x1FFu;
/* Fill ignores draw area, mask bits, and draw offset — writes directly to
* VRAM. Routed through the renderer so it also fills the hi-res
* supersampling mirror (no-op cost when supersampling is off). */
gr_fill_rect((int)dst_x, (int)dst_y, (int)width, (int)height, color16);
/* Native-wide: when the game clears a display buffer, clear the full width
* of that buffer's wide surface over the same rows — refreshing the centred
* content region and keeping the revealed margins clean. */
if (ws_native_wide_active() && ws_is_fb_base(dst_x)) {
gr_wide_clear((int)dst_x, (int)dst_y, (int)height, color16);
/* MMX6 host-side reveal: draw the ±LEFT margin BG columns into the freshly
* cleared band (backmost, so objects mirror over them). Uses dst_y as the band
* + live scroll; no dependence on this frame's not-yet-applied E3/E4/E5. */
gpu_ws_mmx6_emit_reveal((int)dst_x, (int)dst_y);
}
}
static void gp0_exec_draw_mode(void) {
/* 0xE1 — Draw Mode / Texpage
* Bits 0-3: texpage X base
* Bit 4: texpage Y base
* Bits 5-6: semi-transparency mode
* Bits 7-8: texture color mode
* Bit 9: dither
* Bit 10: draw to display area
* Bit 11: texture disable (when allowed by GP1) */
uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu;
texpage_x = param & 0xF;
texpage_y = (param >> 4) & 1;
semi_transparency = (param >> 5) & 3;
texpage_colors = (param >> 7) & 3;
dither_enabled = (param >> 9) & 1;
draw_to_display = (param >> 10) & 1;
texture_disable = (param >> 11) & 1;
/* Sync semi-transparency to SW renderer — actual blending is per-primitive */
}
static void gp0_exec_texture_window(void) {
/* 0xE2 — Texture Window
* Bits 0-4: mask X (in 8-pixel steps)
* Bits 5-9: mask Y
* Bits 10-14: offset X
* Bits 15-19: offset Y */
texture_window_value = gp0_cmd_buf[0] & 0x000FFFFFu;
gr_set_texture_window(texture_window_value);
}
static void gp0_exec_draw_area_tl(void) {
/* 0xE3 — Set Drawing Area Top-Left
* Bits 0-9: X
* Bits 10-19: Y */
uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu;
draw_area_left = param & 0x3FF;
draw_area_top = (param >> 10) & 0x3FF;
gr_set_draw_area((int)draw_area_left, (int)draw_area_top,
(int)draw_area_right, (int)draw_area_bottom);
ws_nw_sync_target(); /* back buffer (draw_area_left) → wide mirror surface */
}
static void gp0_exec_draw_area_br(void) {
/* 0xE4 — Set Drawing Area Bottom-Right
* Bits 0-9: X
* Bits 10-19: Y */
uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu;
draw_area_right = param & 0x3FF;
draw_area_bottom = (param >> 10) & 0x3FF;
gr_set_draw_area((int)draw_area_left, (int)draw_area_top,
(int)draw_area_right, (int)draw_area_bottom);
ws_nw_sync_target(); /* back buffer (draw_area_left) → wide mirror surface */
}
static void gp0_exec_draw_offset(void) {
/* 0xE5 — Set Drawing Offset
* Bits 0-10: X (signed 11-bit)
* Bits 11-21: Y (signed 11-bit) */
uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu;
draw_offset_x = sign_extend(param & 0x7FFu, 11);
draw_offset_y = sign_extend((param >> 11) & 0x7FFu, 11);
/* Native-wide mirrors framebuffer draws into a separate wide surface (canonical
* VRAM stays faithful); the renderer needs the live draw offset to translate
* into surface-local coords, so keep gr_set_draw_offset current. */
gr_set_draw_offset(draw_offset_x, draw_offset_y);
}
static void gp0_exec_mask_bit(void) {
/* 0xE6 — Mask Bit Setting
* Bit 0: set mask bit when drawing (force bit 15 of pixels)
* Bit 1: check mask bit (don't draw to pixels with bit 15 set) */
uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu;
set_mask_bit = param & 1;
check_mask_bit = (param >> 1) & 1;
gr_set_mask_bits((int)set_mask_bit, (int)check_mask_bit);
}
/* A0 upload history for debug inspection */
#define A0_HISTORY_CAP 128
typedef struct {
uint16_t x, y, w, h;
uint32_t first_words[4];
int word_count;
uint32_t func_addr;
uint32_t sp_val; /* CPU $sp at time of A0 header */
uint32_t ra_val; /* CPU $ra register at time of A0 header */
uint32_t s1_val; /* CPU $s1 = LoadImage RECT ptr (x/y/w/h) in FUN_80069dfc */
uint32_t s2_val; /* CPU $s2 = TRUE source pixel ptr (`move s2,a1` at entry) */
uint32_t a0_val; /* CPU $a0 at capture (RECT arg; may be clobbered) */
uint32_t a1_val; /* CPU $a1 at capture (source arg; may be clobbered) */
uint32_t frame_stamp; /* s_frame_count at the upload — for load-vs-upload ordering */
uint32_t stack[10]; /* first 10 words from sp (sp+32=saved $s1, sp+36=saved $ra) */
} A0HistEntry;
static A0HistEntry a0_history[A0_HISTORY_CAP];
static int a0_history_count = 0;
static int a0_capture_slot = -1; /* currently capturing data words for this slot */
extern uint32_t psx_read_word(uint32_t addr);
int gpu_get_a0_history(int index, int *x, int *y, int *w, int *h,
uint32_t *fw0, uint32_t *fw1, int *wcount) {
if (index < 0 || index >= a0_history_count) return 0;
*x = a0_history[index].x; *y = a0_history[index].y;
*w = a0_history[index].w; *h = a0_history[index].h;
*fw0 = a0_history[index].first_words[0];
*fw1 = a0_history[index].first_words[1];
*wcount = a0_history[index].word_count;
return 1;
}
int gpu_get_a0_count(void) { return a0_history_count; }
int gpu_get_a0_extra(int index, uint32_t *func, uint32_t *sp, uint32_t *ra,
uint32_t *s1, uint32_t *stack10) {
if (index < 0 || index >= a0_history_count) return 0;
*func = a0_history[index].func_addr;
*sp = a0_history[index].sp_val;
*ra = a0_history[index].ra_val;
*s1 = a0_history[index].s1_val;
memcpy(stack10, a0_history[index].stack, 10 * sizeof(uint32_t));
return 1;
}
/* True source pointer + arg regs + frame for the LoadImage upload (s2 = source). */
int gpu_get_a0_src(int index, uint32_t *s2, uint32_t *a0, uint32_t *a1, uint32_t *frame) {
if (index < 0 || index >= a0_history_count) return 0;
*s2 = a0_history[index].s2_val;
*a0 = a0_history[index].a0_val;
*a1 = a0_history[index].a1_val;
*frame = a0_history[index].frame_stamp;
return 1;
}
static void gp0_exec_cpu_to_vram(void) {
/* 0xA0 — CPU→VRAM Copy (header: 3 words)
* Word 0: command
* Word 1: destination coords (X bits 0-9, Y bits 16-24)
* Word 2: dimensions (W bits 0-9, H bits 16-24)
* Followed by pixel data words */
vram_write_x = gp0_cmd_buf[1] & 0x3FFu;
vram_write_y = (gp0_cmd_buf[1] >> 16) & 0x1FFu;
uint32_t w = gp0_cmd_buf[2] & 0x3FFu;
uint32_t h = (gp0_cmd_buf[2] >> 16) & 0x1FFu;
/* 0 means max dimension */
vram_write_w = (w == 0) ? 0x400 : (uint16_t)w;
vram_write_h = (h == 0) ? 0x200 : (uint16_t)h;
/* Record for debug */
if (a0_history_count < A0_HISTORY_CAP) {
int slot = a0_history_count++;
a0_history[slot].x = vram_write_x;
a0_history[slot].y = vram_write_y;
a0_history[slot].w = vram_write_w;
a0_history[slot].h = vram_write_h;
a0_history[slot].word_count = 0;
a0_history[slot].func_addr = g_debug_current_func_addr;
memset(a0_history[slot].first_words, 0, sizeof(a0_history[slot].first_words));
/* Capture CPU context for caller tracing.
* func_1FC38524 (shell LoadImage) uses $s1 as source data pointer. */
a0_history[slot].sp_val = 0;
a0_history[slot].ra_val = 0;
a0_history[slot].s1_val = 0;
a0_history[slot].s2_val = 0;
a0_history[slot].a0_val = 0;
a0_history[slot].a1_val = 0;
a0_history[slot].frame_stamp = (uint32_t)s_frame_count;
memset(a0_history[slot].stack, 0, sizeof(a0_history[slot].stack));
if (debug_cpu_ptr) {
uint32_t sp = debug_cpu_ptr->gpr[29];
a0_history[slot].sp_val = sp;
a0_history[slot].ra_val = debug_cpu_ptr->gpr[31];
a0_history[slot].s1_val = debug_cpu_ptr->gpr[17]; /* $s1 */
a0_history[slot].s2_val = debug_cpu_ptr->gpr[18]; /* $s2 = source ptr */
a0_history[slot].a0_val = debug_cpu_ptr->gpr[4]; /* $a0 */
a0_history[slot].a1_val = debug_cpu_ptr->gpr[5]; /* $a1 */
uint32_t sp_phys = sp & 0x1FFFFFu;
for (int si = 0; si < 10 && sp_phys + (si + 1) * 4 <= 0x200000u; si++)
a0_history[slot].stack[si] = psx_read_word(sp + si * 4);
}
a0_capture_slot = slot;
}
vram_write_col = 0;
vram_write_row = 0;
uint32_t num_pixels = (uint32_t)vram_write_w * (uint32_t)vram_write_h;
vram_write_remaining = (num_pixels + 1) / 2;
if (vram_write_remaining > 0)
gp0_state = GP0_VRAM_WRITE;
}
/* C0 (VRAM→CPU) history — uses c0_history_fwd declared at top of file */
#define C0_HISTORY_CAP C0_HISTORY_CAP_FWD
#define c0_history c0_history_fwd
#define c0_history_count c0_history_count_fwd
#define c0_capture_slot c0_capture_slot_fwd
int gpu_get_c0_count(void) { return c0_history_count; }
int gpu_get_c0_history(int index, int *x, int *y, int *w, int *h,
uint32_t *func, uint32_t *sp, uint32_t *s1,
uint32_t *fw0, uint32_t *fw1, int *rcount) {
if (index < 0 || index >= c0_history_count) return 0;
*x = c0_history[index].x; *y = c0_history[index].y;
*w = c0_history[index].w; *h = c0_history[index].h;
*func = c0_history[index].func_addr;
*sp = c0_history[index].sp_val;
*s1 = c0_history[index].s1_val;
*fw0 = c0_history[index].first_words[0];
*fw1 = c0_history[index].first_words[1];
*rcount = c0_history[index].read_count;
return 1;
}
static void gp0_exec_vram_to_cpu(void) {
/* 0xC0 — VRAM→CPU Copy (3 words)
* Word 1: source coords
* Word 2: dimensions
* After this, data is read via GPUREAD */
vram_read_x = gp0_cmd_buf[1] & 0x3FFu;
vram_read_y = (gp0_cmd_buf[1] >> 16) & 0x1FFu;
uint32_t w = gp0_cmd_buf[2] & 0x3FFu;
uint32_t h = (gp0_cmd_buf[2] >> 16) & 0x1FFu;
vram_read_w = (w == 0) ? 0x400 : (uint16_t)w;
vram_read_h = (h == 0) ? 0x200 : (uint16_t)h;
/* Record for debug */
if (c0_history_count < C0_HISTORY_CAP) {
int slot = c0_history_count++;
c0_history[slot].x = vram_read_x;
c0_history[slot].y = vram_read_y;
c0_history[slot].w = vram_read_w;
c0_history[slot].h = vram_read_h;
c0_history[slot].func_addr = g_debug_current_func_addr;
c0_history[slot].sp_val = debug_cpu_ptr ? debug_cpu_ptr->gpr[29] : 0;
c0_history[slot].s1_val = debug_cpu_ptr ? debug_cpu_ptr->gpr[17] : 0;
memset(c0_history[slot].first_words, 0, sizeof(c0_history[slot].first_words));
c0_history[slot].read_count = 0;
c0_capture_slot = slot;
}
vram_read_col = 0;
vram_read_row = 0;
vram_read_active = 1;
}
/* Determine how many words a GP0 command requires (header only, not counting
* variable-length data for 0xA0). Returns -1 for polylines (terminated by
* sentinel). Returns 0 for unknown commands (will be fatal). */
static int gp0_command_word_count(uint8_t opcode) {
switch (opcode) {
/* NOP / control */
case 0x00: return 1;
case 0x01: return 1; /* clear cache */
case 0x02: return 3; /* fill rect */
case 0x1F: return 1; /* IRQ request */
/* Drawing commands — polygons */
case 0x20: case 0x21: case 0x22: case 0x23: return 4; /* mono tri */
case 0x24: case 0x25: case 0x26: case 0x27: return 7; /* textured tri */
case 0x28: case 0x29: case 0x2A: case 0x2B: return 5; /* mono quad */
case 0x2C: case 0x2D: case 0x2E: case 0x2F: return 9; /* textured quad */
case 0x30: case 0x31: case 0x32: case 0x33: return 6; /* shaded tri */
case 0x34: case 0x35: case 0x36: case 0x37: return 9; /* shaded textured tri */
case 0x38: case 0x39: case 0x3A: case 0x3B: return 8; /* shaded quad */
case 0x3C: case 0x3D: case 0x3E: case 0x3F: return 12; /* shaded textured quad */
/* Lines */
case 0x40: case 0x41: case 0x42: case 0x43:
case 0x44: case 0x45: case 0x46: case 0x47: return 3; /* mono line */
case 0x48: case 0x49: case 0x4A: case 0x4B:
case 0x4C: case 0x4D: case 0x4E: case 0x4F: return -1; /* mono polyline */
case 0x50: case 0x51: case 0x52: case 0x53:
case 0x54: case 0x55: case 0x56: case 0x57: return 4; /* shaded line */
case 0x58: case 0x59: case 0x5A: case 0x5B:
case 0x5C: case 0x5D: case 0x5E: case 0x5F: return -1; /* shaded polyline */
/* Rectangles */
case 0x60: case 0x61: case 0x62: case 0x63: return 3; /* variable rect */
case 0x64: case 0x65: case 0x66: case 0x67: return 4; /* variable textured rect */
case 0x68: case 0x69: case 0x6A: case 0x6B: return 2; /* 1x1 dot */
case 0x6C: case 0x6D: case 0x6E: case 0x6F: return 3; /* 1x1 textured */
case 0x70: case 0x71: case 0x72: case 0x73: return 2; /* 8x8 rect */
case 0x74: case 0x75: case 0x76: case 0x77: return 3; /* 8x8 textured */
case 0x78: case 0x79: case 0x7A: case 0x7B: return 2; /* 16x16 rect */
case 0x7C: case 0x7D: case 0x7E: case 0x7F: return 3; /* 16x16 textured */
/* VRAM copy / transfer */
case 0x80: case 0x81: case 0x82: case 0x83:
case 0x84: case 0x85: case 0x86: case 0x87:
case 0x88: case 0x89: case 0x8A: case 0x8B:
case 0x8C: case 0x8D: case 0x8E: case 0x8F:
case 0x90: case 0x91: case 0x92: case 0x93:
case 0x94: case 0x95: case 0x96: case 0x97:
case 0x98: case 0x99: case 0x9A: case 0x9B:
case 0x9C: case 0x9D: case 0x9E: case 0x9F: return 4; /* VRAM→VRAM */
case 0xA0: case 0xA1: case 0xA2: case 0xA3:
case 0xA4: case 0xA5: case 0xA6: case 0xA7:
case 0xA8: case 0xA9: case 0xAA: case 0xAB:
case 0xAC: case 0xAD: case 0xAE: case 0xAF:
case 0xB0: case 0xB1: case 0xB2: case 0xB3:
case 0xB4: case 0xB5: case 0xB6: case 0xB7:
case 0xB8: case 0xB9: case 0xBA: case 0xBB:
case 0xBC: case 0xBD: case 0xBE: case 0xBF: return 3; /* CPU→VRAM (header) */
case 0xC0: case 0xC1: case 0xC2: case 0xC3:
case 0xC4: case 0xC5: case 0xC6: case 0xC7:
case 0xC8: case 0xC9: case 0xCA: case 0xCB:
case 0xCC: case 0xCD: case 0xCE: case 0xCF:
case 0xD0: case 0xD1: case 0xD2: case 0xD3:
case 0xD4: case 0xD5: case 0xD6: case 0xD7:
case 0xD8: case 0xD9: case 0xDA: case 0xDB:
case 0xDC: case 0xDD: case 0xDE: case 0xDF: return 3; /* VRAM→CPU (header) */
/* Environment */
case 0xE0: return 1; /* NOP */
case 0xE1: return 1; /* draw mode */
case 0xE2: return 1; /* texture window */
case 0xE3: return 1; /* draw area TL */
case 0xE4: return 1; /* draw area BR */
case 0xE5: return 1; /* draw offset */
case 0xE6: return 1; /* mask bits */
default:
/* 0x03-0x1E, 0xE7-0xEF, 0xFF: NOP (1 word) per DuckStation */
if ((opcode >= 0x03 && opcode <= 0x1E) ||
(opcode >= 0xE7 && opcode <= 0xEF) ||
opcode == 0xFF) {
return 1;
}
return 0; /* unknown */
}
}
/* Per-opcode execution counters (exposed via gpu_get_opcode_stats) */
static uint32_t gp0_opcode_count[256];
uint32_t gpu_get_opcode_count(uint8_t op) { return gp0_opcode_count[op]; }
/* ---- Per-frame GP0 command ring (always-on, queried via debug server) ---- */
/* We record every GP0 command (header + up to 6 payload words) with the
* frame number it was issued in. Per CLAUDE.md ring-buffer rule: capture
* is continuous and observers query a window of interest later, not arm-
* then-record. ~34 MB at 1M entries. Polyline / long commands get the
* first 6 payload words; that's enough for the header + first vertex pair
* + first uv/color word for diagnosing per-primitive state. */
extern uint64_t s_frame_count; /* defined in debug_server.c */
extern uint32_t g_debug_last_store_pc; /* defined in debug_server.c */
#define GP0_RING_CAP (1u << 20) /* 1 048 576 entries */
/* GpuGp0RingEntry + GPU_GP0_RING_MAX_WORDS are public types in gpu.h. */
static GpuGp0RingEntry *gp0_ring = NULL; /* lazy-alloc on first record */
static uint64_t gp0_ring_seq = 0; /* total commands recorded */
static uint32_t gp0_ring_head = 0; /* next write slot */
static void gp0_ring_record(const uint32_t *words, int n) {
if (!gp0_ring) {
gp0_ring = (GpuGp0RingEntry *)calloc(GP0_RING_CAP, sizeof(*gp0_ring));
if (!gp0_ring) return; /* OOM — capture disabled, no other effect */
}
GpuGp0RingEntry *e = &gp0_ring[gp0_ring_head];
e->frame = (uint32_t)s_frame_count;
e->seq = (uint32_t)gp0_ring_seq;
e->src_addr = gp0_cmd_source_addr;
e->pc = g_debug_last_store_pc;
e->opcode = (uint8_t)((words[0] >> 24) & 0xFF);
e->n_words = (uint8_t)(n > 255 ? 255 : (n < 0 ? 1 : n));
e->pad = 0;
int copy_n = e->n_words > GPU_GP0_RING_MAX_WORDS ? GPU_GP0_RING_MAX_WORDS : e->n_words;
for (int i = 0; i < copy_n; i++) e->cmd[i] = words[i];
for (int i = copy_n; i < GPU_GP0_RING_MAX_WORDS; i++) e->cmd[i] = 0;
gp0_ring_head = (gp0_ring_head + 1) % GP0_RING_CAP;
gp0_ring_seq++;
}
/* Public accessors for debug_server.c */
uint64_t gpu_gp0_ring_total(void) { return gp0_ring_seq; }
uint32_t gpu_gp0_ring_capacity(void) { return GP0_RING_CAP; }
uint32_t gpu_gp0_ring_max_words(void){ return GPU_GP0_RING_MAX_WORDS; }
void gpu_set_gp0_source(uint32_t addr) {
gp0_next_source_addr = addr;
}
/* Fill `out[0..max_out-1]` with entries from the requested frame; returns
* count. Walks from oldest in-buffer to newest so iteration order matches
* draw order within a frame. */
int gpu_gp0_ring_dump_frame(uint32_t frame, GpuGp0RingEntry *out, int max_out) {
if (!gp0_ring || max_out <= 0) return 0;
uint32_t avail = (gp0_ring_seq < GP0_RING_CAP)
? (uint32_t)gp0_ring_seq : GP0_RING_CAP;
uint32_t start = (gp0_ring_seq < GP0_RING_CAP) ? 0 : gp0_ring_head;
int n_out = 0;
for (uint32_t i = 0; i < avail && n_out < max_out; i++) {
uint32_t idx = (start + i) % GP0_RING_CAP;
if (gp0_ring[idx].frame == frame) {
out[n_out++] = gp0_ring[idx];
}
}
return n_out;
}
/* Report the frame range currently held in the ring (oldest..newest). */
void gpu_gp0_ring_frame_span(uint32_t *out_oldest, uint32_t *out_newest) {
if (out_oldest) *out_oldest = 0;
if (out_newest) *out_newest = 0;
if (!gp0_ring || gp0_ring_seq == 0) return;
uint32_t avail = (gp0_ring_seq < GP0_RING_CAP)
? (uint32_t)gp0_ring_seq : GP0_RING_CAP;
uint32_t start = (gp0_ring_seq < GP0_RING_CAP) ? 0 : gp0_ring_head;
uint32_t newest_idx = (start + avail - 1) % GP0_RING_CAP;
if (out_oldest) *out_oldest = gp0_ring[start].frame;
if (out_newest) *out_newest = gp0_ring[newest_idx].frame;
}
/* ---- Draw census ring (ALWAYS-ON) -----------------------------------------
* Every drawn primitive (GP0 opcode 0x20-0x7F) records: frame, the prim's DMA
* source address, the live camera (scratchpad camX/camY), and the prim's first
* vertex in DRAWING space (pre draw_offset). Purpose: see object spawn/despawn
* and edge-cull in DATA, not screenshots. When a background object despawns,
* its prim simply stops appearing in the census at a specific camX while its
* last recorded screen-x shows how far on-screen it still was — i.e. the
* effective (4:3-sized) despawn margin, which the 16:9 view exceeds. A prim
* present in the census but absent on screen would instead indict the renderer
* gate. Query via TCP `ws_census` → CSV file (large dumps mustn't ride TCP). */
#define WS_CENSUS_CAP (1u << 21) /* 2,097,152 entries * 24B = 48 MiB */
typedef struct {
uint32_t frame;
uint32_t src_addr;
int16_t cam_x, cam_y;
int16_t x, y; /* first vertex (screen / pre-draw_offset) */
int16_t xmin, xmax; /* screen-X extent across the prim's position verts */
int16_t base_x; /* back-buffer origin (draw_area_left) at draw time */
uint8_t opcode;
uint8_t pad;
} WsCensusEntry;
static WsCensusEntry *ws_census = NULL;
static uint64_t ws_census_seq = 0;
static int ws_census_on = 1; /* always-on; toggle via ws_census */
extern uint16_t psx_read_half(uint32_t addr);
/* Screen-X extent (raw vertex coords = SX, pre-draw_offset) across a prim's
* position vertices. Polygons (0x20-0x3F) decoded exactly via the packet stride
* (pos word = 1 + i*(1 + gouraud + textured)); rects/sprites/lines fall back to
* the first vertex. Lets the census show how far right each path reaches and
* whether anything is drawn past the 4:3 edge (native-wide margin diagnosis). */
static void prim_sx_extent(uint8_t op, int32_t *xmin, int32_t *xmax) {
int32_t lo = 0x7FFF, hi = -0x8000;
if (op >= 0x20 && op <= 0x3F) {
int nv = (op & 0x08) ? 4 : 3;
int stride = 1 + ((op & 0x10) ? 1 : 0) + ((op & 0x04) ? 1 : 0);
for (int i = 0; i < nv; i++) {
int32_t vx, vy;
parse_vertex(gp0_cmd_buf[1 + i * stride], &vx, &vy);
if (vx < lo) lo = vx;
if (vx > hi) hi = vx;
}
} else {
int32_t vx, vy;
parse_vertex(gp0_cmd_buf[1], &vx, &vy);
lo = hi = vx;
}
*xmin = lo; *xmax = hi;
}
static void ws_census_record(uint8_t opcode, int32_t x, int32_t y) {
if (!ws_census_on) return;
if (!ws_census) {
ws_census = (WsCensusEntry *)calloc(WS_CENSUS_CAP, sizeof(WsCensusEntry));
if (!ws_census) { ws_census_on = 0; return; }
}
int32_t xmn, xmx;
prim_sx_extent(opcode, &xmn, &xmx);
WsCensusEntry *e = &ws_census[ws_census_seq & (WS_CENSUS_CAP - 1)];
e->frame = (uint32_t)s_frame_count;
e->src_addr = gp0_cmd_source_addr;
e->cam_x = (int16_t)psx_read_half(0x1F800176);
e->cam_y = (int16_t)psx_read_half(0x1F800186);
e->x = (int16_t)x;
e->y = (int16_t)y;
e->xmin = (int16_t)xmn;
e->xmax = (int16_t)xmx;
e->base_x = (int16_t)draw_area_left;
e->opcode = opcode;
ws_census_seq++;
}
/* Dump census rows for frames [f0,f1] to a CSV file. Returns row count. */
int gpu_ws_census_dump(uint32_t f0, uint32_t f1, const char *path) {
if (!ws_census) return 0;
FILE *fp = fopen(path, "w");
if (!fp) return -1;
fprintf(fp, "frame,src_addr,cam_x,cam_y,x,y,xmin,xmax,base_x,opcode\n");
uint64_t total = ws_census_seq;
uint64_t avail = total < WS_CENSUS_CAP ? total : WS_CENSUS_CAP;
uint64_t start = total - avail;
int n = 0;
for (uint64_t s = start; s < total; s++) {
WsCensusEntry *e = &ws_census[s & (WS_CENSUS_CAP - 1)];
if (e->frame < f0 || e->frame > f1) continue;
fprintf(fp, "%u,0x%08X,%d,%d,%d,%d,%d,%d,%d,0x%02X\n",
e->frame, e->src_addr, e->cam_x, e->cam_y, e->x, e->y,
e->xmin, e->xmax, e->base_x, e->opcode);
n++;
}
fclose(fp);
return n;
}
void gpu_ws_census_set(int on) { ws_census_on = on ? 1 : 0; }
uint64_t gpu_ws_census_seq(void) { return ws_census_seq; }
/* Execute a fully-collected GP0 command */
static void gp0_execute_command(void) {
uint8_t opcode = (gp0_cmd_buf[0] >> 24) & 0xFF;
gp0_opcode_count[opcode]++;
gp0_ring_record(gp0_cmd_buf, gp0_words_needed);
extern void ws_bg_phase_note(uint32_t op);
ws_bg_phase_note(opcode); /* native-wide 2D-backdrop stretch: background-phase latch */
/* Draw-census: capture every drawing primitive's first vertex + camera. */
if (opcode >= 0x20 && opcode <= 0x7F) {
int32_t cvx, cvy;
parse_vertex(gp0_cmd_buf[1], &cvx, &cvy);
ws_census_record(opcode, cvx, cvy);
}
/* Categorize for diagnostics */
if (opcode <= 0x01) gp0_nop_count++;
else if (opcode == 0x02) gp0_fill_count++;
else if (opcode >= 0x20 && opcode <= 0x7F) gp0_draw_count++;
else if (opcode >= 0x80 && opcode <= 0xDF) gp0_copy_count++;
else if (opcode >= 0xE1 && opcode <= 0xE6) gp0_env_count++;
switch (opcode) {
case 0x00:
case 0x01:
gp0_exec_nop();
break;
case 0x02:
gp0_exec_fill_rect();
break;
case 0xE1:
gp0_exec_draw_mode();
break;
case 0xE2:
gp0_exec_texture_window();
break;
case 0xE3:
gp0_exec_draw_area_tl();
break;
case 0xE4:
gp0_exec_draw_area_br();
break;
case 0xE5:
gp0_exec_draw_offset();
break;
case 0xE6:
gp0_exec_mask_bit();
break;
/* Drawing commands — polygons */
case 0x20: case 0x21: case 0x22: case 0x23:
gp0_exec_mono_tri();
break;
case 0x24: case 0x25: case 0x26: case 0x27:
gp0_exec_textured_tri();
break;
case 0x28: case 0x29: case 0x2A: case 0x2B:
gp0_exec_mono_quad();
break;
case 0x2C: case 0x2D: case 0x2E: case 0x2F:
gp0_exec_textured_quad();
break;
case 0x30: case 0x31: case 0x32: case 0x33:
gp0_exec_shaded_tri();
break;
case 0x34: case 0x35: case 0x36: case 0x37:
gp0_exec_shaded_textured_tri();
break;
case 0x38: case 0x39: case 0x3A: case 0x3B:
gp0_exec_shaded_quad();
break;
case 0x3C: case 0x3D: case 0x3E: case 0x3F:
gp0_exec_shaded_textured_quad();
break;
/* Lines */
case 0x40: case 0x41: case 0x42: case 0x43:
case 0x44: case 0x45: case 0x46: case 0x47:
gp0_exec_mono_line();
break;
case 0x50: case 0x51: case 0x52: case 0x53:
case 0x54: case 0x55: case 0x56: case 0x57:
gp0_exec_shaded_line();
break;
/* Rectangles */
case 0x60: case 0x61: case 0x62: case 0x63:
gp0_exec_mono_rect();
break;
case 0x64: case 0x65: case 0x66: case 0x67:
gp0_exec_textured_rect();
break;
case 0x68: case 0x69: case 0x6A: case 0x6B:
gp0_exec_mono_dot();
break;
case 0x6C: case 0x6D: case 0x6E: case 0x6F: {
/* 1x1 textured dot: cmd, vertex, texcoord+clut (no size word) */
uint32_t color24 = gp0_cmd_buf[0] & 0xFFFFFFu;
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
int raw_texture = (gp0_cmd_buf[0] >> 24) & 1;
int32_t x0, y0;
parse_vertex(gp0_cmd_buf[1], &x0, &y0);
(void)ws_sprt_fixed_transform(&x0, 1); /* position only; 1px stays 1px */
x0 += draw_offset_x; y0 += draw_offset_y;
int u0 = gp0_cmd_buf[2] & 0xFF;
int v0 = (gp0_cmd_buf[2] >> 8) & 0xFF;
uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16);
uint16_t clut_x = (clut & 0x3F) * 16;
uint16_t clut_y = (clut >> 6) & 0x1FF;
setup_textured_draw(color24, semi_trans, raw_texture);
gr_draw_textured_rect(x0, y0, 1, 1, u0, v0, clut_x, clut_y, current_texpage());
break;
}
case 0x70: case 0x71: case 0x72: case 0x73:
gp0_exec_mono_8x8();
break;
case 0x74: case 0x75: case 0x76: case 0x77:
gp0_exec_textured_8x8();
break;
case 0x78: case 0x79: case 0x7A: case 0x7B: {
/* 16x16 mono sprite */
int semi_trans = (gp0_cmd_buf[0] >> 25) & 1;
uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu);
int32_t x0, y0;
parse_vertex(gp0_cmd_buf[1], &x0, &y0);
x0 += draw_offset_x; y0 += draw_offset_y;
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
gr_draw_flat_rect(x0, y0, 16, 16, color);
break;
}
case 0x7C: case 0x7D: case 0x7E: case 0x7F:
gp0_exec_textured_16x16();
break;
/* VRAM→VRAM copy */
case 0x80: case 0x81: case 0x82: case 0x83:
case 0x84: case 0x85: case 0x86: case 0x87:
case 0x88: case 0x89: case 0x8A: case 0x8B:
case 0x8C: case 0x8D: case 0x8E: case 0x8F:
case 0x90: case 0x91: case 0x92: case 0x93:
case 0x94: case 0x95: case 0x96: case 0x97:
case 0x98: case 0x99: case 0x9A: case 0x9B:
case 0x9C: case 0x9D: case 0x9E: case 0x9F: {
/* Word 1: source coords, Word 2: dest coords, Word 3: dimensions */
int src_x = gp0_cmd_buf[1] & 0x3FF;
int src_y = (gp0_cmd_buf[1] >> 16) & 0x1FF;
int dst_x = gp0_cmd_buf[2] & 0x3FF;
int dst_y = (gp0_cmd_buf[2] >> 16) & 0x1FF;
int w = gp0_cmd_buf[3] & 0x3FF;
int h = (gp0_cmd_buf[3] >> 16) & 0x1FF;
if (w == 0) w = 0x400;
if (h == 0) h = 0x200;
gr_copy_rect(src_x, src_y, dst_x, dst_y, w, h);
break;
}
case 0xA0: case 0xA1: case 0xA2: case 0xA3:
case 0xA4: case 0xA5: case 0xA6: case 0xA7:
case 0xA8: case 0xA9: case 0xAA: case 0xAB:
case 0xAC: case 0xAD: case 0xAE: case 0xAF:
case 0xB0: case 0xB1: case 0xB2: case 0xB3:
case 0xB4: case 0xB5: case 0xB6: case 0xB7:
case 0xB8: case 0xB9: case 0xBA: case 0xBB:
case 0xBC: case 0xBD: case 0xBE: case 0xBF:
gp0_exec_cpu_to_vram();
break;
case 0xC0: case 0xC1: case 0xC2: case 0xC3:
case 0xC4: case 0xC5: case 0xC6: case 0xC7:
case 0xC8: case 0xC9: case 0xCA: case 0xCB:
case 0xCC: case 0xCD: case 0xCE: case 0xCF:
case 0xD0: case 0xD1: case 0xD2: case 0xD3:
case 0xD4: case 0xD5: case 0xD6: case 0xD7:
case 0xD8: case 0xD9: case 0xDA: case 0xDB:
case 0xDC: case 0xDD: case 0xDE: case 0xDF:
gp0_exec_vram_to_cpu();
break;
default:
/* NOP range: 0x03-0x1E, 0xE0, 0xE7-0xEF, 0xFF */
if ((opcode >= 0x03 && opcode <= 0x1E) ||
opcode == 0xE0 ||
(opcode >= 0xE7 && opcode <= 0xEF) ||
opcode == 0xFF) {
break; /* NOP — silently consume */
}
/* Any other command (drawing, VRAM-to-VRAM, etc.) is not yet
* implemented. Fatal halt so we know exactly what's needed next,
* with all rings queryable post-mortem. */
{
static char reason[96];
snprintf(reason, sizeof(reason),
"GPU GP0 unimplemented command 0x%02X (word 0x%08X)",
opcode, gp0_cmd_buf[0]);
psx_fatal_halt(reason);
}
}
}
/* ---- GP0 write (0x1F801810 write) — command state machine ---- */
uint64_t gpu_get_gp0_count(void) { return gp0_write_count; }
void gpu_get_gp0_stats(uint64_t* nop, uint64_t* fill, uint64_t* draw, uint64_t* env, uint64_t* copy) {
*nop = gp0_nop_count; *fill = gp0_fill_count;
*draw = gp0_draw_count; *env = gp0_env_count; *copy = gp0_copy_count;
}
void gpu_get_draw_area(GpuDrawArea* out) {
out->left = draw_area_left;
out->top = draw_area_top;
out->right = draw_area_right;
out->bottom = draw_area_bottom;
out->offset_x = draw_offset_x;
out->offset_y = draw_offset_y;
}
uint16_t gpu_vram_peek(int x, int y) {
if (x < 0 || x >= 1024 || y < 0 || y >= 512) return 0;
/* Through the facade so the GL backend syncs its FBO down first. */
return gr_vram_read(x, y);
}
void gpu_write_gp0(uint32_t val) {
gp0_write_count++;
/* State: consuming pixel data for CPU→VRAM transfer */
if (gp0_state == GP0_VRAM_WRITE) {
/* Capture first few data words for debug */
if (a0_capture_slot >= 0 && a0_capture_slot < A0_HISTORY_CAP) {
int wc = a0_history[a0_capture_slot].word_count++;
if (wc < 4)
a0_history[a0_capture_slot].first_words[wc] = val;
}
/* Each word contains two 16-bit pixels (low halfword first) */
for (int i = 0; i < 2; i++) {
uint16_t pixel = (uint16_t)(val >> (i * 16));
uint16_t wx = (vram_write_x + vram_write_col) % 1024;
uint16_t wy = (vram_write_y + vram_write_row) % 512;
/* Respect mask bit settings. The check reads through the facade:
* under the GL backend GPU-drawn mask bits live in the FBO, not
* the CPU array (one sync per burst; free when check is off). */
if (check_mask_bit && (gr_vram_read((int)wx, (int)wy) & 0x8000))
goto next_pixel;
if (set_mask_bit)
pixel |= 0x8000;
/* Route through the renderer so the hi-res supersampling mirror
* is kept coherent (writes native VRAM identically when off). */
gr_vram_write((int)wx, (int)wy, pixel);
next_pixel:
if (++vram_write_col == vram_write_w) {
vram_write_col = 0;
if (++vram_write_row == vram_write_h) {
/* Transfer complete */
gp0_state = GP0_IDLE;
vram_write_remaining = 0;
return;
}
}
}
if (--vram_write_remaining == 0)
gp0_state = GP0_IDLE;
return;
}
/* State: mono polyline — each word is a vertex (or terminator) */
if (gp0_state == GP0_POLYLINE_MONO) {
if ((val & 0xF000F000u) == 0x50005000u) {
/* Terminator: hardware ends a polyline ONLY when the masked word
* matches 0x50005000 (the 0x55555555 terminator) — Beetle
* gpu.cpp:1030, psx-spx. The old `(val & 0xF000F000) != 0` test
* also fired on any NEGATIVE vertex coordinate (Y=0xFFxx) and, at
* shaded color positions, on any color component >= 0x10 in the
* G byte — ending the polyline early and re-parsing its remaining
* words as new GP0 commands. That de-phased the whole command
* stream: garbage prims all over the Tomba2 attract (texture
* garble) and eventually a legit texcoord word 0xFE65FE58 parsed
* in IDLE state -> "GP0 unknown command 0xFE" fatal (village). */
gp0_state = GP0_IDLE;
return;
}
int32_t x, y;
parse_vertex(val, &x, &y);
x += draw_offset_x; y += draw_offset_y;
if (polyline_has_prev) {
gr_draw_line(polyline_prev_x, polyline_prev_y, x, y, polyline_color);
}
polyline_prev_x = x; polyline_prev_y = y;
polyline_has_prev = 1;
return;
}
/* State: shaded polyline — alternating color, vertex words */
if (gp0_state == GP0_POLYLINE_SHADED) {
/* Even words (after cmd) are colors, odd words are vertices.
* Sequence: [cmd+C0] [V0] [C1] [V1] [C2] [V2] ...
* polyline_has_prev tracks: 0=need V0, 1=need C_next, 2=need V_next */
if (!polyline_has_prev) {
/* First vertex */
int32_t x, y;
parse_vertex(val, &x, &y);
x += draw_offset_x; y += draw_offset_y;
polyline_prev_x = x; polyline_prev_y = y;
polyline_prev_c = polyline_color;
polyline_has_prev = 1;
return;
}
if (polyline_has_prev == 1) {
/* Expecting color word (or terminator). Same hardware rule as the
* mono case: ONLY the masked 0x50005000 pattern terminates. */
if ((val & 0xF000F000u) == 0x50005000u) {
gp0_state = GP0_IDLE;
return;
}
polyline_color = rgb888_to_rgb555(val & 0xFFFFFFu);
polyline_has_prev = 2;
return;
}
/* polyline_has_prev == 2: vertex word */
{
int32_t x, y;
parse_vertex(val, &x, &y);
x += draw_offset_x; y += draw_offset_y;
gr_draw_shaded_line(polyline_prev_x, polyline_prev_y, polyline_prev_c,
x, y, polyline_color);
polyline_prev_x = x; polyline_prev_y = y;
polyline_prev_c = polyline_color;
polyline_has_prev = 1;
}
return;
}
/* State: collecting words for a multi-word command */
if (gp0_state == GP0_COLLECTING) {
gp0_cmd_buf[gp0_words_collected++] = val;
if (gp0_words_collected >= gp0_words_needed) {
gp0_state = GP0_IDLE;
gp0_execute_command();
}
return;
}
/* State: IDLE — this is the first word of a new command */
uint8_t opcode = (val >> 24) & 0xFF;
int word_count = gp0_command_word_count(opcode);
if (word_count == 0) {
/* Unknown command — fatal halt with rings queryable post-mortem */
static char reason[96];
snprintf(reason, sizeof(reason),
"GPU GP0 unknown command 0x%02X (word 0x%08X)", opcode, val);
psx_fatal_halt(reason);
}
if (word_count < 0) {
/* Variable-length polyline command.
* Mono (0x48-0x4F): [cmd+color] [v0] [v1] ... [terminator]
* Shaded(0x58-0x5F): [cmd+C0] [v0] [C1] [v1] ... [terminator]
* Terminator: (word & 0xF000F000) == 0x50005000 (0x55555555). */
int shaded = (opcode & 0x10) != 0; /* 0x58+ = shaded, 0x48+ = mono */
polyline_semi_trans = (val >> 25) & 1;
polyline_color = rgb888_to_rgb555(val & 0xFFFFFFu);
polyline_prev_c = polyline_color;
polyline_has_prev = 0;
gr_set_semi_transparency(polyline_semi_trans, (int)semi_transparency);
gp0_state = shaded ? GP0_POLYLINE_SHADED : GP0_POLYLINE_MONO;
gp0_draw_count++;
/* Record polyline header (variable-length body not captured;
* just enough so per-frame stream shows the polyline existed). */
gp0_opcode_count[opcode]++;
uint32_t hdr_only[1] = { val };
gp0_ring_record(hdr_only, 1);
return;
}
gp0_cmd_buf[0] = val;
gp0_cmd_source_addr = gp0_next_source_addr;
if (word_count == 1) {
gp0_words_collected = 1;
gp0_words_needed = 1;
gp0_execute_command();
} else {
gp0_state = GP0_COLLECTING;
gp0_words_collected = 1;
gp0_words_needed = word_count;
}
}
/* ---- GP1 write (0x1F801814 write) ---- */
static void gp1_reset(void) {
/* GP1(00h): Reset GPU — clears FIFO, resets all state, display off */
gpu_init();
}
static void gp1_reset_command_buffer(void) {
/* GP1(01h): Reset command buffer — clears FIFO, aborts current command */
gp0_state = GP0_IDLE;
gp0_words_collected = 0;
gp0_words_needed = 0;
vram_write_remaining = 0;
}
static void gp1_ack_irq1(void) {
/* GP1(02h): Acknowledge IRQ1 */
irq1_flag = 0;
}
static void gp1_display_enable(uint32_t val) {
/* GP1(03h): Display enable — bit 0: 0=on, 1=off */
display_disabled = val & 1;
}
static void gp1_dma_direction(uint32_t val) {
/* GP1(04h): DMA direction — bits 0-1 */
dma_direction = val & 3;
}
static void gp1_display_area_start(uint32_t val) {
/* GP1(05h): Start of display area in VRAM
* bits 0-9: X (in halfwords, 0-1023)
* bits 10-18: Y (0-511) */
display_area_x = val & 0x3FF;
display_area_y = (val >> 10) & 0x1FF;
ws_note_display_base(display_area_x); /* learn the display buffer set (native-wide) */
}
static void gp1_h_display_range(uint32_t val) {
/* GP1(06h): Horizontal display range
* bits 0-11: X1
* bits 12-23: X2 */
h_display_x1 = val & 0xFFF;
h_display_x2 = (val >> 12) & 0xFFF;
}
static void gp1_v_display_range(uint32_t val) {
/* GP1(07h): Vertical display range
* bits 0-9: Y1
* bits 10-19: Y2 */
v_display_y1 = val & 0x3FF;
v_display_y2 = (val >> 10) & 0x3FF;
}
static void gp1_display_mode(uint32_t val) {
/* GP1(08h): Display mode
* bit 0-1: horizontal resolution 1 (0=256, 1=320, 2=512, 3=640)
* bit 2: vertical resolution (0=240, 1=480)
* bit 3: video mode (0=NTSC, 1=PAL)
* bit 4: display area color depth (0=15bit, 1=24bit)
* bit 5: vertical interlace (0=off, 1=on)
* bit 6: horizontal resolution 2 (0=normal, 1=368)
* bit 7: "reverseflag" */
hres1 = val & 3;
vres = (val >> 2) & 1;
video_mode = (val >> 3) & 1;
display_depth = (val >> 4) & 1;
vertical_interlace = (val >> 5) & 1;
hres2 = (val >> 6) & 1;
reverse_flag = (val >> 7) & 1;
}
static void gp1_get_info(uint32_t val) {
/* GP1(10h): Get GPU info — writes result to GPUREAD latch.
* Mednafen-psx masks the subcommand to 4 bits (val & 0x0F) and
* services cases 2..5, 7, 8. Tomba's ResetGraph() uses param 7 to
* read the GPU version (must be 2) to pick its video-mode path —
* the wrong value here lands the game on a no-draw branch. */
uint32_t which = val & 0x0F;
switch (which) {
case 2: /* texture window */
gpuread_latch = texture_window_value;
break;
case 3: /* draw area top-left */
gpuread_latch = draw_area_left | (draw_area_top << 10);
break;
case 4: /* draw area bottom-right */
gpuread_latch = draw_area_right | (draw_area_bottom << 10);
break;
case 5: /* draw offset */
gpuread_latch = ((uint32_t)draw_offset_x & 0x7FFu) |
(((uint32_t)draw_offset_y & 0x7FFu) << 11);
break;
case 7: /* GPU version (real-hw + mednafen return 2) */
gpuread_latch = 2;
break;
case 8: /* unknown info index, real hw / mednafen return 0 */
gpuread_latch = 0;
break;
default:
/* N=0,1,6,9..15: leave latch unchanged */
break;
}
}
void gpu_write_gp1(uint32_t val) {
uint32_t cmd = (val >> 24) & 0x3F;
switch (cmd) {
case 0x00: gp1_reset(); break;
case 0x01: gp1_reset_command_buffer(); break;
case 0x02: gp1_ack_irq1(); break;
case 0x03: gp1_display_enable(val); break;
case 0x04: gp1_dma_direction(val); break;
case 0x05: gp1_display_area_start(val); break;
case 0x06: gp1_h_display_range(val); break;
case 0x07: gp1_v_display_range(val); break;
case 0x08: gp1_display_mode(val); break;
case 0x10: case 0x11: case 0x12: case 0x13:
case 0x14: case 0x15: case 0x16: case 0x17:
case 0x18: case 0x19: case 0x1A: case 0x1B:
case 0x1C: case 0x1D: case 0x1E: case 0x1F:
gp1_get_info(val); break;
default: {
static char reason[96];
snprintf(reason, sizeof(reason),
"GPU GP1 unknown command 0x%02X (word 0x%08X)", cmd, val);
psx_fatal_halt(reason);
}
}
}
/* ---- boot snapshot: complete GPU register state (see boot_state.h) ---- */
#define GPU_SNAP_FIELDS(X) \
X(texpage_x) X(texpage_y) X(semi_transparency) X(texpage_colors) \
X(dither_enabled) X(draw_to_display) X(texture_disable) X(texture_window_value) \
X(set_mask_bit) X(check_mask_bit) \
X(interlace_field) X(reverse_flag) \
X(draw_area_left) X(draw_area_top) X(draw_area_right) X(draw_area_bottom) \
X(draw_offset_x) X(draw_offset_y) \
X(hres1) X(hres2) X(vres) X(video_mode) X(display_depth) X(vertical_interlace) \
X(display_disabled) X(irq1_flag) X(dma_direction) X(lcf) \
X(display_area_x) X(display_area_y) \
X(h_display_x1) X(h_display_x2) X(v_display_y1) X(v_display_y2) \
X(gpuread_latch) \
X(gp0_state) \
X(gp0_cmd_buf) X(gp0_words_collected) X(gp0_words_needed) \
X(gp0_next_source_addr) X(gp0_cmd_source_addr) \
X(polyline_color) X(polyline_prev_x) X(polyline_prev_y) X(polyline_prev_c) \
X(polyline_semi_trans) X(polyline_has_prev) \
X(vram_write_x) X(vram_write_y) X(vram_write_w) X(vram_write_h) \
X(vram_write_col) X(vram_write_row) X(vram_write_remaining) \
X(vram_read_active) X(vram_read_x) X(vram_read_y) X(vram_read_w) X(vram_read_h) \
X(vram_read_col) X(vram_read_row)
#define GPU_COSIM_SNAP_FIELDS(X) \
X(texpage_x) X(texpage_y) X(semi_transparency) X(texpage_colors) \
X(dither_enabled) X(draw_to_display) X(texture_disable) X(texture_window_value) \
X(set_mask_bit) X(check_mask_bit) \
X(interlace_field) X(reverse_flag) \
X(draw_area_left) X(draw_area_top) X(draw_area_right) X(draw_area_bottom) \
X(draw_offset_x) X(draw_offset_y) \
X(hres1) X(hres2) X(vres) X(video_mode) X(display_depth) X(vertical_interlace) \
X(display_disabled) X(irq1_flag) X(dma_direction) X(lcf) \
X(display_area_x) X(display_area_y) \
X(h_display_x1) X(h_display_x2) X(v_display_y1) X(v_display_y2) \
X(gpuread_latch) \
X(gp0_state) \
X(gp0_cmd_buf) X(gp0_words_collected) X(gp0_words_needed) \
X(polyline_color) X(polyline_prev_x) X(polyline_prev_y) X(polyline_prev_c) \
X(polyline_semi_trans) X(polyline_has_prev) \
X(vram_write_x) X(vram_write_y) X(vram_write_w) X(vram_write_h) \
X(vram_write_col) X(vram_write_row) X(vram_write_remaining) \
X(vram_read_active) X(vram_read_x) X(vram_read_y) X(vram_read_w) X(vram_read_h) \
X(vram_read_col) X(vram_read_row)
uint32_t gpu_snapshot_bytes(void){ uint32_t n=0;
#define X(f) n += (uint32_t)sizeof(f);
GPU_SNAP_FIELDS(X)
#undef X
return n; }
void gpu_snapshot_write(uint8_t *p){
#define X(f) memcpy(p,&(f),sizeof(f)); p+=sizeof(f);
GPU_SNAP_FIELDS(X)
#undef X
}
uint32_t gpu_cosim_snapshot_bytes(void){ uint32_t n=0;
#define X(f) n += (uint32_t)sizeof(f);
GPU_COSIM_SNAP_FIELDS(X)
#undef X
return n; }
void gpu_cosim_snapshot_write(uint8_t *p){
#define X(f) memcpy(p,&(f),sizeof(f)); p+=sizeof(f);
GPU_COSIM_SNAP_FIELDS(X)
#undef X
}
void gpu_cosim_dump(char *out, int cap) {
if (!out || cap <= 0) return;
char *p = out;
size_t rem = (size_t)cap;
#define APPEND(fmt, ...) do { \
if (rem <= 1) break; \
int w = snprintf(p, rem, fmt, __VA_ARGS__); \
if (w < 0) { out[0] = 0; return; } \
if ((size_t)w >= rem) { p += rem - 1; rem = 1; } \
else { p += (size_t)w; rem -= (size_t)w; } \
} while (0)
#define X(f) APPEND(" %s %llx", #f, (unsigned long long)(uint32_t)(f));
X(texpage_x) X(texpage_y) X(semi_transparency) X(texpage_colors)
X(dither_enabled) X(draw_to_display) X(texture_disable) X(texture_window_value)
X(set_mask_bit) X(check_mask_bit)
X(interlace_field) X(reverse_flag)
X(draw_area_left) X(draw_area_top) X(draw_area_right) X(draw_area_bottom)
X(draw_offset_x) X(draw_offset_y)
X(hres1) X(hres2) X(vres) X(video_mode) X(display_depth) X(vertical_interlace)
X(display_disabled) X(irq1_flag) X(dma_direction) X(lcf)
X(display_area_x) X(display_area_y)
X(h_display_x1) X(h_display_x2) X(v_display_y1) X(v_display_y2)
X(gpuread_latch)
X(gp0_state)
for (int i = 0; i < 16; i++) {
APPEND(" gp0_cmd_buf%d %08x", i, gp0_cmd_buf[i]);
}
X(gp0_words_collected) X(gp0_words_needed)
X(gp0_next_source_addr) X(gp0_cmd_source_addr)
X(polyline_color) X(polyline_prev_x) X(polyline_prev_y) X(polyline_prev_c)
X(polyline_semi_trans) X(polyline_has_prev)
X(vram_write_x) X(vram_write_y) X(vram_write_w) X(vram_write_h)
X(vram_write_col) X(vram_write_row) X(vram_write_remaining)
X(vram_read_active) X(vram_read_x) X(vram_read_y) X(vram_read_w) X(vram_read_h)
X(vram_read_col) X(vram_read_row)
#undef X
APPEND("%s", "\n");
#undef APPEND
}
int gpu_snapshot_read(const uint8_t *p, uint32_t len){ if(len!=gpu_snapshot_bytes()) return 0;
#define X(f) memcpy(&(f),p,sizeof(f)); p+=sizeof(f);
GPU_SNAP_FIELDS(X)
#undef X
return 1; }
uint16_t* gpu_get_vram_ptr(void){ return vram; }
uint32_t gpu_get_vram_bytes(void){ return (uint32_t)sizeof(vram); }