mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
dcb63cb570
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>
3217 lines
140 KiB
C
3217 lines
140 KiB
C
/* 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); }
|