Something went wrong. Try again.
A Playstation One (PS1) emulator fully written in Jam
Something went wrong. Try again.
jamstation gpu.jam
43 kB · 1211 lines
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212// SPDX-License-Identifier: MIT// Copyright (c) 2026 Raphael Amorim
// [0..15] command FIFO buffer// [16] buf_index// [17] cmd_args_remaining// [18] cmd_data_remaining// [19] state (0=RECV_CMD, 1=RECV_ARGS, 2=RECV_DATA)// [20] recv_data (latched arg byte for RECV_DATA pump)// [21] color (current command's 24-bit BGR colour)// [22..29] generic command counters: xpos, ypos, xsiz, ysiz, tsiz,// addr, xcnt, ycnt// [30..33] unused (was v0..v3 vertex scratch; the rasterizer reads none)// [34..37] OWNED BY main.jam's frame loop, NOT the GPU: [34]=last CPU-clocked// SPU sample, [35]=SPU sample accumulator, [36]=scanline,// [37]=f32 GPU-cycle accumulator (type-punned). Do NOT reuse here.// [38..43] C0 (VRAM->CPU) state: xcnt, ycnt, xsiz, ysiz, tsiz, addr// [44] gp1_10h_req// [45] gpuread// [46] gpustat// [47] display_mode// [48..51] draw_x1, draw_y1, draw_x2, draw_y2// [52..53] off_x, off_y (11-bit signed, sign-extended)// [54..57] texw_mx, texw_my, texw_ox, texw_oy// [58..59] clut_x, clut_y// [60..62] texp_x, texp_y, texp_d// [63..68] disp_x, disp_y, disp_x1..disp_y2// [69] unused// [70] texture-disable raw input (E1 bit 11)// [71] texture-disable allow gate (GP1 0x09); GPUSTAT.15 = g[71] & g[70]
const { irqRaise, IC_VBLANK } = import("irq");
const GPU_STATE_WORDS: u32 = 80;const VRAM_PIXELS: u32 = 524288;const GP_RECV_CMD: u32 = 0;const GP_RECV_ARGS: u32 = 1;const GP_RECV_DATA: u32 = 2;
pub fn gpuToBgr555(c: u32) u32 { return ((c & 0x0000F8) >> 3) | ((c & 0x00F800) >> 6) | ((c & 0xF80000) >> 9);}
pub fn gpuFamilyCount(g: *mut[] u32, family: u32) u32 { return g[72 + (family & 7)];}
pub fn vramWritePixel(vram: *mut[] u8, x: u32, y: u32, bgr555: u32) { if (x >= 1024 || y >= 512) { return; } const off: u32 = (y * 1024 + x) * 2; vram[off] = (bgr555 & 0xFF) as u8; vram[off + 1] = ((bgr555 >> 8) & 0xFF) as u8;}
pub fn vramReadPixel(vram: *mut[] u8, x: u32, y: u32) u32 { if (x >= 1024 || y >= 512) { return 0; } const off: u32 = (y * 1024 + x) * 2; return (vram[off] as u32) | ((vram[off + 1] as u32) << 8);}
pub fn gpuRead32(g: *mut[] u32, vram: *mut[] u8, off: u32) u32 { if (off == 0x00) { var data: u32 = 0; if (g[42] != 0) { // C0 (VRAM->CPU) transfer in progress - drain two pixels. data = vramPackTwo(g, vram); g[42] = g[42] - 2; } if (g[44] != 0) { const sub: u32 = g[44] & 7; if (sub == 2) { data = ((g[57] >> 3) << 15) | ((g[56] >> 3) << 10) | ((g[55] >> 3) << 5) | (g[54] >> 3); } if (sub == 3) { data = (g[49] << 10) | g[48]; } if (sub == 4) { data = (g[51] << 10) | g[50]; } if (sub == 5) { data = (g[53] << 10) | g[52]; } g[44] = 0; } return data; } if (off == 0x04) { // OR GPUSTAT with 0x1C000000 to keep the BIOS happy. return g[46] | 0x1C000000; } return 0;}
pub fn gpuRead16(g: *mut[] u32, vram: *mut[] u8, off: u32) u32 { return gpuRead32(g, vram, off & 0xFFFFFFFC) & 0xFFFF;}
pub fn gpuRead8(g: *mut[] u32, vram: *mut[] u8, off: u32) u32 { return gpuRead32(g, vram, off & 0xFFFFFFFC) & 0xFF;}
pub fn vramPackTwo(g: *mut[] u32, vram: *mut[] u8) u32 { const baseX: u32 = g[43] & 0x3FF; const baseY: u32 = (g[43] >> 10) & 0x1FF; var x0: u32 = (baseX + g[38]) & 0x3FF; var y0: u32 = (baseY + g[39]) & 0x1FF; var data: u32 = vramReadPixel(vram, x0, y0); g[38] = g[38] + 1; if (g[38] == g[40]) { g[39] = g[39] + 1; g[38] = 0; } g[45] = g[45] - 1; if (g[45] > 0) { var x1: u32 = (baseX + g[38]) & 0x3FF; var y1: u32 = (baseY + g[39]) & 0x1FF; data = data | (vramReadPixel(vram, x1, y1) << 16); g[38] = g[38] + 1; if (g[38] == g[40]) { g[39] = g[39] + 1; g[38] = 0; } g[45] = g[45] - 1; } return data;}
pub fn gpuWrite32(g: *mut[] u32, vram: *mut[] u8, off: u32, val: u32) { match (off) { 0x00 { gpuGp0(g, vram, val); } 0x04 { gpuGp1(g, val); } _ {} }}
pub fn gpuWrite16(g: *mut[] u32, vram: *mut[] u8, off: u32, val: u32) { gpuWrite32(g, vram, off, val & 0xFFFF);}
pub fn gpuWrite8(g: *mut[] u32, vram: *mut[] u8, off: u32, val: u32) { gpuWrite32(g, vram, off, val & 0xFF);}
pub fn gpuGp0(g: *mut[] u32, vram: *mut[] u8, val: u32) { const st: u32 = g[19]; if (st == GP_RECV_CMD) { g[16] = 0; g[g[16]] = val; g[16] = g[16] + 1; gpuUpdateCmd(g, vram); } else if (st == GP_RECV_ARGS) { g[g[16]] = val; g[16] = g[16] + 1; if (g[17] > 0) { g[17] = g[17] - 1; } gpuUpdateCmd(g, vram); } else { g[20] = val; gpuUpdateCmd(g, vram); }}
// GP1 (control) - display enable, DMA direction, display area, etc.pub fn gpuGp1(g: *mut[] u32, val: u32) { match ((val >> 24) & 0xFF) { // GP1(0x00): leave gpustat untouched here. // A previous clobber of gpustat wiped texpage / display-enable bits // accumulated from prior GP0 commands; diff-trace showed Musashi // reading GPUSTAT and getting the wrong value at PC=0x8005C204. // Just reset the command-buffer state; leave gpustat alone. 0x00 | 0x01 { g[19] = GP_RECV_CMD; g[16] = 0; g[17] = 0; g[18] = 0; } // Acknowledge GPU IRQ (clear bit 24 of GPUSTAT). 0x02 { g[46] = g[46] & (~0x01000000); } // Display enable: bit 23 of GPUSTAT = 1 when disabled. 0x03 { g[46] = g[46] & (~0x00800000); g[46] = g[46] | ((val << 23) & 0x00800000); } // GP1(04h) DMA direction. Handled as a no-op here: the direction is // never reflected into GPUSTAT bits 29-30, so they stay 0. Real // hardware *does* set them, but reflecting the direction here made // GPUSTAT bit 30 read 1 (dir 2), and a game branch on that bit then // diverged (REG_TRACE cmp, L≈19.477M). Keep it a no-op. 0x04 { } 0x05 { g[63] = val & 0x3FF; g[64] = (val >> 10) & 0x1FF; } 0x06 { g[65] = val & 0xFFF; g[66] = (val >> 12) & 0xFFF; } 0x07 { g[67] = val & 0x1FF; g[68] = (val >> 10) & 0x1FF; } 0x08 { g[47] = val & 0xFFFFFF; } // GP1(0x09) - Allow Texture Disable. Stores the allow flag in g[71]; // GPUSTAT.15 is NOT updated immediately (ps1-tests // testUnsetAllowTextureDisablePreservesBit verifies that the // current bit-15 stays put until the next GP0_E1 write). 0x09 { g[71] = val & 1; } 0x10 { g[44] = val & 7; } _ {} }}
pub fn gpuUpdateCmd(g: *mut[] u32, vram: *mut[] u8) { const cmd: u32 = (g[0] >> 24) & 0xFF; const family: u32 = (g[0] >> 29) & 7;
if (g[19] == GP_RECV_CMD) { g[72 + family] = g[72 + family] + 1; }
match (family) { 1 { gpuPolyCmd(g, vram); return; } 2 { gpuLineCmd(g, vram); return; } 3 { gpuRectCmd(g, vram); return; } _ {} }
match (cmd) { 0x00 | 0x01 { g[19] = GP_RECV_CMD; return; } 0x02 { gpuFillRect(g, vram); return; }
0xE1 { g[46] = (g[46] & 0xFFFFF800) | (g[0] & 0x7FF); g[70] = (g[0] >> 11) & 1; g[46] = (g[46] & 0xFFFF7FFF) | ((g[71] & g[70]) << 15); g[60] = (g[46] & 0xF) << 6; g[61] = (g[46] & 0x10) << 4; g[62] = (g[46] >> 7) & 3; g[19] = GP_RECV_CMD; return; } 0xE2 { g[54] = (g[0] & 0x1F) << 3; g[55] = ((g[0] >> 5) & 0x1F) << 3; g[56] = ((g[0] >> 10) & 0x1F) << 3; g[57] = ((g[0] >> 15) & 0x1F) << 3; g[19] = GP_RECV_CMD; return; } 0xE3 { g[48] = g[0] & 0x3FF; g[49] = (g[0] >> 10) & 0x1FF; g[19] = GP_RECV_CMD; return; } 0xE4 { g[50] = g[0] & 0x3FF; g[51] = (g[0] >> 10) & 0x1FF; g[19] = GP_RECV_CMD; return; } 0xE5 { var ox: u32 = g[0] & 0x7FF; var oy: u32 = (g[0] >> 11) & 0x7FF; if ((ox & 0x400) != 0) { ox = ox | 0xFFFFF800; } if ((oy & 0x400) != 0) { oy = oy | 0xFFFFF800; } g[52] = ox; g[53] = oy; g[19] = GP_RECV_CMD; return; } 0xE6 { g[46] = (g[46] & 0xFFFFE7FF) | (((g[0] & 0x03) << 11) & 0x1800); g[58] = g[0] & 0x01; // set-mask flag (raster-time) g[59] = (g[0] >> 1) & 0x01; // check-mask flag g[19] = GP_RECV_CMD; return; }
0x80 { gpuVramToVram(g, vram); return; } 0xA0 { gpuCpuToVram(g, vram); return; } 0xC0 { gpuVramToCpu(g, vram); return; } _ { g[19] = GP_RECV_CMD; } }}
pub fn gpuFillRect(g: *mut[] u32, vram: *mut[] u8) { if (g[19] == GP_RECV_CMD) { g[19] = GP_RECV_ARGS; g[17] = 2; return; } if (g[17] != 0) { return; } const color: u32 = gpuToBgr555(g[0] & 0xFFFFFF); const x0: u32 = g[1] & 0x3F0; const y0: u32 = (g[1] >> 16) & 0x1FF; const w: u32 = ((g[2] & 0x3FF) + 0xF) & 0xFFFFFFF0; const h: u32 = (g[2] >> 16) & 0x1FF; var y: u32 = 0; while (y < h) { var x: u32 = 0; while (x < w) { vramWritePixel(vram, x0 + x, y0 + y, color); x = x + 1; } y = y + 1; } g[19] = GP_RECV_CMD;}
pub fn gpuVramToVram(g: *mut[] u32, vram: *mut[] u8) { if (g[19] == GP_RECV_CMD) { g[19] = GP_RECV_ARGS; g[17] = 3; return; } if (g[17] != 0) { return; } const sx: u32 = g[1] & 0xFFFF; const sy: u32 = (g[1] >> 16) & 0xFFFF; const dx: u32 = g[2] & 0xFFFF; const dy: u32 = (g[2] >> 16) & 0xFFFF; const w: u32 = g[3] & 0xFFFF; const h: u32 = (g[3] >> 16) & 0xFFFF; var y: u32 = 0; while (y < h) { var x: u32 = 0; while (x < w) { const px: u32 = vramReadPixel(vram, sx + x, sy + y); vramWritePixel(vram, dx + x, dy + y, px); x = x + 1; } y = y + 1; } g[19] = GP_RECV_CMD;}
pub fn gpuCpuToVram(g: *mut[] u32, vram: *mut[] u8) { if (g[19] == GP_RECV_CMD) { g[19] = GP_RECV_ARGS; g[17] = 2; return; } if (g[19] == GP_RECV_ARGS) { if (g[17] != 0) { return; } g[22] = g[1] & 0x3FF; g[23] = (g[1] >> 16) & 0x1FF; var xs: u32 = g[2] & 0xFFFF; var ys: u32 = (g[2] >> 16) & 0xFFFF; xs = ((xs - 1) & 0x3FF) + 1; ys = ((ys - 1) & 0x1FF) + 1; g[24] = xs; g[25] = ys; g[26] = ((xs * ys) + 1) & 0xFFFFFFFE; g[28] = 0; g[29] = 0; g[19] = GP_RECV_DATA; return; } const x0: u32 = (g[22] + g[28]) & 0x3FF; const y0: u32 = (g[23] + g[29]) & 0x1FF; vramWritePixel(vram, x0, y0, g[20] & 0xFFFF); g[28] = g[28] + 1; if (g[28] == g[24]) { g[29] = g[29] + 1; g[28] = 0; } const x1: u32 = (g[22] + g[28]) & 0x3FF; const y1: u32 = (g[23] + g[29]) & 0x1FF; vramWritePixel(vram, x1, y1, (g[20] >> 16) & 0xFFFF); g[28] = g[28] + 1; if (g[28] == g[24]) { g[29] = g[29] + 1; g[28] = 0; } g[26] = g[26] - 2; if (g[26] == 0) { g[28] = 0; g[29] = 0; g[19] = GP_RECV_CMD; }}
pub fn gpuVramToCpu(g: *mut[] u32, vram: *mut[] u8) { if (g[19] == GP_RECV_CMD) { g[19] = GP_RECV_ARGS; g[17] = 2; return; } if (g[17] != 0) { return; } const x0: u32 = g[1] & 0x3FF; const y0: u32 = (g[1] >> 16) & 0x1FF; var xs: u32 = g[2] & 0xFFFF; var ys: u32 = (g[2] >> 16) & 0xFFFF; xs = ((xs - 1) & 0x3FF) + 1; ys = ((ys - 1) & 0x1FF) + 1; g[38] = 0; g[39] = 0; g[40] = xs; g[41] = ys; g[42] = ((xs * ys) + 1) & 0xFFFFFFFE; g[43] = x0 | (y0 << 10); g[45] = xs * ys; g[19] = GP_RECV_CMD;}
pub fn gpuPolyCmd(g: *mut[] u32, vram: *mut[] u8) { if (g[19] == GP_RECV_CMD) { const flags: u32 = (g[0] >> 24) & 0xFF; const isQuad: bool = (flags & 0x08) != 0; const isTex: bool = (flags & 0x04) != 0; const isGour: bool = (flags & 0x10) != 0; var verts: u32 = 3; if (isQuad) { verts = 4; } var perVert: u32 = 1; if (isTex) { perVert = perVert + 1; } if (isGour) { perVert = perVert + 1; } var args: u32 = perVert * verts; if (isGour) { args = args - 1; } g[17] = args; g[19] = GP_RECV_ARGS; return; } if (g[17] != 0) { return; } g[19] = GP_RECV_CMD; gpuRasterPoly(g, vram);}
pub fn gpuSe11(v: u32) u32 { if ((v & 0x400) != 0) { return v | 0xFFFFF800; } return v & 0x7FF;}
pub fn gpuVertX(xy: u32) u32 { return gpuSe11(xy & 0xFFFF);}
pub fn gpuVertY(xy: u32) u32 { return gpuSe11((xy >> 16) & 0xFFFF);}
pub fn gpuEdge(ax: u32, ay: u32, bx: u32, by: u32, cx: u32, cy: u32) i64 { const ax64: i64 = sext32(ax); const ay64: i64 = sext32(ay); const bx64: i64 = sext32(bx); const by64: i64 = sext32(by); const cx64: i64 = sext32(cx); const cy64: i64 = sext32(cy); return (bx64 - ax64) * (cy64 - ay64) - (by64 - ay64) * (cx64 - ax64);}
pub fn sext32(v: u32) i64 { var w: i64 = v as i64; if ((v & 0x80000000) != 0) { w = w - 0x100000000; } return w;}
pub fn gpuTopLeftRule(z: i64, ax: u32, ay: u32, bx: u32, by: u32) bool { if (z < 0) { return true; } if (z != 0) { return false; } const ays: i64 = sext32(ay); const bys: i64 = sext32(by); const axs: i64 = sext32(ax); const bxs: i64 = sext32(bx); if (bys > ays) { return true; } if (bys == ays && bxs < axs) { return true; } return false;}
pub fn gpuMin3(a: u32, b: u32, c: u32) u32 { var m: u32 = a; if (sext32(b) < sext32(m)) { m = b; } if (sext32(c) < sext32(m)) { m = c; } return m;}
pub fn gpuMax3(a: u32, b: u32, c: u32) u32 { var m: u32 = a; if (sext32(b) > sext32(m)) { m = b; } if (sext32(c) > sext32(m)) { m = c; } return m;}
pub fn gpuRasterPoly(g: *mut[] u32, vram: *mut[] u8) { const flags: u32 = (g[0] >> 24) & 0xFF; const isQuad: bool = (flags & 0x08) != 0; const isTex: bool = (flags & 0x04) != 0; const isGour: bool = (flags & 0x10) != 0; const isRaw: bool = (flags & 0x01) != 0;
// PSX command layout per nocash psx-spx: // monochrome triangle buf[0]=cmd+colour, buf[1..3]=xy // monochrome quad buf[0]=cmd+colour, buf[1..4]=xy // textured triangle buf[0]=cmd+colour, buf[1]=vv0xy, // buf[2]=vv0clut+uv, buf[3]=vv1xy, // buf[4]=vv1page+uv, buf[5]=vv2xy, // buf[6]=vv2uv // textured quad +buf[7]=vv3xy, buf[8]=vv3uv // shaded triangle buf[0]=colour0+cmd, buf[1]=vv0xy, // buf[2]=colour1, buf[3]=vv1xy, ... // We only render mono + textured here; shaded reuses the base // colour from buf[0].
var v0x: u32 = 0; var v0y: u32 = 0; var v1x: u32 = 0; var v1y: u32 = 0; var v2x: u32 = 0; var v2y: u32 = 0; var v3x: u32 = 0; var v3y: u32 = 0; var uu0: u32 = 0; var vv0: u32 = 0; var uu1: u32 = 0; var vv1: u32 = 0; var uu2: u32 = 0; var vv2: u32 = 0; var uu3: u32 = 0; var vv3: u32 = 0;
var c0: u32 = g[0] & 0xFFFFFF; var c1: u32 = c0; var c2: u32 = c0; var c3: u32 = c0; var clutX: u32 = 0; var clutY: u32 = 0; var tpx: u32 = g[60]; var tpy: u32 = g[61]; var depth: u32 = g[62];
// Decode the four PSX polygon-command shapes: // mono flat buf[0]=cmd+col, buf[1..n]=xy // tex flat buf[0]=cmd+col, then per-vert (xy, page/clut+uv) // mono shaded buf[0]=col0+cmd, then per-vert (xy, colN+pad) // tex shaded buf[0]=col0+cmd, then per-vert (xy, page/clut+uv, colN) if (isTex && isGour) { v0x = gpuVertX(g[1]); v0y = gpuVertY(g[1]); uu0 = g[2] & 0xFF; vv0 = (g[2] >> 8) & 0xFF; const clut: u32 = (g[2] >> 16) & 0xFFFF; clutX = (clut & 0x3F) << 4; clutY = (clut >> 6) & 0x1FF; c1 = g[3] & 0xFFFFFF; v1x = gpuVertX(g[4]); v1y = gpuVertY(g[4]); uu1 = g[5] & 0xFF; vv1 = (g[5] >> 8) & 0xFF; const page: u32 = (g[5] >> 16) & 0xFFFF; tpx = (page & 0xF) << 6; tpy = (page & 0x10) << 4; depth = (page >> 7) & 3; // Undocumented behaviour (fixes Mortal Kombat II, Bubble Bobble, // Driver 1 & 2): textured polys update GPUSTAT bits 0-8 from the // polygon's tpage byte. Without this, the GPUSTAT texpage bits // drift from the expected value. // Undocumented behaviour: textured polys also update GPUSTAT // bits 0-8 from the tpage byte, and (when GP1(0x09) "allow // texture disable" is on) also drive bit 15 from tpage bit 11. // ps1-tests gpu/gp0-e1 testTexturedPolygonsTextureDisable verifies. g[46] = (g[46] & 0xFFFFFE00) | (page & 0x1FF); g[70] = (page >> 11) & 1; g[46] = (g[46] & 0xFFFF7FFF) | ((g[71] & g[70]) << 15); g[60] = tpx; g[61] = tpy; g[62] = depth; c2 = g[6] & 0xFFFFFF; v2x = gpuVertX(g[7]); v2y = gpuVertY(g[7]); uu2 = g[8] & 0xFF; vv2 = (g[8] >> 8) & 0xFF; if (isQuad) { c3 = g[9] & 0xFFFFFF; v3x = gpuVertX(g[10]); v3y = gpuVertY(g[10]); uu3 = g[11] & 0xFF; vv3 = (g[11] >> 8) & 0xFF; } } else if (isTex) { v0x = gpuVertX(g[1]); v0y = gpuVertY(g[1]); uu0 = g[2] & 0xFF; vv0 = (g[2] >> 8) & 0xFF; const clut: u32 = (g[2] >> 16) & 0xFFFF; clutX = (clut & 0x3F) << 4; clutY = (clut >> 6) & 0x1FF; v1x = gpuVertX(g[3]); v1y = gpuVertY(g[3]); uu1 = g[4] & 0xFF; vv1 = (g[4] >> 8) & 0xFF; const page: u32 = (g[4] >> 16) & 0xFFFF; tpx = (page & 0xF) << 6; tpy = (page & 0x10) << 4; depth = (page >> 7) & 3; // Undocumented behaviour: textured polys also update GPUSTAT // bits 0-8 from the tpage byte, and (when GP1(0x09) "allow // texture disable" is on) also drive bit 15 from tpage bit 11. // ps1-tests gpu/gp0-e1 testTexturedPolygonsTextureDisable verifies. g[46] = (g[46] & 0xFFFFFE00) | (page & 0x1FF); g[70] = (page >> 11) & 1; g[46] = (g[46] & 0xFFFF7FFF) | ((g[71] & g[70]) << 15); // Undocumented behaviour (fixes Mortal Kombat II, Bubble Bobble, // Driver 1 & 2 - and Musashi FMV): // propagate the polygon's tpage into the derived globals so a // following textured RECTANGLE (which carries clut but no // tpage) inherits this state instead of stale values. Without // it, the rect samples the wrong VRAM region -> texel 0 -> // texel-0 skip -> entire FMV frame is black. g[60] = tpx; g[61] = tpy; g[62] = depth; v2x = gpuVertX(g[5]); v2y = gpuVertY(g[5]); uu2 = g[6] & 0xFF; vv2 = (g[6] >> 8) & 0xFF; if (isQuad) { v3x = gpuVertX(g[7]); v3y = gpuVertY(g[7]); uu3 = g[8] & 0xFF; vv3 = (g[8] >> 8) & 0xFF; } } else if (isGour) { // gouraud flat (cmd 0x30, 0x32, 0x38, 0x3A) - col0 already in c0 v0x = gpuVertX(g[1]); v0y = gpuVertY(g[1]); c1 = g[2] & 0xFFFFFF; v1x = gpuVertX(g[3]); v1y = gpuVertY(g[3]); c2 = g[4] & 0xFFFFFF; v2x = gpuVertX(g[5]); v2y = gpuVertY(g[5]); if (isQuad) { c3 = g[6] & 0xFFFFFF; v3x = gpuVertX(g[7]); v3y = gpuVertY(g[7]); } } else { // monochrome flat (cmd 0x20, 0x22, 0x28, 0x2A) v0x = gpuVertX(g[1]); v0y = gpuVertY(g[1]); v1x = gpuVertX(g[2]); v1y = gpuVertY(g[2]); v2x = gpuVertX(g[3]); v2y = gpuVertY(g[3]); if (isQuad) { v3x = gpuVertX(g[4]); v3y = gpuVertY(g[4]); } }
const baseColor: u32 = g[0] & 0xFFFFFF;
const isTransp: bool = (flags & 0x02) != 0; var transpMode: u32 = (g[46] >> 5) & 3; if (isTex) { var page: u32 = (g[4] >> 16) & 0xFFFF; if (isGour) { page = (g[5] >> 16) & 0xFFFF; } transpMode = (page >> 5) & 3; }
gpuRasterTri(g, vram, isTex, isRaw, isTransp, transpMode, isGour, v0x, v0y, uu0, vv0, c0, v1x, v1y, uu1, vv1, c1, v2x, v2y, uu2, vv2, c2, tpx, tpy, clutX, clutY, depth, baseColor); if (isQuad) { gpuRasterTri(g, vram, isTex, isRaw, isTransp, transpMode, isGour, v1x, v1y, uu1, vv1, c1, v2x, v2y, uu2, vv2, c2, v3x, v3y, uu3, vv3, c3, tpx, tpy, clutX, clutY, depth, baseColor); }}
pub fn gpuRasterTri(g: *mut[] u32, vram: *mut[] u8, isTex: bool, isRaw: bool, isTransp: bool, transpMode: u32, isGour: bool, ax0: u32, ay0: u32, au: u32, av: u32, ac: u32, bx0: u32, by0: u32, bu: u32, bv: u32, bc: u32, cx0: u32, cy0: u32, cu: u32, cv: u32, cc: u32, tpx: u32, tpy: u32, clutX: u32, clutY: u32, depth: u32, baseColor: u32) { const ox: u32 = g[52]; const oy: u32 = g[53]; var ax: u32 = ax0 + ox; var ay: u32 = ay0 + oy; var bx: u32 = bx0 + ox; var by: u32 = by0 + oy; var cx: u32 = cx0 + ox; var cy: u32 = cy0 + oy; var bu2: u32 = bu; var bv2: u32 = bv; var cu2: u32 = cu; var cv2: u32 = cv; var bc2: u32 = bc; var cc2: u32 = cc;
const area: i64 = gpuEdge(ax, ay, bx, by, cx, cy); if (area == 0) { return; } if (area < 0) { const tx: u32 = bx; const ty: u32 = by; bx = cx; by = cy; cx = tx; cy = ty; const tu: u32 = bu2; const tv: u32 = bv2; bu2 = cu2; bv2 = cv2; cu2 = tu; cv2 = tv; const tc: u32 = bc2; bc2 = cc2; cc2 = tc; }
const xmin: u32 = gpuMin3(ax, bx, cx); const ymin: u32 = gpuMin3(ay, by, cy); const xmax: u32 = gpuMax3(ax, bx, cx); const ymax: u32 = gpuMax3(ay, by, cy); // Reject only triangles wider/taller than the GPU's real limits if (sext32(xmax - xmin) > 2048 || sext32(ymax - ymin) > 1024) { return; }
const absArea: i64 = gpuAbsI64(gpuEdge(ax, ay, bx, by, cx, cy)); const dx1: u32 = g[48]; const dy1: u32 = g[49]; const dx2: u32 = g[50]; const dy2: u32 = g[51];
var y: u32 = ymin; while (sext32(y) <= sext32(ymax)) { var x: u32 = xmin; while (sext32(x) <= sext32(xmax)) { if (sext32(x) >= sext32(dx1) && sext32(x) <= sext32(dx2) && sext32(y) >= sext32(dy1) && sext32(y) <= sext32(dy2)) { const z0: i64 = gpuEdge(bx, by, cx, cy, x, y); const z1: i64 = gpuEdge(cx, cy, ax, ay, x, y); const z2: i64 = gpuEdge(ax, ay, bx, by, x, y); if (!gpuTopLeftRule(z0, bx, by, cx, cy) && !gpuTopLeftRule(z1, cx, cy, ax, ay) && !gpuTopLeftRule(z2, ax, ay, bx, by)) { var color: u32 = 0; if (isTex) { const tx: i64 = (z0 * (au as i64) + z1 * (bu2 as i64) + z2 * (cu2 as i64)); const ty: i64 = (z0 * (av as i64) + z1 * (bv2 as i64) + z2 * (cv2 as i64)); const txu: u32 = gpuDivClamp(tx, absArea, 0xFF); const tyu: u32 = gpuDivClamp(ty, absArea, 0xFF); // GP0(0xE2) texture window: mask/offset/wrap. const txw: u32 = gpuTexWinX(g, txu); const tyw: u32 = gpuTexWinY(g, tyu); const texel: u32 = gpuFetchTexel(vram, txw, tyw, tpx, tpy, clutX, clutY, depth); if (texel == 0) { x = x + 1; continue; } if (isRaw) { color = texel; } else { // Modulate by the per-vertex gouraud colour // when shaded (the interpolated colour is the // modulator), else the flat base. var modc: u32 = baseColor; if (isGour) { const ar: i64 = (ac as i64) & 0xFF; const ag: i64 = ((ac as i64) >> 8) & 0xFF; const ab: i64 = ((ac as i64) >> 16) & 0xFF; const br: i64 = (bc2 as i64) & 0xFF; const bg: i64 = ((bc2 as i64) >> 8) & 0xFF; const bb: i64 = ((bc2 as i64) >> 16) & 0xFF; const cr: i64 = (cc2 as i64) & 0xFF; const cg: i64 = ((cc2 as i64) >> 8) & 0xFF; const cb: i64 = ((cc2 as i64) >> 16) & 0xFF; const nr: i64 = z0 * ar + z1 * br + z2 * cr; const ng: i64 = z0 * ag + z1 * bg + z2 * cg; const nb: i64 = z0 * ab + z1 * bb + z2 * cb; const ir: u32 = gpuSatChan(nr, absArea, 0); const ig: u32 = gpuSatChan(ng, absArea, 0); const ib: u32 = gpuSatChan(nb, absArea, 0); modc = ir | (ig << 8) | (ib << 16); } color = gpuModulate(texel, modc); } if (isTransp && (texel & 0x8000) != 0) { const back: u32 = vramReadPixel(vram, x & 0x3FF, y & 0x1FF); color = gpuBlend(color, back, transpMode); } } else { // Untextured: gouraud-interpolate the three // per-vertex colours by barycentric weights, or // use the base colour if not shaded. A 4x4 ordered // dither kernel is applied pre-clamp for gradient // banding suppression. var rgb: u32 = baseColor; if (isGour) { const ar: i64 = (ac as i64) & 0xFF; const ag: i64 = ((ac as i64) >> 8) & 0xFF; const ab: i64 = ((ac as i64) >> 16) & 0xFF; const br: i64 = (bc2 as i64) & 0xFF; const bg: i64 = ((bc2 as i64) >> 8) & 0xFF; const bb: i64 = ((bc2 as i64) >> 16) & 0xFF; const cr: i64 = (cc2 as i64) & 0xFF; const cg: i64 = ((cc2 as i64) >> 8) & 0xFF; const cb: i64 = ((cc2 as i64) >> 16) & 0xFF; // Unrounded barycentric numerator (z·c sum). const nr: i64 = z0 * ar + z1 * br + z2 * cr; const ng: i64 = z0 * ag + z1 * bg + z2 * cg; const nb: i64 = z0 * ab + z1 * bb + z2 * cb; // dither offset from a 4x4 kernel indexed by // (x - xmin) & 3 and (y - ymin) & 3. const dx: u32 = (x - xmin) & 3; const dy: u32 = (y - ymin) & 3; const dith: i64 = gpuDitherKernel((dy * 4) + dx); const ir: u32 = gpuSatChan(nr, absArea, dith); const ig: u32 = gpuSatChan(ng, absArea, dith); const ib: u32 = gpuSatChan(nb, absArea, dith); rgb = ir | (ig << 8) | (ib << 16); } color = gpuToBgr555(rgb); if (isTransp) { const back: u32 = vramReadPixel(vram, x & 0x3FF, y & 0x1FF); color = gpuBlend(color, back, transpMode); } } vramWritePixel(vram, x & 0x3FF, y & 0x1FF, color); } } x = x + 1; } y = y + 1; }}
pub fn gpuAbsI64(v: i64) i64 { if (v < 0) { return -v; } return v;}
// 4x4 ordered dither kernel.// Indexed by `dy * 4 + dx` where `dx = (x - xmin) & 3`, `dy = (y - ymin) & 3`.pub fn gpuDitherKernel(idx: u32) i64 { match (idx) { 0 { return -4; } 1 { return 0; } 2 { return -3; } 3 { return 1; } 4 { return 2; } 5 { return -2; } 6 { return 3; } 7 { return -1; } 8 { return -3; } 9 { return 1; } 10 { return -4; } 11 { return 0; } 12 { return 3; } 13 { return -1; } 14 { return 2; } _ { return -2; } }}
pub fn gpuSatChan(num: i64, area: i64, dith: i64) u32 { if (area == 0) { return 0; } var quot: i64 = num / area; quot = quot + dith; if (quot < 0) { quot = 0; } if (quot > 255) { quot = 255; } return quot as u32;}
pub fn gpuDivClamp(num: i64, den: i64, maxOut: u32) u32 { if (den == 0) { return 0; } var q: i64 = num / den; if (q < 0) { return 0; } if (q > (maxOut as i64)) { return maxOut; } return q as u32;}
pub fn gpuLineCmd(g: *mut[] u32, vram: *mut[] u8) { const flags: u32 = (g[0] >> 24) & 0xFF; const isGour: bool = (flags & 0x10) != 0; const isPoly: bool = (flags & 0x08) != 0; if (g[19] == GP_RECV_CMD) { g[19] = GP_RECV_ARGS; if (isPoly) { g[17] = 2; return; } var args: u32 = 2; if (isGour) { args = args + 1; } g[17] = args; return; } if (isPoly) { // A poly-line ends when the most-recently received word masks to // 0x50005000, and it draws NOTHING (the poly-line render path is // not implemented). Bound the FIFO write index so we don't // overflow the 16-word buffer. const last: u32 = g[g[16] - 1]; if ((last & 0xF000F000) == 0x50005000) { g[19] = GP_RECV_CMD; return; } g[16] = 1; return; } if (g[17] != 0) { return; } var v0xy: u32 = 0; var v1xy: u32 = 0; if (isGour) { v0xy = g[1]; v1xy = g[3]; } else { v0xy = g[1]; v1xy = g[2]; } const color: u32 = gpuToBgr555(g[0] & 0xFFFFFF); const x0: u32 = gpuVertX(v0xy) + g[52]; const y0: u32 = gpuVertY(v0xy) + g[53]; const x1: u32 = gpuVertX(v1xy) + g[52]; const y1: u32 = gpuVertY(v1xy) + g[53]; gpuPlotLine(g, vram, x0, y0, x1, y1, color); g[19] = GP_RECV_CMD;}
// Bresenham line rasterizer.pub fn gpuPlotLine(g: *mut[] u32, vram: *mut[] u8, x0: u32, y0: u32, x1: u32, y1: u32, color: u32) { const sx0: i64 = sext32(x0); const sy0: i64 = sext32(y0); const sx1: i64 = sext32(x1); const sy1: i64 = sext32(y1); var adx: i64 = sx1 - sx0; var ady: i64 = sy1 - sy0; if (adx < 0) { adx = -adx; } if (ady < 0) { ady = -ady; } if (ady < adx) { if (sx0 > sx1) { gpuPlotLineLow(g, vram, x1, y1, x0, y0, color); } else { gpuPlotLineLow(g, vram, x0, y0, x1, y1, color); } } else { if (sy0 > sy1) { gpuPlotLineHigh(g, vram, x1, y1, x0, y0, color); } else { gpuPlotLineHigh(g, vram, x0, y0, x1, y1, color); } }}
// Shallow-slope: x increments each step, y on Bresenham crossings.pub fn gpuPlotLineLow(g: *mut[] u32, vram: *mut[] u8, x0: u32, y0: u32, x1: u32, y1: u32, color: u32) { const sx0: i64 = sext32(x0); const sy0: i64 = sext32(y0); const sx1: i64 = sext32(x1); const sy1: i64 = sext32(y1); const dx: i64 = sx1 - sx0; var dy: i64 = sy1 - sy0; var yi: i64 = 1; if (dy < 0) { yi = -1; dy = -dy; } var d: i64 = (2 * dy) - dx; var y: i64 = sy0; var x: i64 = sx0; while (x < sx1) { gpuPlotPixel(g, vram, x, y, color); if (d > 0) { y = y + yi; d = d + 2 * (dy - dx); } else { d = d + 2 * dy; } x = x + 1; }}
// Steep-slope: y increments each step, x on Bresenham crossings.pub fn gpuPlotLineHigh(g: *mut[] u32, vram: *mut[] u8, x0: u32, y0: u32, x1: u32, y1: u32, color: u32) { const sx0: i64 = sext32(x0); const sy0: i64 = sext32(y0); const sx1: i64 = sext32(x1); const sy1: i64 = sext32(y1); var dx: i64 = sx1 - sx0; const dy: i64 = sy1 - sy0; var xi: i64 = 1; if (dx < 0) { xi = -1; dx = -dx; } var d: i64 = (2 * dx) - dy; var x: i64 = sx0; var y: i64 = sy0; while (y < sy1) { gpuPlotPixel(g, vram, x, y, color); if (d > 0) { x = x + xi; d = d + 2 * (dx - dy); } else { d = d + 2 * dx; } y = y + 1; }}
// Bounds-checked + drawing-area-clipped pixel write.pub fn gpuPlotPixel(g: *mut[] u32, vram: *mut[] u8, x: i64, y: i64, color: u32) { const dx1: i64 = sext32(g[48]); const dy1: i64 = sext32(g[49]); const dx2: i64 = sext32(g[50]); const dy2: i64 = sext32(g[51]); if (x < dx1 || x > dx2 || y < dy1 || y > dy2) { return; } if (x < 0 || x >= 1024 || y < 0 || y >= 512) { return; } vramWritePixel(vram, (x as u32) & 0x3FF, (y as u32) & 0x1FF, color);}
pub fn gpuRectCmd(g: *mut[] u32, vram: *mut[] u8) { if (g[19] == GP_RECV_CMD) { const flags: u32 = (g[0] >> 24) & 0xFF; const sizeBits: u32 = (flags >> 3) & 3; const isTex: bool = (flags & 0x04) != 0; var args: u32 = 1; if (sizeBits == 0) { args = args + 1; } if (isTex) { args = args + 1; } g[17] = args; g[19] = GP_RECV_ARGS; return; } if (g[17] != 0) { return; } const flags: u32 = (g[0] >> 24) & 0xFF; const sizeBits: u32 = (flags >> 3) & 3; const isTex: bool = (flags & 0x04) != 0; const isRaw: bool = (flags & 0x01) != 0; const isTransp: bool = (flags & 0x02) != 0; const transpMode: u32 = (g[46] >> 5) & 3; var bufIdx: u32 = 1; const xy: u32 = g[bufIdx]; bufIdx = bufIdx + 1; var uvword: u32 = 0; if (isTex) { uvword = g[bufIdx]; bufIdx = bufIdx + 1; } var w: u32 = 1; var h: u32 = 1; match (sizeBits) { 1 { w = 1; h = 1; } 2 { w = 8; h = 8; } 3 { w = 16; h = 16; } 0 { const sz: u32 = g[bufIdx]; w = sz & 0xFFFF; h = (sz >> 16) & 0xFFFF; } _ {} } var x: u32 = gpuSe11((xy & 0xFFFF) + g[52]); var y: u32 = gpuSe11(((xy >> 16) & 0xFFFF) + g[53]);
const dx1: i64 = sext32(g[48]); const dy1: i64 = sext32(g[49]); const dx2: i64 = sext32(g[50]); const dy2: i64 = sext32(g[51]); const sx: i64 = sext32(x); const sy: i64 = sext32(y);
if (!isTex) { const color: u32 = gpuToBgr555(g[0] & 0xFFFFFF); var iy: u32 = 0; while (iy < h) { var ix: u32 = 0; while (ix < w) { const px: i64 = sx + (ix as i64); const py: i64 = sy + (iy as i64); if (px >= dx1 && px <= dx2 && py >= dy1 && py <= dy2 && px >= 0 && px < 1024 && py >= 0 && py < 512) { var outColor: u32 = color; if (isTransp) { const back: u32 = vramReadPixel(vram, (px as u32) & 0x3FF, (py as u32) & 0x1FF); outColor = gpuBlend(color, back, transpMode); } vramWritePixel(vram, (px as u32) & 0x3FF, (py as u32) & 0x1FF, outColor); } ix = ix + 1; } iy = iy + 1; } } else { const uu0: u32 = uvword & 0xFF; const vv0: u32 = (uvword >> 8) & 0xFF; const clut: u32 = (uvword >> 16) & 0xFFFF; const clutx: u32 = (clut & 0x3F) << 4; const cluty: u32 = (clut >> 6) & 0x1FF; var iy: u32 = 0; while (iy < h) { var ix: u32 = 0; while (ix < w) { const px: i64 = sx + (ix as i64); const py: i64 = sy + (iy as i64); if (px >= dx1 && px <= dx2 && py >= dy1 && py <= dy2 && px >= 0 && px < 1024 && py >= 0 && py < 512) { const tx: u32 = gpuTexWinX(g, (uu0 + ix) & 0xFF); const ty: u32 = gpuTexWinY(g, (vv0 + iy) & 0xFF); const texel: u32 = gpuFetchTexel(vram, tx, ty, g[60], g[61], clutx, cluty, g[62]); if (texel != 0) { var color: u32 = texel; if (!isRaw) { color = gpuModulate(texel, g[0] & 0xFFFFFF); } var thisTransp: bool = false; if (isTransp) { thisTransp = (texel & 0x8000) != 0; } if (thisTransp) { const back: u32 = vramReadPixel(vram, (px as u32) & 0x3FF, (py as u32) & 0x1FF); color = gpuBlend(color, back, transpMode); } vramWritePixel(vram, (px as u32) & 0x3FF, (py as u32) & 0x1FF, color); } } ix = ix + 1; } iy = iy + 1; } } g[19] = GP_RECV_CMD;}
pub fn gpuTexWinX(g: *mut[] u32, t: u32) u32 { return ((t & (~g[54])) | (g[56] & g[54])) & 0xFF;}
pub fn gpuTexWinY(g: *mut[] u32, t: u32) u32 { return ((t & (~g[55])) | (g[57] & g[55])) & 0xFF;}
pub fn gpuFetchTexel(vram: *mut[] u8, tx: u32, ty: u32, tpx: u32, tpy: u32, clutx: u32, cluty: u32, depth: u32) u32 { if (depth == 0) { // 4 texels per VRAM pixel. const texel: u32 = vramReadPixel(vram, tpx + (tx >> 2), tpy + ty); const idx: u32 = (texel >> ((tx & 3) * 4)) & 0xF; return vramReadPixel(vram, clutx + idx, cluty); } if (depth == 1) { // 2 texels per VRAM pixel. const texel: u32 = vramReadPixel(vram, tpx + (tx >> 1), tpy + ty); const idx: u32 = (texel >> ((tx & 1) * 8)) & 0xFF; return vramReadPixel(vram, clutx + idx, cluty); } // 15bpp direct. return vramReadPixel(vram, tpx + tx, tpy + ty);}
pub fn gpuModulate(texel: u32, mod: u32) u32 { const tr: u32 = (texel & 0x1F) << 3; const tg: u32 = ((texel >> 5) & 0x1F) << 3; const tb: u32 = ((texel >> 10) & 0x1F) << 3; const mr: u32 = mod & 0xFF; const mg: u32 = (mod >> 8) & 0xFF; const mb: u32 = (mod >> 16) & 0xFF; var cr: u32 = ((tr * mr) + 0x40) >> 7; var cg: u32 = ((tg * mg) + 0x40) >> 7; var cb: u32 = ((tb * mb) + 0x40) >> 7; if (cr > 255) { cr = 255; } if (cg > 255) { cg = 255; } if (cb > 255) { cb = 255; } return gpuToBgr555(cr | (cg << 8) | (cb << 16));}
// 0 B/2 + F/2// 1 B + F// 2 B - F// 3 B + F/4pub fn gpuBlend(fore: u32, back: u32, mode: u32) u32 { const fr: u32 = (fore & 0x1F) << 3; const fg: u32 = ((fore >> 5) & 0x1F) << 3; const fb: u32 = ((fore >> 10) & 0x1F) << 3; const br: u32 = (back & 0x1F) << 3; const bg: u32 = ((back >> 5) & 0x1F) << 3; const bb: u32 = ((back >> 10) & 0x1F) << 3; var cr: u32 = 0; var cg: u32 = 0; var cb: u32 = 0; if (mode == 0) { cr = (br + fr) >> 1; cg = (bg + fg) >> 1; cb = (bb + fb) >> 1; } else if (mode == 1) { cr = br + fr; cg = bg + fg; cb = bb + fb; } else if (mode == 2) { // The blend is `br - fr`; we'd underflow in u32, so clamp at 0. if (br > fr) { cr = br - fr; } else { cr = 0; } if (bg > fg) { cg = bg - fg; } else { cg = 0; } if (bb > fb) { cb = bb - fb; } else { cb = 0; } } else { cr = br + (fr >> 2); cg = bg + (fg >> 2); cb = bb + (fb >> 2); } if (cr > 255) { cr = 255; } if (cg > 255) { cg = 255; } if (cb > 255) { cb = 255; } return gpuToBgr555(cr | (cg << 8) | (cb << 16));}
pub const Gpu = struct { buf: [80]u32,
pub fn init() Self { var s: Self = Self { buf: [0; 80] }; s.buf[19] = GP_RECV_CMD; s.buf[46] = 0x00800000; s.buf[47] = 1; return s; }
pub fn familyCount(self: Self, family: u32) u32 { return gpuFamilyCount(self.buf.asMutPtr(), family); }
pub fn read8(self: Self, vram: *mut[] u8, off: u32) u32 { return gpuRead8(self.buf.asMutPtr(), vram, off); }
pub fn read16(self: Self, vram: *mut[] u8, off: u32) u32 { return gpuRead16(self.buf.asMutPtr(), vram, off); }
pub fn read32(self: Self, vram: *mut[] u8, off: u32) u32 { return gpuRead32(self.buf.asMutPtr(), vram, off); }
pub fn write8(self: Self, vram: *mut[] u8, off: u32, val: u32) { gpuWrite8(self.buf.asMutPtr(), vram, off, val); }
pub fn write16(self: Self, vram: *mut[] u8, off: u32, val: u32) { gpuWrite16(self.buf.asMutPtr(), vram, off, val); }
pub fn write32(self: Self, vram: *mut[] u8, off: u32, val: u32) { gpuWrite32(self.buf.asMutPtr(), vram, off, val); }
pub fn gp0(self: Self, vram: *mut[] u8, val: u32) { gpuGp0(self.buf.asMutPtr(), vram, val); }
pub fn bufPtr(self: Self) *mut[] u32 { return self.buf.asMutPtr(); }};