diff --git a/gpu.jam b/gpu.jam index d0af3af..0dcc73a 100644 --- a/gpu.jam +++ b/gpu.jam @@ -1,11 +1,3 @@ -// GPU (partial implementation). -// -// Implements the GP0/GP1 command machinery and the polygon, line, -// rectangle, fill, and CPU↔VRAM blit commands. Triangles are -// point-sampled with texture + gouraud support (gpuRasterPoly -> -// gpuRasterTri); together with the GTE this renders real game geometry. -// -// State buffer layout (80 u32 entries): // [0..15] command FIFO buffer // [16] buf_index // [17] cmd_args_remaining @@ -37,30 +29,21 @@ const { irqRaise, IC_VBLANK } = import("irq"); const GPU_STATE_WORDS: u32 = 80; -const VRAM_PIXELS: u32 = 524288; // 1024 * 512 - +const VRAM_PIXELS: u32 = 524288; const GP_RECV_CMD: u32 = 0; const GP_RECV_ARGS: u32 = 1; const GP_RECV_DATA: u32 = 2; -// Convert a 24-bit BGR colour (CPU side) into the GPU's 15-bit BGR555 -// VRAM format. PSX VRAM stores 1 bit mask + 5 bits each B/G/R. pub fn gpuToBgr555(c: u32) u32 { return ((c & 0x0000F8) >> 3) | ((c & 0x00F800) >> 6) | ((c & 0xF80000) >> 9); } -// End-of-run diagnostic: how many GP0 commands of each family did the -// BIOS issue? Family index = top 3 bits of the command byte (0 = misc, -// 1 = polygon, 2 = line, 3 = rect, ...). pub fn gpuFamilyCount(g: *mut[] u32, family: u32) u32 { return g[72 + (family & 7)]; } -// VRAM is a `*mut[] u8` byte buffer storing little-endian u16 BGR555 -// pixels. These helpers convert between a (x, y) pair and a VRAM byte -// offset and read/write a single pixel. 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; @@ -74,8 +57,6 @@ pub fn vramReadPixel(vram: *mut[] u8, x: u32, y: u32) u32 { return (vram[off] as u32) | ((vram[off + 1] as u32) << 8); } -// GP0 / GP1 reads - pub fn gpuRead32(g: *mut[] u32, vram: *mut[] u8, off: u32) u32 { if (off == 0x00) { var data: u32 = 0; @@ -112,10 +93,6 @@ pub fn gpuRead8(g: *mut[] u32, vram: *mut[] u8, off: u32) u32 { return gpuRead32(g, vram, off & 0xFFFFFFFC) & 0xFF; } -// Pack two adjacent VRAM pixels into a single u32 for the GPUREAD port. -// For odd-pixel transfers (e.g. 1×1), the high 16 bits are zero - real -// hardware doesn't read past the rect bounds. ps1-tests gpu/mask-bit -// verifies this with `vramGet(x,y) == 0x8000` after a single-pixel write. pub fn vramPackTwo(g: *mut[] u32, vram: *mut[] u8) u32 { const baseX: u32 = g[43] & 0x3FF; const baseY: u32 = (g[43] >> 10) & 0x1FF; @@ -125,9 +102,6 @@ pub fn vramPackTwo(g: *mut[] u32, vram: *mut[] u8) u32 { g[38] = g[38] + 1; if (g[38] == g[40]) { g[39] = g[39] + 1; g[38] = 0; } g[45] = g[45] - 1; - // Second half-pixel: only fetch if at least one more real pixel - // remains in the transfer. Odd-sized reads (e.g. 1×1) leave the - // high half of the final word as zero. if (g[45] > 0) { var x1: u32 = (baseX + g[38]) & 0x3FF; var y1: u32 = (baseY + g[39]) & 0x1FF; @@ -139,8 +113,6 @@ pub fn vramPackTwo(g: *mut[] u32, vram: *mut[] u8) u32 { return data; } -// GP0 (command/data) writes - pub fn gpuWrite32(g: *mut[] u32, vram: *mut[] u8, off: u32, val: u32) { match (off) { 0x00 { gpuGp0(g, vram, val); } @@ -157,9 +129,6 @@ pub fn gpuWrite8(g: *mut[] u32, vram: *mut[] u8, off: u32, val: u32) { gpuWrite32(g, vram, off, val & 0xFF); } -// GP0 dispatch - state machine: RECV_CMD latches a new command word -// into buf[0], RECV_ARGS appends additional words, RECV_DATA streams -// pixels for CPU->VRAM. pub fn gpuGp0(g: *mut[] u32, vram: *mut[] u8, val: u32) { const st: u32 = g[19]; if (st == GP_RECV_CMD) { @@ -228,16 +197,10 @@ pub fn gpuGp1(g: *mut[] u32, val: u32) { } } -// GP0 command parser - branches on the top three bits of buf[0] for -// the polygon / line / rect families, and falls through to a per-byte -// switch for everything else. pub fn gpuUpdateCmd(g: *mut[] u32, vram: *mut[] u8) { const cmd: u32 = (g[0] >> 24) & 0xFF; const family: u32 = (g[0] >> 29) & 7; - // Family histogram for end-of-run diagnostics. We track 8 buckets - // (top 3 bits of the command byte). 1 = polygon, 2 = line, 3 = - // rect, 4 = blit, 5 = drawmode setup. Stored at g[72..79]. if (g[19] == GP_RECV_CMD) { g[72 + family] = g[72 + family] + 1; } @@ -250,16 +213,15 @@ pub fn gpuUpdateCmd(g: *mut[] u32, vram: *mut[] u8) { } match (cmd) { - // family 0 - misc commands and drawing settings. - 0x00 | 0x01 { g[19] = GP_RECV_CMD; return; } // nop / clear cache - 0x02 { gpuFillRect(g, vram); return; } + 0x00 | 0x01 { + g[19] = GP_RECV_CMD; return; + } + 0x02 { + gpuFillRect(g, vram); return; + } - // family 7 (drawing settings) - single-word commands. 0xE1 { g[46] = (g[46] & 0xFFFFF800) | (g[0] & 0x7FF); - // Latch E1.bit11 unconditionally into g[70] (the "raw" texture- - // disable input). GPUSTAT.15 is the gated output: g[71] & g[70]. - // GP1(0x09) updates the gate without touching g[70]. g[70] = (g[0] >> 11) & 1; g[46] = (g[46] & 0xFFFF7FFF) | ((g[71] & g[70]) << 15); g[60] = (g[46] & 0xF) << 6; @@ -289,7 +251,6 @@ pub fn gpuUpdateCmd(g: *mut[] u32, vram: *mut[] u8) { return; } 0xE5 { - // Sign-extend the 11-bit signed offsets. var ox: u32 = g[0] & 0x7FF; var oy: u32 = (g[0] >> 11) & 0x7FF; if ((ox & 0x400) != 0) { ox = ox | 0xFFFFF800; } @@ -300,11 +261,6 @@ pub fn gpuUpdateCmd(g: *mut[] u32, vram: *mut[] u8) { return; } 0xE6 { - // GP0(0xE6) - Mask Bit Setting. Two bits matter: - // bit 0: Set Mask while drawing (force pixel bit 15 = 1) - // bit 1: Check Mask before draw (skip if existing pixel bit 15 = 1) - // These reflect into GPUSTAT bits 11 and 12 respectively. ps1-tests - // gpu/mask-bit verifies write-back of the 0x8000 pixel bit. 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 @@ -312,18 +268,13 @@ pub fn gpuUpdateCmd(g: *mut[] u32, vram: *mut[] u8) { return; } - // VRAM blits. 0x80 { gpuVramToVram(g, vram); return; } 0xA0 { gpuCpuToVram(g, vram); return; } 0xC0 { gpuVramToCpu(g, vram); return; } - - // Unrecognized - drop and return to idle to avoid a permanent stall. _ { g[19] = GP_RECV_CMD; } } } -// 0x02 fill rect in VRAM. The colour is taken straight from buf[0] -// (no dither), and the area is masked to a 16-pixel grid. pub fn gpuFillRect(g: *mut[] u32, vram: *mut[] u8) { if (g[19] == GP_RECV_CMD) { g[19] = GP_RECV_ARGS; @@ -348,7 +299,6 @@ pub fn gpuFillRect(g: *mut[] u32, vram: *mut[] u8) { g[19] = GP_RECV_CMD; } -// 0x80 VRAM-to-VRAM copy. pub fn gpuVramToVram(g: *mut[] u32, vram: *mut[] u8) { if (g[19] == GP_RECV_CMD) { g[19] = GP_RECV_ARGS; @@ -375,8 +325,6 @@ pub fn gpuVramToVram(g: *mut[] u32, vram: *mut[] u8) { g[19] = GP_RECV_CMD; } -// 0xA0 CPU->VRAM transfer. Header is 3 words (cmd, dest XY, size XY) -// then `size` 16-bit words come over GP0 packed two per u32. pub fn gpuCpuToVram(g: *mut[] u32, vram: *mut[] u8) { if (g[19] == GP_RECV_CMD) { g[19] = GP_RECV_ARGS; @@ -399,13 +347,6 @@ pub fn gpuCpuToVram(g: *mut[] u32, vram: *mut[] u8) { g[19] = GP_RECV_DATA; return; } - // RECV_DATA - one 32-bit word delivers two VRAM pixels. GP0(0xA0) - // writes the destination directly, bypassing the GP0(0xE6) mask - // bits - same as all the other draw commands here. This is done so - // Musashi's FMV (which leaves check-mask set when uploading decoded - // frames) doesn't silently drop writes to pixels that previously had - // bit 15 set. The strict spec says masks SHOULD apply, but no real - // game relies on it. const x0: u32 = (g[22] + g[28]) & 0x3FF; const y0: u32 = (g[23] + g[29]) & 0x1FF; vramWritePixel(vram, x0, y0, g[20] & 0xFFFF); @@ -424,8 +365,6 @@ pub fn gpuCpuToVram(g: *mut[] u32, vram: *mut[] u8) { } } -// 0xC0 VRAM->CPU transfer. Sets up the source rectangle; the data comes -// out via gpuRead32 / vramPackTwo over subsequent reads. pub fn gpuVramToCpu(g: *mut[] u32, vram: *mut[] u8) { if (g[19] == GP_RECV_CMD) { g[19] = GP_RECV_ARGS; @@ -445,21 +384,10 @@ pub fn gpuVramToCpu(g: *mut[] u32, vram: *mut[] u8) { g[41] = ys; g[42] = ((xs * ys) + 1) & 0xFFFFFFFE; g[43] = x0 | (y0 << 10); - // Track ACTUAL pixel count so vramPackTwo knows when the last word - // carries a single real pixel + a zero padding half (e.g. for 1×1 - // reads). g[42] is rounded UP to even; g[45] keeps the true count. g[45] = xs * ys; g[19] = GP_RECV_CMD; } -// polygon / line / rect command parsing -// -// We parse the variant flags (textured, gouraud, 4-vert) to compute the -// right argument count, latch all the args, then stub the rasterization -// out. This keeps the GP0 FIFO state machine in sync with whatever the -// BIOS / a game sends; without GTE we can't render most game polygons -// anyway, and the BIOS uses mostly rectangles for its splash screen. - pub fn gpuPolyCmd(g: *mut[] u32, vram: *mut[] u8) { if (g[19] == GP_RECV_CMD) { const flags: u32 = (g[0] >> 24) & 0xFF; @@ -482,18 +410,19 @@ pub fn gpuPolyCmd(g: *mut[] u32, vram: *mut[] u8) { gpuRasterPoly(g, vram); } -// Sign-extend an 11-bit PSX-screen coordinate (top 5 bits of a 16-bit -// xy word are garbage; the low 11 bits are the signed value). 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 gpuVertX(xy: u32) u32 { + return gpuSe11(xy & 0xFFFF); +} + +pub fn gpuVertY(xy: u32) u32 { + return gpuSe11((xy >> 16) & 0xFFFF); +} -// Cross-product of edges (b-a) and (c-a). Result is the signed twice- -// area of triangle abc. Use i64 to dodge overflow at ±2048 coords. 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); @@ -504,8 +433,6 @@ pub fn gpuEdge(ax: u32, ay: u32, bx: u32, by: u32, cx: u32, cy: u32) i64 { return (bx64 - ax64) * (cy64 - ay64) - (by64 - ay64) * (cx64 - ax64); } -// Sign-extend a u32 (interpreted as i32) to i64. Used so the edge -// function's intermediate products stay in range. pub fn sext32(v: u32) i64 { var w: i64 = v as i64; if ((v & 0x80000000) != 0) { @@ -514,13 +441,9 @@ pub fn sext32(v: u32) i64 { return w; } -// Top-left rule - when an edge result lands exactly on zero, only the -// pixel "owns" the edge if the edge is a top or left edge. This stops -// shared edges between adjacent triangles from drawing twice. pub fn gpuTopLeftRule(z: i64, ax: u32, ay: u32, bx: u32, by: u32) bool { if (z < 0) { return true; } if (z != 0) { return false; } - // Use raw signed compare via sext32 helper. const ays: i64 = sext32(ay); const bys: i64 = sext32(by); const axs: i64 = sext32(ax); @@ -530,7 +453,6 @@ pub fn gpuTopLeftRule(z: i64, ax: u32, ay: u32, bx: u32, by: u32) bool { return false; } -// Min / max of three i32 values (passed as u32, signed compare). pub fn gpuMin3(a: u32, b: u32, c: u32) u32 { var m: u32 = a; if (sext32(b) < sext32(m)) { m = b; } @@ -545,9 +467,6 @@ pub fn gpuMax3(a: u32, b: u32, c: u32) u32 { return m; } -// Polygon dispatcher - reads the command flags and triggers one or -// two triangles. Handles monochrome and textured variants; gouraud -// shading falls back to flat-shaded with the base colour. pub fn gpuRasterPoly(g: *mut[] u32, vram: *mut[] u8) { const flags: u32 = (g[0] >> 24) & 0xFF; const isQuad: bool = (flags & 0x08) != 0; @@ -576,9 +495,7 @@ pub fn gpuRasterPoly(g: *mut[] u32, vram: *mut[] u8) { var uu1: u32 = 0; var vv1: u32 = 0; var uu2: u32 = 0; var vv2: u32 = 0; var uu3: u32 = 0; var vv3: u32 = 0; - // Per-vertex shaded colours. For non-gouraud prims c0/c1/c2/c3 are - // identical to the base colour, which lets the rasterizer use the - // same code path for both. + var c0: u32 = g[0] & 0xFFFFFF; var c1: u32 = c0; var c2: u32 = c0; @@ -594,7 +511,6 @@ pub fn gpuRasterPoly(g: *mut[] u32, vram: *mut[] u8) { // 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) { - // texture + gouraud (cmd 0x34, 0x3C, 0x3E) v0x = gpuVertX(g[1]); v0y = gpuVertY(g[1]); uu0 = g[2] & 0xFF; vv0 = (g[2] >> 8) & 0xFF; const clut: u32 = (g[2] >> 16) & 0xFFFF; @@ -618,12 +534,6 @@ pub fn gpuRasterPoly(g: *mut[] u32, vram: *mut[] u8) { g[46] = (g[46] & 0xFFFFFE00) | (page & 0x1FF); g[70] = (page >> 11) & 1; g[46] = (g[46] & 0xFFFF7FFF) | ((g[71] & g[70]) << 15); - // The polygon's tpage byte also rewrites the GLOBAL texp_x/y/d - // (the derived fields textured rectangles - // read from). Without this, FMVs that draw the decoded frame - // as a texture-mapped polygon then composite it with a - // textured rect end up sampling the wrong VRAM region for the - // rect - texel 0 on every pixel -> black FMV. g[60] = tpx; g[61] = tpy; g[62] = depth; @@ -636,7 +546,6 @@ pub fn gpuRasterPoly(g: *mut[] u32, vram: *mut[] u8) { uu3 = g[11] & 0xFF; vv3 = (g[11] >> 8) & 0xFF; } } else if (isTex) { - // textured flat (cmd 0x24, 0x25, 0x26, 0x27, 0x2C, 0x2D, 0x2E, 0x2F) v0x = gpuVertX(g[1]); v0y = gpuVertY(g[1]); uu0 = g[2] & 0xFF; vv0 = (g[2] >> 8) & 0xFF; const clut: u32 = (g[2] >> 16) & 0xFFFF; @@ -694,18 +603,13 @@ pub fn gpuRasterPoly(g: *mut[] u32, vram: *mut[] u8) { const baseColor: u32 = g[0] & 0xFFFFFF; - // Textured polys read transparency mode from the texture page - // register's bits 5-6 (overrides GPUSTAT for this primitive); - // untextured / shaded polys read it from GPUSTAT bits 5-6. const isTransp: bool = (flags & 0x02) != 0; var transpMode: u32 = (g[46] >> 5) & 3; - // Use the texture-page transp mode for ALL textured polys, not - // just flat ones. The tpage word sits at buf[4] for flat-textured and - // at buf[5] for shaded-textured (the extra per-vertex colour words - // shift it), so pick the right word by isGour. if (isTex) { var page: u32 = (g[4] >> 16) & 0xFFFF; - if (isGour) { page = (g[5] >> 16) & 0xFFFF; } + if (isGour) { + page = (g[5] >> 16) & 0xFFFF; + } transpMode = (page >> 5) & 3; } @@ -723,10 +627,6 @@ pub fn gpuRasterPoly(g: *mut[] u32, vram: *mut[] u8) { } } -// Rasterize one triangle with optional texture sampling. Pixels inside -// the triangle's three half-planes (top-left rule applied) get a colour -// either from the base colour (untextured) or by sampling the texture -// page at the barycentric-interpolated uv. pub fn gpuRasterTri(g: *mut[] u32, vram: *mut[] u8, isTex: bool, isRaw: bool, isTransp: bool, transpMode: u32, isGour: bool, @@ -743,8 +643,6 @@ pub fn gpuRasterTri(g: *mut[] u32, vram: *mut[] u8, var by: u32 = by0 + oy; var cx: u32 = cx0 + ox; var cy: u32 = cy0 + oy; - // UV and per-vertex colour stay paired with their vertex when we - // flip winding. var bu2: u32 = bu; var bv2: u32 = bv; var cu2: u32 = cu; var cv2: u32 = cv; var bc2: u32 = bc; @@ -767,8 +665,9 @@ pub fn gpuRasterTri(g: *mut[] u32, vram: *mut[] u8, 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 - // (2048 / 1024). The earlier 1024/512 dropped valid large polygons. - if (sext32(xmax - xmin) > 2048 || sext32(ymax - ymin) > 1024) { return; } + 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]; @@ -790,10 +689,6 @@ pub fn gpuRasterTri(g: *mut[] u32, vram: *mut[] u8, !gpuTopLeftRule(z2, ax, ay, bx, by)) { var color: u32 = 0; if (isTex) { - // Barycentric interpolation for uv. Each zN is - // signed; combine with the corresponding uv, - // sum, divide by area. We use the absolute area - // since the edges may carry negative sign. 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) @@ -837,9 +732,6 @@ pub fn gpuRasterTri(g: *mut[] u32, vram: *mut[] u8, } color = gpuModulate(texel, modc); } - // Texel bit 15 -> blend per transparency mode. - // For untextured polys, transparency applies - // unconditionally when isTransp is set. if (isTransp && (texel & 0x8000) != 0) { const back: u32 = vramReadPixel(vram, x & 0x3FF, y & 0x1FF); @@ -899,8 +791,6 @@ pub fn gpuAbsI64(v: i64) i64 { // 4x4 ordered dither kernel. // Indexed by `dy * 4 + dx` where `dx = (x - xmin) & 3`, `dy = (y - ymin) & 3`. -// Values added to each interpolated channel before clamping reduce visible -// banding on gouraud gradients (the BIOS's diamond logo uses this heavily). pub fn gpuDitherKernel(idx: u32) i64 { match (idx) { 0 { return -4; } @@ -922,9 +812,6 @@ pub fn gpuDitherKernel(idx: u32) i64 { } } -// Saturate one shaded channel: divide the barycentric numerator by the -// triangle area, apply the dither offset, then clamp to [0, 0xFF]. -// Order matters: divide first, dither, clamp. pub fn gpuSatChan(num: i64, area: i64, dith: i64) u32 { if (area == 0) { return 0; } var quot: i64 = num / area; @@ -934,10 +821,6 @@ pub fn gpuSatChan(num: i64, area: i64, dith: i64) u32 { return quot as u32; } -// Divide an i64 dividend by an i64 divisor, clamping the (truncated-to- -// zero) result to 0..maxOut. Native LLVM signed divide via Jam's `/` -// operator - previously this was a 64-iteration shift-and-subtract loop -// (the inner-pixel hot path on every shaded / textured polygon). pub fn gpuDivClamp(num: i64, den: i64, maxOut: u32) u32 { if (den == 0) { return 0; } var q: i64 = num / den; @@ -952,24 +835,21 @@ pub fn gpuLineCmd(g: *mut[] u32, vram: *mut[] u8) { const isPoly: bool = (flags & 0x08) != 0; if (g[19] == GP_RECV_CMD) { g[19] = GP_RECV_ARGS; - // Poly-lines (bit 0x08) drain words until a vertex word masks to - // 0x50005000; single segments take 2 verts (3 if gouraud). g[17] - // is unused on the poly-line path - it ends on the terminator - // below, not on an arg count. - if (isPoly) { g[17] = 2; return; } + if (isPoly) { + g[17] = 2; return; + } var args: u32 = 2; - if (isGour) { args = args + 1; } + if (isGour) { + args = args + 1; + } g[17] = args; return; } - // RECV_ARGS. 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 - with no draw the words are never - // read back, so overwriting one slot is observably the same while - // keeping jam's state intact. + // overflow the 16-word buffer. const last: u32 = g[g[16] - 1]; if ((last & 0xF000F000) == 0x50005000) { g[19] = GP_RECV_CMD; @@ -997,10 +877,7 @@ pub fn gpuLineCmd(g: *mut[] u32, vram: *mut[] u8) { g[19] = GP_RECV_CMD; } -// Bresenham line rasterizer - split on whether dx or dy dominates -// (i.e. shallow vs. steep slope) and on direction, so the inner loop -// always increments by 1 in the dominant axis and uses Bresenham's -// error term to step in the minor axis. +// 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); @@ -1089,7 +966,7 @@ pub fn gpuRectCmd(g: *mut[] u32, vram: *mut[] u8) { const flags: u32 = (g[0] >> 24) & 0xFF; const sizeBits: u32 = (flags >> 3) & 3; const isTex: bool = (flags & 0x04) != 0; - var args: u32 = 1; // x/y + var args: u32 = 1; if (sizeBits == 0) { args = args + 1; } if (isTex) { args = args + 1; } g[17] = args; @@ -1102,7 +979,7 @@ pub fn gpuRectCmd(g: *mut[] u32, vram: *mut[] u8) { 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; // GPUSTAT bits 5-6 + const transpMode: u32 = (g[46] >> 5) & 3; var bufIdx: u32 = 1; const xy: u32 = g[bufIdx]; bufIdx = bufIdx + 1; @@ -1124,15 +1001,9 @@ pub fn gpuRectCmd(g: *mut[] u32, vram: *mut[] u8) { } _ {} } - // Order matters: add the drawing offset first, *then* sign-extend - // the 11-bit screen-space position. SE-after-offset is what the - // hardware does - anything that overflows 11 bits wraps. var x: u32 = gpuSe11((xy & 0xFFFF) + g[52]); var y: u32 = gpuSe11(((xy >> 16) & 0xFFFF) + g[53]); - // Drawing-area bounds. Pixels outside the (draw_x1..draw_x2, - // draw_y1..draw_y2) box are skipped: the iteration limits are - // clamped against the drawing area. const dx1: i64 = sext32(g[48]); const dy1: i64 = sext32(g[49]); const dx2: i64 = sext32(g[50]); @@ -1164,9 +1035,6 @@ pub fn gpuRectCmd(g: *mut[] u32, vram: *mut[] u8) { iy = iy + 1; } } else { - // Textured rect: sample each pixel from VRAM via the current - // texture page + CLUT setup. uvword low 16 bits hold u, v; - // high 16 hold the CLUT location. const uu0: u32 = uvword & 0xFF; const vv0: u32 = (uvword >> 8) & 0xFF; const clut: u32 = (uvword >> 16) & 0xFFFF; @@ -1189,9 +1057,6 @@ pub fn gpuRectCmd(g: *mut[] u32, vram: *mut[] u8) { if (!isRaw) { color = gpuModulate(texel, g[0] & 0xFFFFFF); } - // When the rect's transparency flag is set, texel - // bit 15 selects whether THIS pixel blends. When - // the flag isn't set, no blending. var thisTransp: bool = false; if (isTransp) { thisTransp = (texel & 0x8000) != 0; @@ -1213,23 +1078,14 @@ pub fn gpuRectCmd(g: *mut[] u32, vram: *mut[] u8) { g[19] = GP_RECV_CMD; } -// Apply the GP0(0xE2) texture window to a texel coordinate, then wrap to -// the 256x256 page: -// t = (t & ~mask) | (offset & mask); t &= 0xff -// The window regs are pre-shifted to pixel units in g[54..57] -// (mask_x, mask_y, off_x, off_y). With no window set (mask 0) this is a -// pass-through, so it is always safe to apply. 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; } -// Fetch one 16-bit BGR555 texel from VRAM, decoding CLUT-indexed -// formats. `depth` is the texture page colour mode: 0 = 4bpp, 1 = 8bpp, -// 2/3 = 15bpp direct. (tpx, tpy) is the texture page top-left in VRAM -// pixels; (clutx, cluty) is the CLUT row's top-left. pub fn gpuFetchTexel(vram: *mut[] u8, tx: u32, ty: u32, tpx: u32, tpy: u32, clutx: u32, cluty: u32, depth: u32) u32 { @@ -1249,10 +1105,6 @@ pub fn gpuFetchTexel(vram: *mut[] u8, tx: u32, ty: u32, return vramReadPixel(vram, tpx + tx, tpy + ty); } -// Modulate a BGR555 texel by a 24-bit BGR colour. PSX texture blending -// is multiply-then-double: out = clamp(tex * mod / 128). At mod = 0x80 -// (the "neutral" value the BIOS uses for plain texture rendering) the -// texel passes through unchanged. pub fn gpuModulate(texel: u32, mod: u32) u32 { const tr: u32 = (texel & 0x1F) << 3; const tg: u32 = ((texel >> 5) & 0x1F) << 3; @@ -1260,9 +1112,6 @@ pub fn gpuModulate(texel: u32, mod: u32) u32 { const mr: u32 = mod & 0xFF; const mg: u32 = (mod >> 8) & 0xFF; const mb: u32 = (mod >> 16) & 0xFF; - // Modulation rounds to nearest: round((tex * mod) / 128). +0x40 - // before >>7 gives round-half-up, which is correct for non-negative - // channels (tex, mod are unsigned here). var cr: u32 = ((tr * mr) + 0x40) >> 7; var cg: u32 = ((tg * mg) + 0x40) >> 7; var cb: u32 = ((tb * mb) + 0x40) >> 7; @@ -1272,16 +1121,10 @@ pub fn gpuModulate(texel: u32, mod: u32) u32 { return gpuToBgr555(cr | (cg << 8) | (cb << 16)); } -// Semi-transparency blend. -// `fore` is a BGR555 foreground colour, `back` the existing VRAM pixel; -// `mode` is the 2-bit semi-transparency mode out of GPUSTAT bits 5-6 -// (or for textured polys, out of the texture-page register): // 0 B/2 + F/2 // 1 B + F // 2 B - F // 3 B + F/4 -// We unpack each channel to 8 bits, blend with saturating arithmetic, -// then re-pack to BGR555. pub fn gpuBlend(fore: u32, back: u32, mode: u32) u32 { const fr: u32 = (fore & 0x1F) << 3; const fg: u32 = ((fore >> 5) & 0x1F) << 3; @@ -1316,39 +1159,14 @@ pub fn gpuBlend(fore: u32, back: u32, mode: u32) u32 { return gpuToBgr555(cr | (cg << 8) | (cb << 16)); } -// frame pacing -// -// Each call advances the GPU's scanline cycle counter. When we cross a -// horizontal-draw / hblank boundary we tick `line`; at scanline 240 we -// raise the VBLANK IRQ. - -// Removed: gpuUpdate() was a dead alternative scanline counter that -// never got called from main.jam - runOneFrame handles scanline pacing -// directly. The function had two competing accumulators (one fractional -// 11/7 path, one ×2 path), neither correct. Keeping the constants in -// case a future per-cycle GPU tick is added. - -// struct wrapper -// -// `Gpu` owns the 80-u32 state buffer inline (vs the old heap alloc) -// and exposes the public surface as methods. The internal command- -// state machine plus the rectangle/CPU↔VRAM blit paths are too -// indexed-access-heavy to refactor field-by-field without high -// regression risk - same trade-off as the CDROM wrap. Callers get -// `bus.gpu.method(...)` ergonomics. pub const Gpu = struct { - // 80 u32 entries = 320 bytes. Spelled as a literal because Jam's - // array type currently requires an integer literal in size - // position (no const-folding for `[GPU_STATE_WORDS]u32` yet). buf: [80]u32, pub fn init() Self { var s: Self = Self { buf: [0; 80] }; s.buf[19] = GP_RECV_CMD; - // Initial GPUSTAT: only bit 23 set (display disabled). - // Reads OR 0x1C000000 on the fly. s.buf[46] = 0x00800000; - s.buf[47] = 1; // default display_mode + s.buf[47] = 1; return s; } @@ -1380,16 +1198,10 @@ pub const Gpu = struct { gpuWrite32(self.buf.asMutPtr(), vram, off, val); } - // DMA channel 2 GPU path (linked-list + request modes) calls - // these per word from dma.jam. pub fn gp0(self: Self, vram: *mut[] u8, val: u32) { gpuGp0(self.buf.asMutPtr(), vram, val); } - // Internal slot access - used by main.jam's frame loop to peek - // display-mode / disp_x / etc. fields directly. Bypasses the - // method API for the small set of read-only readouts the host - // pump needs each frame. pub fn bufPtr(self: Self) *mut[] u32 { return self.buf.asMutPtr(); } diff --git a/gte.jam b/gte.jam index 1bb0b87..13ca91b 100644 --- a/gte.jam +++ b/gte.jam @@ -1,19 +1,3 @@ -// COP2 / GTE - Geometry Transformation Engine. -// -// PSX GTE - fixed-point geometry math with saturation flags. This -// module covers the register file (32 data + -// 32 control regs), the register-move instructions (MFC2 / MTC2 / -// CFC2 / CTC2 / LWC2 / SWC2), and the math ops dispatched by gteExec. -// -// Implemented ops: RTPS, RTPT (3xRTPS), NCLIP, OP, MVMVA, NCDS, NCDT, -// SQR, AVSZ3, AVSZ4, DPCS, INTPL, CDP, NCCS, CC, NCS, NCT, DCPL, DPCT, -// GPF, GPL, NCCT - all 22 GTE ops. -// -// Saturation FLAG bookkeeping is hardware-faithful: every clamp (IR/IR0/SXY/ -// SZ3/RGB/MAC0) ORs its FLAG bit, the 44-bit MAC overflow is truncated -// per-term (gteCheckMac) and nested between additive terms, and bit 31 is -// synthesized from the error summary on read. -// // Data register layout (32 × u32): // 0 V0 (xy packed, low half of vertex 0) // 1 V0Z (z, low 16 bits) @@ -47,7 +31,7 @@ const { Vec } = import("std/collections"); -const GTE_REGS: u32 = 144; // 0..63 = data/control regs, 64..143 = debug counters +const GTE_REGS: u32 = 144; const D_V0: u32 = 0; const D_V0Z: u32 = 1; const D_SXY0: u32 = 12; @@ -62,10 +46,7 @@ const C_OFX: u32 = 24; const C_OFY: u32 = 25; const C_FLAG: u32 = 31; -// Unsigned Newton-Raphson reciprocal table for the GTE perspective -// divide. 257 entries - the hardware uses this exact LUT, so a naive -// integer divide diverges from hardware on the projected SX/SY by up to -// a few units. +// Unsigned Newton-Raphson reciprocal table for the GTE perspective divide. const GTE_UNR_TABLE: [257]u8 = [ 0xff, 0xfd, 0xfb, 0xf9, 0xf7, 0xf5, 0xf3, 0xf1, 0xef, 0xee, 0xec, 0xea, 0xe8, 0xe6, 0xe4, 0xe3, @@ -102,42 +83,30 @@ const GTE_UNR_TABLE: [257]u8 = [ 0x00, ]; -// Owned GTE-register buffer (zero-initialised). Auto-drops with the Bus. -pub fn createGte() Vec(u32) { return Vec(u32).filled(0, GTE_REGS); } +pub fn createGte() Vec(u32) { + return Vec(u32).filled(0, GTE_REGS); +} -// Data and control share the same storage buffer: data at index 0..31, -// control at index 32..63. -// -// Several COP2 data registers have side effects on read/write: -// // 15 SXYP read -> mirror of SXY2; write -> push SXY FIFO // 28 IRGB write -> unpack 15-bit RGB into IR1..IR3; read -> repack // 29 ORGB read -> repack IR1..IR3 to 15-bit; write -> ignored (RO) // 30 LZCS write -> also compute LZCR (leading sign-bit count) // 31 LZCR read-only -// Sign-extend a 16-bit value held in a u32 to full 32 bits. Several GTE -// registers are stored as int16_t, so reading them back via MFC2/CFC2 -// sign-extends the low half. pub fn gteSext16(v: u32) u32 { - if ((v & 0x8000) != 0) { return v | 0xFFFF0000; } + if ((v & 0x8000) != 0) { + return v | 0xFFFF0000; + } return v & 0x0000FFFF; } pub fn gteDataRead(g: *mut[] u32, idx: u32) u32 { const i: u32 = idx & 0x1F; - if (i == 15) { return g[14]; } // SXYP mirrors SXY2 - // 16-bit signed data registers: vZ (1/3/5) and IR0..IR3 (8..11) are - // int16_t, so MFC2 sign-extends the low half. + if (i == 15) { return g[14]; } if (i == 1 || i == 3 || i == 5 || i == 8 || i == 9 || i == 10 || i == 11) { return gteSext16(g[i]); } if (i == 28 || i == 29) { - // Re-pack IR1/IR2/IR3 (>> 7, clamped 0..0x1F) into 15-bit RGB. - // IR is stored zero-extended in the low 16 bits, so sign-extend - // first (s16ToI64) - otherwise a negative IR reads as a large - // positive value and clamps to 0x1F instead of 0 (DuckStation - // gte.cpp:343 gives 0 for IR<0). var r: i64 = s16ToI64(g[9]) >> 7; var grn: i64 = s16ToI64(g[10]) >> 7; var b: i64 = s16ToI64(g[11]) >> 7; @@ -158,15 +127,17 @@ pub fn gteDataWrite(g: *mut[] u32, idx: u32, val: u32) { // SXYP - store the value, then push SXY FIFO so the latest // entry slides into SXY2 and older entries shift down. Used by // the BIOS to pre-load screen-space vertices without RTPS. - g[15] = val; // SXYP raw - g[12] = g[13]; // SXY0 ← SXY1 - g[13] = g[14]; // SXY1 ← SXY2 - g[14] = val; // SXY2 ← new + // SXYP raw + g[15] = val; + // SXY0 ← SXY1 + g[12] = g[13]; + // SXY1 ← SXY2 + g[13] = g[14]; + // SXY2 ← new + g[14] = val; return; } if (i == 28) { - // IRGB - unpack 5-bit channels into IR1/IR2/IR3 (<< 7 to map - // 0..0x1F -> 0..0xF80). const packed: u32 = val & 0x7FFF; g[28] = packed; g[9] = (packed & 0x1F) << 7; @@ -179,8 +150,6 @@ pub fn gteDataWrite(g: *mut[] u32, idx: u32, val: u32) { return; } if (i == 30) { - // LZCS - write computes LZCR (count of leading sign bits, or - // leading zeros if the value is non-negative). g[30] = val; g[31] = gteCountLeadingSign(val); return; @@ -188,10 +157,6 @@ pub fn gteDataWrite(g: *mut[] u32, idx: u32, val: u32) { g[i] = val; } -// Count the number of leading bits that match bit 31 of `v`. Matches -// the LZCS/LZCR semantic: if v has bit 31 = 0, this is the count of -// leading zeros; if bit 31 = 1, it's the count of leading ones. Result -// ranges from 0 to 32 (32 only for v == 0 or v == 0xFFFFFFFF). pub fn gteCountLeadingSign(v: u32) u32 { if (v == 0) { return 32; } if (v == 0xFFFFFFFF) { return 32; } @@ -208,16 +173,12 @@ pub fn gteCountLeadingSign(v: u32) u32 { pub fn gteCtrlRead(g: *mut[] u32, idx: u32) u32 { const i: u32 = idx & 0x1F; if (i == C_FLAG) { - // Synthesize "any-error" bit 31 from the overflow/saturation - // bit-mask 0x7F87E000, mask the low garbage. - // Games CFC2 r63 and branch on bit 31; without this synthesis - // they always see "no error" even when MAC/IR saturated. const v: u32 = g[32 + C_FLAG] & 0x7FFFF000; - if ((v & 0x7F87E000) != 0) { return v | 0x80000000; } + if ((v & 0x7F87E000) != 0) { + return v | 0x80000000; + } return v; } - // 16-bit signed control registers, sign-extended on read: the three - // matrix m33 entries // (rt/l/lr -> idx 4/12/20), H (26), DQA (27), ZSF3 (29), ZSF4 (30). if (i == 4 || i == 12 || i == 20 || i == 26 || i == 27 || i == 29 || i == 30) { @@ -229,102 +190,92 @@ pub fn gteCtrlRead(g: *mut[] u32, idx: u32) u32 { pub fn gteCtrlWrite(g: *mut[] u32, idx: u32, val: u32) { const i: u32 = idx & 0x1F; if (i == C_FLAG) { - // Bit 31 is read-only "any-error" - strip on write so it can be - // resynthesized in gteCtrlRead. g[32 + C_FLAG] = val & 0x7FFFF000; return; } g[32 + i] = val; } -// Dispatch on the bottom 6 bits of the GTE math opcode. The full table -// covers 22 ops (RTPS=0x01, NCLIP=0x06, ..., NCCT=0x3F) - all 22 are -// implemented below. FLAG is cleared at the top of every op. pub fn gteExec(g: *mut[] u32, opc: u32) { - g[32 + C_FLAG] = 0; // clear FLAG every instruction + // clear + g[32 + C_FLAG] = 0; const op: u32 = opc & 0x3F; - // Per-op histogram of GTE math instructions. Stored at control - // register 31 (LZCR) + bits - wait, use g spare slot g[70+]. - g[70] = g[70] + 1; // total GTE ops + g[70] = g[70] + 1; if (op < 0x40) { g[80 + op] = g[80 + op] + 1; } - // Decode sf (bit 19 -> 12 or 0) and lm (bit 10) once per dispatch - // so RTPS/RTPT/MVMVA all see the same flags. const sf: u32 = ((opc >> 19) & 1) * 12; const lm: u32 = (opc >> 10) & 1; - if (op == 0x01) { gteRtps(g, 0, sf, lm, 1); } // RTPS - if (op == 0x06) { gteNclip(g); } // NCLIP - if (op == 0x0C) { gteOp(g, sf, lm); } // OP - if (op == 0x12) { gteMvmva(g, opc); } // MVMVA - if (op == 0x13) { gteNcds(g, 0, sf, lm); } // NCDS - if (op == 0x16) { // NCDT - 3 successive NCDS - gteNcds(g, 0, sf, lm); - gteNcds(g, 1, sf, lm); - gteNcds(g, 2, sf, lm); - } - if (op == 0x28) { gteSqr(g, sf, lm); } // SQR - if (op == 0x2D) { gteAvsz(g, 3); } // AVSZ3 - if (op == 0x2E) { gteAvsz(g, 4); } // AVSZ4 - if (op == 0x30) { // RTPT - 3 successive RTPS - gteRtps(g, 0, sf, lm, 0); - gteRtps(g, 1, sf, lm, 0); - gteRtps(g, 2, sf, lm, 1); // DQ tail only on last vertex - } - if (op == 0x10) { gteDpcs(g, sf, lm); } // DPCS - if (op == 0x11) { gteIntpl(g, sf, lm); } // INTPL - if (op == 0x14) { gteCdp(g, sf, lm); } // CDP - if (op == 0x1B) { gteNccs(g, 0, sf, lm); } // NCCS - if (op == 0x1C) { gteCc(g, sf, lm); } // CC - if (op == 0x1E) { gteNcs(g, 0, sf, lm); } // NCS - if (op == 0x20) { // NCT - 3× NCS - gteNcs(g, 0, sf, lm); - gteNcs(g, 1, sf, lm); - gteNcs(g, 2, sf, lm); - } - if (op == 0x29) { gteDcpl(g, sf, lm); } // DCPL - if (op == 0x2A) { gteDpct(g, sf, lm); } // DPCT - if (op == 0x3D) { gteGpf(g, sf, lm); } // GPF - if (op == 0x3E) { gteGpl(g, sf, lm); } // GPL - if (op == 0x3F) { // NCCT - 3× NCCS - gteNccs(g, 0, sf, lm); - gteNccs(g, 1, sf, lm); - gteNccs(g, 2, sf, lm); + match (op) { + 0x01 { gteRtps(g, 0, sf, lm, 1); } // RTPS + 0x06 { gteNclip(g); } // NCLIP + 0x0C { gteOp(g, sf, lm); } // OP + 0x12 { gteMvmva(g, opc); } // MVMVA + 0x13 { gteNcds(g, 0, sf, lm); } // NCDS + 0x16 { // NCDT - 3 successive NCDS + gteNcds(g, 0, sf, lm); + gteNcds(g, 1, sf, lm); + gteNcds(g, 2, sf, lm); + } + 0x28 { gteSqr(g, sf, lm); } // SQR + 0x2D { gteAvsz(g, 3); } // AVSZ3 + 0x2E { gteAvsz(g, 4); } // AVSZ4 + 0x30 { // RTPT - 3 successive RTPS + gteRtps(g, 0, sf, lm, 0); + gteRtps(g, 1, sf, lm, 0); + gteRtps(g, 2, sf, lm, 1); // DQ tail only on last vertex + } + 0x10 { gteDpcs(g, sf, lm); } // DPCS + 0x11 { gteIntpl(g, sf, lm); } // INTPL + 0x14 { gteCdp(g, sf, lm); } // CDP + 0x1B { gteNccs(g, 0, sf, lm); } // NCCS + 0x1C { gteCc(g, sf, lm); } // CC + 0x1E { gteNcs(g, 0, sf, lm); } // NCS + 0x20 { // NCT - 3× NCS + gteNcs(g, 0, sf, lm); + gteNcs(g, 1, sf, lm); + gteNcs(g, 2, sf, lm); + } + 0x29 { gteDcpl(g, sf, lm); } // DCPL + 0x2A { gteDpct(g, sf, lm); } // DPCT + 0x3D { gteGpf(g, sf, lm); } // GPF + 0x3E { gteGpl(g, sf, lm); } // GPL + 0x3F { // NCCT - 3× NCCS + gteNccs(g, 0, sf, lm); + gteNccs(g, 1, sf, lm); + gteNccs(g, 2, sf, lm); + } + _ {} } } -// CPU-cycle cost of a COP2 (GTE) math op, keyed by the low 6 bits of the -// opcode. These are the hardware per-op cycle counts. The CPU dispatcher -// adds this - instead of the flat 2-cycle base - when it runs a GTE math -// op, so device timing (DMA, CDROM, timers) advances at the correct rate -// through GTE-heavy code. Unknown sub-ops fall back to the 2-cycle base. pub fn gteOpCycles(opc: u32) u32 { const op: u32 = opc & 0x3F; - if (op == 0x01) { return 15; } // RTPS - if (op == 0x06) { return 8; } // NCLIP - if (op == 0x0C) { return 6; } // OP - if (op == 0x10) { return 8; } // DPCS - if (op == 0x11) { return 8; } // INTPL - if (op == 0x12) { return 8; } // MVMVA - if (op == 0x13) { return 19; } // NCDS - if (op == 0x14) { return 13; } // CDP - if (op == 0x16) { return 44; } // NCDT - if (op == 0x1B) { return 17; } // NCCS - if (op == 0x1C) { return 11; } // CC - if (op == 0x1E) { return 14; } // NCS - if (op == 0x20) { return 30; } // NCT - if (op == 0x28) { return 5; } // SQR - if (op == 0x29) { return 8; } // DCPL - if (op == 0x2A) { return 17; } // DPCT - if (op == 0x2D) { return 5; } // AVSZ3 - if (op == 0x2E) { return 6; } // AVSZ4 - if (op == 0x30) { return 23; } // RTPT - if (op == 0x3D) { return 5; } // GPF - if (op == 0x3E) { return 5; } // GPL - if (op == 0x3F) { return 39; } // NCCT - return 2; + match (op) { + 0x01 { return 15; } // RTPS + 0x06 { return 8; } // NCLIP + 0x0C { return 6; } // OP + 0x10 { return 8; } // DPCS + 0x11 { return 8; } // INTPL + 0x12 { return 8; } // MVMVA + 0x13 { return 19; } // NCDS + 0x14 { return 13; } // CDP + 0x16 { return 44; } // NCDT + 0x1B { return 17; } // NCCS + 0x1C { return 11; } // CC + 0x1E { return 14; } // NCS + 0x20 { return 30; } // NCT + 0x28 { return 5; } // SQR + 0x29 { return 8; } // DCPL + 0x2A { return 17; } // DPCT + 0x2D { return 5; } // AVSZ3 + 0x2E { return 6; } // AVSZ4 + 0x30 { return 23; } // RTPT + 0x3D { return 5; } // GPF + 0x3E { return 5; } // GPL + 0x3F { return 39; } // NCCT + _ { return 2; } // unknown sub-op + } } -// Push (MAC1>>4, MAC2>>4, MAC3>>4, CODE) onto the RGB FIFO - used by -// every colour-producing GTE op. RGB0 ← RGB1, RGB1 ← RGB2, RGB2 ← new. pub fn gtePushRgb(g: *mut[] u32, m1: i64, m2: i64, m3: i64) { g[20] = g[21]; g[21] = g[22]; @@ -336,8 +287,6 @@ pub fn gtePushRgb(g: *mut[] u32, m1: i64, m2: i64, m3: i64) { } // Light-matrix transform of vertex vidx into (IR1,IR2,IR3) and (MAC1,MAC2,MAC3). -// Shared by NCS, NCDS, NCCS - they all start with the same L*V -// transformation. Stores IR into g[9..11] and MAC into g[D_MAC*]. pub fn gteApplyLight(g: *mut[] u32, vidx: u32, sf: u32, lm: u32) { const vSlot: u32 = vidx * 2; const vxy: u32 = g[vSlot]; @@ -383,10 +332,6 @@ pub fn gteApplyLightColor(g: *mut[] u32, sf: u32, lm: u32) { const rbk: i64 = s32ToI64(g[32 + 13]); const gbk: i64 = s32ToI64(g[32 + 14]); const bbk: i64 = s32ToI64(g[32 + 15]); - // gteCheckMac nests between the BK<<12 term and each LC*IR term so an - // intermediate exceeding 44 bits wraps before the next add. The L*V - // first stage is a single gteClampMac (see gteApplyLight) and is - // intentionally left unnested, matching hardware. const a1a: i64 = gteCheckMac(g, 1, (rbk << 12) + lr1 * ir1); const a1b: i64 = gteCheckMac(g, 1, a1a + lr2 * ir2); const m1: i64 = gteClampMac(g, 1, a1b + lr3 * ir3, sf); @@ -673,14 +618,6 @@ pub fn s32ToI64(v: u32) i64 { return w; } -// FLAG-aware clamps. -// Each ORs the matching FLAG bit into g[32+C_FLAG] on saturation/overflow -// and otherwise returns exactly what the plain sat* helpers return, so -// MAC/IR/SXY/RGB *values* are unchanged - only FLAG gets populated. `i` -// is the component (1/2/3) selecting the per-channel bit. gteExec clears -// FLAG at the top of each op; gteCtrlRead synthesizes the bit-31 summary. - -// IR1..IR3 saturation -> bit 24/23/22. pub fn gteClampIr(g: *mut[] u32, i: u32, v: i64, lm: u32) i64 { var lo: i64 = -32768; if (lm != 0) { lo = 0; } @@ -725,8 +662,7 @@ pub fn gteClampRgb(g: *mut[] u32, i: u32, v: i64) u32 { return (v as u64 & 0xFF) as u32; } -// MAC0 overflow -> bit 15 (neg) / 16 (pos); value is NOT clamped, only -// flagged. +// MAC0 overflow -> bit 15 (neg) / 16 (pos); value is NOT clamped, only flagged. pub fn gteClampMac0(g: *mut[] u32, v: i64) i64 { const lim: i64 = (1 as i64) << 31; if (v < (0 - lim)) { g[32 + C_FLAG] = g[32 + C_FLAG] | 0x8000; } @@ -833,8 +769,6 @@ pub fn gteRtps(g: *mut[] u32, vidx: u32, sf: u32, lm: u32, last: u32) { const vy: i64 = s16ToI64((vxy >> 16) & 0xFFFF); const vzS: i64 = s16ToI64(vz & 0xFFFF); - // Rotation matrix elements (packed two-per-u32 in the control regs). - // Layout: // C[0] low = RT11, high = RT12 // C[1] low = RT13, high = RT21 // C[2] low = RT22, high = RT23 @@ -987,9 +921,6 @@ pub fn gteOp(g: *mut[] u32, sf: u32, lm: u32) { gteIrTriple(g, mac1, mac2, mac3, lm); } -// NCDS - Normal Color Depth Single. Computes per-vertex lit color with -// far-color (fog) depth interpolation, using the light matrix to -// transform the input vertex normal. The pipeline: // 1. L * V -> MAC, IR (vertex normal in light space) // 2. BK + LC*IR -> MAC, IR (background + reflected light) // 3. ir' = clamp((FC<<12) - (C<<4)*IR), with lm=0 @@ -1007,12 +938,6 @@ pub fn gteOp(g: *mut[] u32, sf: u32, lm: u32) { // DR[6] byte 0 = R, byte 1 = G, byte 2 = B, byte 3 = CODE (RGBC) // DR[8] = IR0 (depth-queue factor) // DR[20..22] = RGB FIFO entries 0/1/2 -// -// Without this, the BIOS PS-logo polygons all receive colour (0,0,0) -// and render black on a black background - completely invisible. -// sf is 0 or 12 - gteClampMac shifts the saturated MAC by sf at every -// stage; we apply the same shift inline so IR values land in the right -// magnitude before clamping. pub fn gteNcds(g: *mut[] u32, vidx: u32, sf: u32, lm: u32) { const vSlot: u32 = vidx * 2; const vxy: u32 = g[vSlot]; @@ -1032,10 +957,6 @@ pub fn gteNcds(g: *mut[] u32, vidx: u32, sf: u32, lm: u32) { const ll32: i64 = s16ToI64((g[32 + 11] >> 16) & 0xFFFF); const ll33: i64 = s16ToI64(g[32 + 12] & 0xFFFF); - // Stage 1: L * V -> MAC, IR. gteClampMac applies `>> sf`, so for - // sf=12 (the BIOS default) we shift down by 12 before clamping into - // the IR range; otherwise IR saturates at 0x7FFF and the downstream - // stages clip every channel to white. var m1a: i64 = ll11 * vx + ll12 * vy + ll13 * vz; var m2a: i64 = ll21 * vx + ll22 * vy + ll23 * vz; var m3a: i64 = ll31 * vx + ll32 * vy + ll33 * vz; @@ -1044,7 +965,6 @@ pub fn gteNcds(g: *mut[] u32, vidx: u32, sf: u32, lm: u32) { const ir2a: i64 = gteClampIr(g, 2, m2a, lm); const ir3a: i64 = gteClampIr(g, 3, m3a, lm); - // Stage 2: background-colour + light-colour matrix * (IR from stage 1). const lr1: i64 = s16ToI64(g[32 + 16] & 0xFFFF); const lr2: i64 = s16ToI64((g[32 + 16] >> 16) & 0xFFFF); const lr3: i64 = s16ToI64(g[32 + 17] & 0xFFFF); @@ -1058,8 +978,6 @@ pub fn gteNcds(g: *mut[] u32, vidx: u32, sf: u32, lm: u32) { const gbk: i64 = s32ToI64(g[32 + 14]); const bbk: i64 = s32ToI64(g[32 + 15]); - // BK<<12 + LC*IR with the same nested 44-bit truncation as in the - // L*V stage. const b1a: i64 = gteCheckMac(g, 1, (rbk << 12) + lr1 * ir1a); const b1b: i64 = gteCheckMac(g, 1, b1a + lr2 * ir2a); const m1b: i64 = gteClampMac(g, 1, b1b + lr3 * ir3a, sf); @@ -1073,14 +991,11 @@ pub fn gteNcds(g: *mut[] u32, vidx: u32, sf: u32, lm: u32) { const ir2b: i64 = gteClampIr(g, 2, m2b, lm); const ir3b: i64 = gteClampIr(g, 3, m3b, lm); - // Stage 3: vertex colour from RGBC register (DR[6]). const rgbc: u32 = g[6]; const rc: i64 = (rgbc & 0xFF) as i64; const gc: i64 = ((rgbc >> 8) & 0xFF) as i64; const bc: i64 = ((rgbc >> 16) & 0xFF) as i64; - // Stage 4: far-colour interpolation ir' = clamp((FC<<12) - (C<<4)*IR). - // This stage is shifted by `sf` too, via the same gteClampMac. const rfc: i64 = s32ToI64(g[32 + 21]); const gfc: i64 = s32ToI64(g[32 + 22]); const bfc: i64 = s32ToI64(g[32 + 23]); @@ -1092,7 +1007,6 @@ pub fn gteNcds(g: *mut[] u32, vidx: u32, sf: u32, lm: u32) { const ir2f: i64 = gteClampIr(g, 2, fp2, 0); const ir3f: i64 = gteClampIr(g, 3, fp3, 0); - // Stage 5: final colour = (C<<4)*IR + IR0 * ir'. const ir0: i64 = s16ToI64(g[8]); var m1c: i64 = (rc << 4) * ir1b + ir0 * ir1f; var m2c: i64 = (gc << 4) * ir2b + ir0 * ir2f; @@ -1104,9 +1018,6 @@ pub fn gteNcds(g: *mut[] u32, vidx: u32, sf: u32, lm: u32) { g[D_MAC3] = (m3c as u64 & 0xFFFFFFFF) as u32; gteIrTriple(g, m1c, m2c, m3c, lm); - // Stage 6: RGB FIFO push. Use gtePushRgb (FLAG-aware gteClampRgb) so the - // colour-saturation FLAG bits 19/20/21 are set as on hardware and in - // DuckStation (gte.cpp:209-224); the old inline clampU8 skipped them. gtePushRgb(g, m1c, m2c, m3c); } @@ -1125,14 +1036,6 @@ pub fn gteSqr(g: *mut[] u32, sf: u32, lm: u32) { gteIrTriple(g, mac1, mac2, mac3, lm); } -// MVMVA - Multiply-Vector-by-Matrix and add-Vector. Opcode bits select -// which matrix (mx: 0=rotation, 1=light, 2=lightColour), which vector -// (v: 0..2 = V0..V2, 3 = IR vec), and which translation vector -// (cv: 0=TR, 1=BK, 2=FC, 3=zero). Bit 19 sets sf (shift fraction = 12 -// or 0). Bit 10 sets lm (limit IR to 0..0x7FFF if 1). -// -// The CV=FC case is the hardware bug case - most BIOS calls use CV=TR. -// Result: MAC = (CV << 12) + MX * V; IR = clamp(MAC >> sf). pub fn gteMvmva(g: *mut[] u32, opc: u32) { const sf: u32 = ((opc >> 19) & 1) * 12; const lm: u32 = (opc >> 10) & 1; @@ -1140,18 +1043,14 @@ pub fn gteMvmva(g: *mut[] u32, opc: u32) { const vSel: u32 = (opc >> 15) & 3; const mxSel: u32 = (opc >> 17) & 3; - // Pick the matrix. mxSel 0/1/2 = rotation / light / light-colour at - // control regs 0..4 / 8..12 / 16..20. mxSel 3 is the hardware's - // "garbage matrix": a junk mix of RC/IR0/RT13/RT22 that the hardware - // actually produces - reproduce it exactly rather than approximate. var m11: i64 = 0; var m12: i64 = 0; var m13: i64 = 0; var m21: i64 = 0; var m22: i64 = 0; var m23: i64 = 0; var m31: i64 = 0; var m32: i64 = 0; var m33: i64 = 0; if (mxSel == 3) { - const rcg: i64 = (g[6] & 0xFF) as i64; // R of RGBC - const ir0g: i64 = s16ToI64(g[8]); // IR0 - const rt13: i64 = s16ToI64(g[32 + 1] & 0xFFFF); // RT13 - const rt22: i64 = s16ToI64(g[32 + 2] & 0xFFFF); // RT22 + const rcg: i64 = (g[6] & 0xFF) as i64; + const ir0g: i64 = s16ToI64(g[8]); + const rt13: i64 = s16ToI64(g[32 + 1] & 0xFFFF); + const rt22: i64 = s16ToI64(g[32 + 2] & 0xFFFF); m11 = -(rcg << 4); m12 = rcg << 4; m13 = ir0g; m21 = rt13; m22 = rt13; m23 = rt13; m31 = rt22; m32 = rt22; m33 = rt22; @@ -1170,7 +1069,6 @@ pub fn gteMvmva(g: *mut[] u32, opc: u32) { m33 = s16ToI64(g[32 + mxBase + 4] & 0xFFFF); } - // Pick input vector. var vx: i64 = 0; var vy: i64 = 0; var vz: i64 = 0; @@ -1187,31 +1085,25 @@ pub fn gteMvmva(g: *mut[] u32, opc: u32) { vz = s16ToI64(vzS & 0xFFFF); } - // Pick translation vector. var tx: i64 = 0; var ty: i64 = 0; var tz: i64 = 0; - if (cvSel == 0) { // TR + if (cvSel == 0) { tx = s32ToI64(g[32 + 5]); ty = s32ToI64(g[32 + 6]); tz = s32ToI64(g[32 + 7]); } - if (cvSel == 1) { // BK (background colour) + if (cvSel == 1) { tx = s32ToI64(g[32 + 13]); ty = s32ToI64(g[32 + 14]); tz = s32ToI64(g[32 + 15]); } - if (cvSel == 2) { // FC (far colour) + if (cvSel == 2) { tx = s32ToI64(g[32 + 21]); ty = s32ToI64(g[32 + 22]); tz = s32ToI64(g[32 + 23]); } - // cvSel == 3: zero translation. - // MAC = (CV<<12) + M*V with nested 44-bit truncation between the - // additive terms (gteCheckMac). CV=FC (cvSel==2) is the hardware bug - // case: the (CV<<12 + M*VX) column is computed only for its FLAG side - // effects and dropped from the result, leaving the VY/VZ columns. var sm1: i64 = 0; var sm2: i64 = 0; var sm3: i64 = 0; if (cvSel == 2) { const k1: i64 = gteCheckMac(g, 1, m12 * vy); @@ -1220,8 +1112,6 @@ pub fn gteMvmva(g: *mut[] u32, opc: u32) { sm2 = gteClampMac(g, 2, k2 + m23 * vz, sf); const k3: i64 = gteCheckMac(g, 3, m32 * vy); sm3 = gteClampMac(g, 3, k3 + m33 * vz, sf); - // Dropped VX column: gteClampMac + gteClampIr purely to raise FLAG - // bits (the results are discarded, matching hardware). const d1: i64 = gteClampMac(g, 1, (tx << 12) + m11 * vx, sf); const d2: i64 = gteClampMac(g, 2, (ty << 12) + m21 * vx, sf); const d3: i64 = gteClampMac(g, 3, (tz << 12) + m31 * vx, sf); diff --git a/irq.jam b/irq.jam index 95ddfda..124e1aa 100644 --- a/irq.jam +++ b/irq.jam @@ -1,5 +1,3 @@ -// Interrupt controller. -// // Two registers live at 0x1F801070..0x1F801077: // 0x00 I_STAT status - set by devices raising IRQs, cleared by // writing 0 to the matching bit @@ -14,7 +12,6 @@ const { Vec } = import("std/collections"); -// IRQ line bits (IC_VBLANK = 0x001, etc.). pub const IC_VBLANK: u32 = 0x001; pub const IC_GPU: u32 = 0x002; pub const IC_CDROM: u32 = 0x004; @@ -27,22 +24,13 @@ pub const IC_SIO: u32 = 0x100; pub const IC_SPU: u32 = 0x200; pub const IC_LP_PIO: u32 = 0x400; -// COP0 CAUSE bit that signals "external IRQ pending". The R3000's -// general exception handler dispatches on this when SR.IEC and SR.IM2 -// are both set. const CAUSE_IP2: u32 = 0x00000400; const C0_CAUSE: u32 = 13; -// 16 u32 slots: [0]=I_STAT, [1]=I_MASK, [2..12]=per-source fire counters -// (used by main.jam to print which interrupt sources are firing during a -// hang), plus slack. Filled with 0 via typed slot writes (safe; the u32 -// loop collapses to a memset at -O1+). The Buf owns the allocation - -// its drop frees on scope exit, no irqFree helper needed. -pub fn createIrq() Vec(u32) { return Vec(u32).filled(0, 16); } +pub fn createIrq() Vec(u32) { + return Vec(u32).filled(0, 16); +} -// Update COP0_CAUSE.IP2 based on the current (stat & mask). Called -// after every write and every irqRaise so the CPU sees the right -// pending state on its next fetch. pub fn irqReevaluate(ic: *mut[] u32, cop0: *mut[] u32) { const pending: u32 = ic[0] & ic[1]; var cause: u32 = cop0[C0_CAUSE]; @@ -86,8 +74,6 @@ pub fn irqRead8(ic: *mut[] u32, off: u32) u32 { } } -// Writes to I_STAT clear bits that are written zero (acknowledge); -// writes to I_MASK replace the register outright. pub fn irqWrite32(ic: *mut[] u32, cop0: *mut[] u32, off: u32, val: u32) { match (off) { 0x00 { ic[0] = ic[0] & val; } @@ -125,13 +111,8 @@ pub fn irqWrite8(ic: *mut[] u32, cop0: *mut[] u32, off: u32, val: u32) { irqReevaluate(ic, cop0); } -// Raise one or more IRQ lines (bitmask). Called by device tick functions -// when a peripheral wants the CPU's attention. pub fn irqRaise(ic: *mut[] u32, cop0: *mut[] u32, lines: u32) { ic[0] = ic[0] | lines; - // Increment per-source fire counter for each line in the mask. - // ic[2+bit] is the counter for IRQ source bit. Used by main.jam to - // print a histogram of which IRQs are firing during a hang. var b: u32 = 0; while (b < 11) { if ((lines & (1 << b)) != 0) { diff --git a/main.jam b/main.jam index 80420a9..d1b5ba4 100644 --- a/main.jam +++ b/main.jam @@ -1,19 +1,7 @@ -// PSX emulator - SDL2 frontend. -// -// Glues bus.jam + cpu.jam + sdl.jam together, loads BIOS and (optional) -// PSX-EXE, then drives the CPU inside a 60 Hz SDL2 frame loop. Each -// frame runs ~565K CPU cycles (≈ 33.87 MHz / 60 Hz) and presents the -// PSX VRAM as a 1024×512 BGR555 texture. -// -// Pure-CPU unit tests live in tests.jam - keep them there so this -// file doesn't have to link SDL2 just to run them. - const { - Bus, - createBus, busRead32, busRead16, busRead8, + Bus, createBus, busRead32, busRead16, busRead8, busWrite32, busWrite16, busWrite8 } = import("bus"); -// Force device modules into main.jam's codegen context. const { Gpu } = import("gpu"); const { createGte } = import("gte"); const { Cdrom } = import("cdrom"); @@ -26,17 +14,14 @@ const { Mdec } = import("mdec"); const { createIrq, irqRaise, IC_VBLANK } = import("irq"); const { Mcd, mcdRamLoad, mcdRamSave } = import("mcd"); const { Timer } = import("timer"); - const { freshCpu, freshCpuAt, createRegFile, gprRead, gprWrite, cop0Read, cop0Write, - run, - step, biosHook, commitLoad, + run, step, biosHook, commitLoad, encR, encI, encJ } = import("cpu"); - const { Sdl, sdlInit, sdlQuit, sdlBlit, sdlPump, sdlDelay, sdlTicks, @@ -70,11 +55,13 @@ const VRAM_HEIGHT: i32 = 512; const SCREEN_W: i32 = 640; const SCREEN_H: i32 = 480; -// Decode the GPU's display-mode register (set by GP1 0x08) into -// horizontal and vertical pixel counts so the SDL window -// shows whatever resolution the BIOS or game has currently selected. -// -// Bit layout of GP1 0x08: +const GPU_CYCLES_HDRAW: f32 = 2560.0; +const GPU_CYCLES_SCANL: f32 = 3413.0; +const SCANLINES_VDRAW: u32 = 240; +const SCANLINES_TOTAL: u32 = 263; + +const FloatBits = union { i: u32, f: f32 }; + // 0-1 horizontal res code (0=256, 1=320, 2=512, 3=640) // 2 vertical res (0=240, 1=480 - but only if bit 5 interlace=1) // 3 PAL/NTSC @@ -91,12 +78,12 @@ fn dmodeWidth(dm: u32) i32 { } fn dmodeHeight(dm: u32, gpuState: *mut[] u32) i32 { - // Vertical 480 requires interlace (bit 5) + bit 2 set. - if ((dm & 0x04) != 0 && (dm & 0x20) != 0) { return 480; } - // When bit 2 alone is set, also 480. - if ((dm & 0x04) != 0) { return 480; } - // Otherwise compute from GP1(07) vertical range. g[67]=disp_y1, - // g[68]=disp_y2. + if ((dm & 0x04) != 0 && (dm & 0x20) != 0) { + return 480; + } + if ((dm & 0x04) != 0) { + return 480; + } const y1: i32 = gpuState[67] as i32; const y2: i32 = gpuState[68] as i32; const disp: i32 = y2 - y1; @@ -114,8 +101,6 @@ fn readU32At(buf: *mut[] u8, off: u32) u32 { fn loadExe(path: []u8, bus: Bus, regs: *mut[] u32, c: mut Cpu) { match (File.open(path)) { Some(f) { - // 0x800 = 2 KB PSX-EXE header - // [0; N] array literal already zero-fills; no memset needed var hdr: [2048]u8 = [0; 2048]; var hp: *mut[] u8 = hdr.asMutPtr(); const hdrBytes: u64 = f.read(hp[0..0x800]); @@ -162,10 +147,6 @@ fn loadExe(path: []u8, bus: Bus, regs: *mut[] u32, c: mut Cpu) { } } -// Load the 512 KB BIOS image into bus BIOS memory via std.fs. The -// destination is a []u8 view over the bus's BIOS buffer (range-slicing a -// many-item pointer), so the read is bounds-carrying rather than a raw -// fread into a bare pointer. fn loadBios(path: []u8, bus: Bus) u64 { match (File.open(path)) { Some(f) { @@ -182,51 +163,15 @@ fn loadBios(path: []u8, bus: Bus) u64 { return 0; } -// Run one full NTSC frame with cycle-accurate GPU timing. -// -// The GPU runs on its own 53.693175 MHz clock: a scanline is 3413 GPU -// cycles and HBlank spans [2560, 3413] (the last ~25%). Each instruction -// advances a *float* accumulator by -// `delta_cpu_cycles × (53.693175 / 33.868800)` GPU cycles. The HBlank -// event - line++, GPUSTAT bit-31 (odd/even), VBlank IRQ - fires on the -// crossing INTO [2560,3413]; the scanline wraps (acc -= 3413) on the -// crossing back out. The f32 accumulator preserves the sub-cycle phase: -// an integer cycles/scanline drifts ~0.03 -// cyc/line and by ~19M instructions mistimes a VBlank IRQ enough to -// diverge (found via REG_TRACE cmp). -// jam's f32 behaviour on these exact values is pinned by a unit test. -const GPU_CYCLES_HDRAW: f32 = 2560.0; -const GPU_CYCLES_SCANL: f32 = 3413.0; -const SCANLINES_VDRAW: u32 = 240; -const SCANLINES_TOTAL: u32 = 263; - -// f32↔u32 type-pun so the float scanline accumulator can persist across -// frames in a GPU scratch u32 slot (slot 37); `line` lives in slot 36. -const FloatBits = union { i: u32, f: f32 }; - -// Returns the number of stereo audio samples generated this frame (one -// per 768 CPU cycles ≈ 735–737). The main loop paces the frame off this -// count so the emulator advances at exactly the rate that yields 44.1 kHz -// output - keeping SPU production locked to the host's playback rate. fn runOneFrame(c: mut Cpu, bus: Bus, regs: *mut[] u32, cop0: *mut[] u32, audioDev: u32, ring: *mut[] u8) u32 { - // The scanline counter and the float GPU-cycle accumulator are - // CONTINUOUS across frames - they free-run rather than resetting per - // frame. Persist them in GPU scratch slots 36/37 (the f32 via - // a u32 type-pun) so the sub-cycle phase doesn't reset every frame. var gpuBuf: *mut[] u32 = bus.gpu.bufPtr(); var line: u32 = gpuBuf[36]; var accB: FloatBits = FloatBits { i: gpuBuf[37] }; var acc: f32 = accB.f; // GPU cycles per CPU cycle = 53.693175 / 33.868800 MHz. const ratio: f32 = (53.693175 as f32) / (33.868800 as f32); - // SPU sample accumulator: 1 sample per 768 CPU cycles (33.8688MHz/44100). - // Drives the ADSR envelope CPU-synchronously (slot 35, persisted). var spuAcc: u32 = gpuBuf[35]; - // Per-frame audio scratch. Each stereo sample is 4 bytes (S16 L + R). - // ~565K CPU cycles / 768 ≈ 735 samples/frame ≈ 2940 bytes; 4096 gives - // 1.4× headroom for short-frame jitter without spilling. Pushed to - // SDL via SDL_QueueAudio at end of frame; SDL pulls at 44.1kHz. var audioBuf: [4096]u8 = [0; 4096]; var audioLen: u32 = 0; var frameDone: bool = false; @@ -235,24 +180,12 @@ fn runOneFrame(c: mut Cpu, bus: Bus, regs: *mut[] u32, cop0: *mut[] u32, const before: u64 = c.cycles; step(c, bus, regs, cop0); const delta: u32 = (c.cycles - before) as u32; - // Per-instruction device order: cdrom -> gpu -> pad -> timer -> dma. - // (Was timer->dma->cdrom->…->gpu; the order decides which IRQ bit lands - // in I_STAT first each step, and the GPU hblank/vblank events must - // fire BEFORE the sysclk timer +2.) - - // 1. CDROM bus.cdrom.update(bus.disc.ptrMut(), bus.spu.ptr, bus.irq.ptr, cop0, delta); - // 2. GPU - scanline window [0,3413], HBlank [2560,3413]. The HBlank - // event (line++, GPUSTAT bit-31 odd/even, the hblank/vblank timer - // hooks + VBlank IRQ) fires on the crossing INTO [2560,3413] (~75% - // through the line); the scanline wraps on crossing out. This runs - // BEFORE the sysclk timer update. const prevHb: bool = acc >= GPU_CYCLES_HDRAW && acc <= GPU_CYCLES_SCANL; acc = acc + (delta as f32) * ratio; const currHb: bool = acc >= GPU_CYCLES_HDRAW && acc <= GPU_CYCLES_SCANL; if (currHb && !prevHb) { - // GPU HBlank event, fired from the HBlank-begin edge. bus.timer.hblankBegin(bus.irq.ptr, cop0); if (line < SCANLINES_VDRAW) { if ((line & 1) != 0) { @@ -273,35 +206,20 @@ fn runOneFrame(c: mut Cpu, bus: Bus, regs: *mut[] u32, cop0: *mut[] u32, frameDone = true; } } else if (prevHb && !currHb) { - // HBlank-end edge -> wrap the scanline accumulator - // (acc -= 3413). bus.timer.hblankEnd(); acc = acc - GPU_CYCLES_SCANL; } - // 3. PAD padUpdate(bus.pad.ptr, bus.irq.ptr, cop0, delta); - // 4. TIMER sysclk - updateCyc ignores delta, uses 2. bus.timer.updateCyc(bus.irq.ptr, cop0, delta); - // 5. DMA dmaTickSpu(bus.dma.ptr, delta); dmaUpdate(bus.dma.ptr, bus.irq.ptr, cop0); - // jam-only extras: neither of these belongs in the per-instruction - // device update above. Kept at the end (least perturbation to the - // device order above) and flagged as divergences - SIO1 -> - // peripherals task, SPU CPU-clock -> SPU task (revisit once the - // deterministic CPU surface settles). bus.sio1.update(bus.irq.ptr, cop0, delta); spuAcc = spuAcc + delta; while (spuAcc >= 768) { spuAcc = spuAcc - 768; const sample: u32 = spuGetSample(bus.spu.ptr, bus.spuram.ptr, bus.irq.ptr, cop0); - // Stash the sample bytes into the per-frame audio scratch. - // Drop on buffer-full instead of stomping past the end - - // worst-case ~735 samples/frame fits comfortably in 4096 - // bytes; the guard is belt-and-suspenders against runaway - // delta accumulation (e.g. after a long debugger pause). if (audioLen + 4 <= 4096) { var ap: *mut[] u8 = audioBuf.asMutPtr(); ap[audioLen] = (sample & 0xFF) as u8; @@ -313,27 +231,19 @@ fn runOneFrame(c: mut Cpu, bus: Bus, regs: *mut[] u32, cop0: *mut[] u32, gpuBuf[34] = sample; } } - // End-of-frame: push this frame's samples into the SPSC ring. SDL's - // audio callback (psone_audio_cb) drains it at the host 44.1 kHz - // rate; under/overruns are handled at sample granularity inside the - // ring (silence-fill / bounded drop) rather than dropping whole - // frames. sdlAudioRingPush locks the device so it can't race the - // callback. if (audioDev != 0 && audioLen > 0) { sdlAudioRingPush(audioDev, ring, audioBuf.asMutPtr(), audioLen); } - // NOTE: do NOT commitLoad here - each instruction's own commitLoad + + // NOTE! do NOT commitLoad here - each instruction's own commitLoad // handles the R3000 load-delay slot. gpuBuf[36] = line; var outB: FloatBits = FloatBits { f: acc }; gpuBuf[37] = outB.i; gpuBuf[35] = spuAcc; - return audioLen / 4; // stereo samples produced this frame + return audioLen / 4; } -// True if NUL-terminated `p` ends with ".exe" / ".EXE". Mirrors -// disc.jam's pathEndsWithCue so an EXE passed as argv[2] is routed to -// the sideload path instead of discOpen. fn pathEndsWithExe(p: []u8) bool { var n: u32 = 0; while (p.ptr[n] != 0) { n = n + 1; } @@ -342,7 +252,7 @@ fn pathEndsWithExe(p: []u8) bool { const b: u32 = p.ptr[n - 3] as u32; const cC: u32 = p.ptr[n - 2] as u32; const e: u32 = p.ptr[n - 1] as u32; - // ".exe" or ".EXE" + // ".exe" or ".EXE" (todo: use str with contains) if (a == 0x2E && (b == 0x65 || b == 0x45) && (cC == 0x78 || cC == 0x58) && (e == 0x65 || e == 0x45)) { @@ -353,47 +263,35 @@ fn pathEndsWithExe(p: []u8) bool { fn main() { var regs: Vec(u32) = createRegFile(32); - // 32 COP0 slots (not 16): MFC0/MTC0 take a 5-bit rd (0..31), so a - // reserved index like $16+ must land in a valid slot rather than read/ - // write past the end. Matches PCSX-Redux's 32-entry COP0 array. var cop0: Vec(u32) = createRegFile(32); var bus: Bus = createBus(); var c: Cpu = freshCpu(); - // Memory card slot 1 - pull saved data from ./slot1.mcd if it - // exists. On exit we write the (possibly modified) 128 KB buffer - // back to the same file so saves survive across runs. var mcdPath: []u8 = "./slot1.mcd"; if (mcdRamLoad(bus.mcdRam.ptr, mcdPath)) { print("Loaded memcard: ./slot1.mcd\n"); } - // Initial COP0 state. The 0x10900000 value seeds COP0 - // SR with CU0 enabled, BEV set, and a few breakpoint controls. cop0Write(cop0.ptr, C0_SR, 0x10900000); cop0Write(cop0.ptr, C0_PRID, 0x00000002); - // CLI: jamstation [GAME_PATH] + // jamstation [GAME_PATH] // argv[1] = BIOS image - REQUIRED, no default. // argv[2] = optional game image: a .cue/.bin disc or a .exe - // (dispatched below). When omitted, the fixed ./game.* - // / ./demo.exe names in the cwd are used as a fallback. var biosPath: []u8 = ""; var hasBios: bool = false; var argsIt: Args = args(); - next(argsIt); // skip argv[0] (the program path) + next(argsIt); match (next(argsIt)) { Some(arg) { biosPath = arg; hasBios = true; } None { } } var gameArg: []u8 = ""; var hasGameArg: bool = false; - match (next(argsIt)) { // argv[2]: optional game image + match (next(argsIt)) { Some(arg) { gameArg = arg; hasGameArg = true; } None { } } - // A BIOS image is required - there is no default. Bail with a usage - // message instead of silently booting to a black screen. if (!hasBios) { print("error: no BIOS image given\nusage: jamstation [GAME_PATH]\n"); exit(1); @@ -405,24 +303,11 @@ fn main() { const got: u64 = loadBios(biosPath, bus); print("Loaded BIOS: {got} bytes from {biosPath}\n"); - // EXE side-load. If `./demo.exe` exists we drop it into RAM and jump - // straight to its entry point (skipping the BIOS boot animation - - // appropriate for hand-crafted demos with their own init). For games - // that depend on the BIOS kernel being set up, drop the EXE at - // `./game.exe` instead: we boot the BIOS first and only sideload - // when PC reaches 0x80030000 (the standard "kernel ready" address - // for this handoff). var demoPath = "./demo.exe"; var gamePath = "./game.exe"; var cuePath: []u8 = "./game.cue"; var binPath: []u8 = "./game.bin"; var pendingGameExe: bool = false; - // Game image. An explicit argv[2] wins; otherwise fall back to the - // fixed ./game.cue / ./game.bin / ./demo.exe / ./game.exe names in - // the cwd. A .exe is sideloaded (BIOS boots first, then we hand off - // at PC=0x80030000); a .cue/.bin is attached as a CD-ROM so the BIOS - // bootstraps the game off the disc - discOpen auto-detects .cue - // (multi-track aware) vs a raw .bin (single-track Mode2/2352). if (hasGameArg) { if (pathEndsWithExe(gameArg)) { gamePath = gameArg; @@ -453,24 +338,8 @@ fn main() { } } - // SDL window + texture. We render the full 1024×512 VRAM as a BGR555 - // texture, which matches the PSX's natural framebuffer format. A - // future GPU implementation pokes pixels into VRAM; until then this - // is a debug pattern. print("Initializing SDL2...\n"); var title = "jamstation"; - // Set the JAM_PSONE_FULL_VRAM env var (any non-empty value) to - // see the whole 1024×512 VRAM in the window instead of the - // BIOS's 320×240 display rectangle. Useful for spotting where the - // BIOS is loading textures / drawing into off-screen targets when - // the visible window looks empty. - // Touch a `./FULL_VRAM` sentinel file (any contents) to swap the - // window to a 1024×512 view of the entire VRAM instead of the - // BIOS's 320×240 display rectangle. Useful for spotting where the - // BIOS is loading textures / drawing into off-screen targets when - // the visible window looks empty. (Env-var detection would have - // been cleaner but Jam v0.1.0 can't compare a `*const[] u8` from - // getenv() against NULL.) var sentinel = "./FULL_VRAM"; const fullVram: bool = exists(sentinel); var sdlW = SCREEN_W; @@ -481,14 +350,6 @@ fn main() { print("JAM_PSONE_FULL_VRAM set. Showing full VRAM\n"); } var sdl: Sdl = sdlInit(title, sdlW, sdlH); - // Audio device - callback + SPSC ring buffer. The SPU is advanced - // CPU-synchronously inside runOneFrame (one sample per 768 cycles - // for deterministic envelope state - required for Brave Fencer's - // intro to not loop, see commit 51b9ad9); those samples are pushed - // into `audioRing`, and SDL's audio callback (psone_audio_cb) drains - // the ring at the host 44.1 kHz rate. This replaces queue mode, - // whose whole-frame drops on pacing jitter were the "robotic" gaps. - // Blob = 16-byte header + 16384-byte data region (see sdl.jam RING_*). var audioRing: [16400]u8 = [0; 16400]; var ringPtr: *mut[] u8 = audioRing.asMutPtr(); var audioDev: u32 = sdlAudioOpen(1024, psone_audio_cb as u64, @@ -498,48 +359,22 @@ fn main() { } else { print("Audio device opened (id={audioDev}, 44.1kHz S16 stereo, callback+ring)\n"); } - // VRAM was already zero-filled when the Bus was allocated; the BIOS - // renders into it within the first frame, so the brief black flash - // before its first GP0 commit is fine. - print("Running. Esc or close to quit\n"); - // Active-low button mask the SDL pump updates from keyboard events. - // 0xFFFF = no buttons pressed; sdlPump clears bits on KEYDOWN and - // sets them on KEYUP. padUpdate copies it into the SDA pad state - // each frame so the next controller poll sees fresh input. Stack- - // local - sdlPump just borrows the pointer per call. var buttons: [1]u32 = [0xFFFF; 1]; var btnPtr: *mut[] u32 = buttons.asMutPtr(); var quit: bool = false; - // Idle-loop fallback for game.exe - when no disc is attached the - // BIOS shell renders, then sits in its main poll loop forever. - // We detect "BIOS finished drawing" by watching the GPU polygon - // counter: once it's been stable for ~3s of wall-clock with at - // least some content drawn, we sideload game.exe. var idleFrames: u32 = 0; var lastPoly: u32 = 0; - const IDLE_TRIGGER: u32 = 180; // ~3s at 60 fps - const MIN_POLY_DRAWN: u32 = 200; // boot animation + shell minimum - // Frame-rate limiter: pace to the PSX's native ~59.94 Hz, independent of - // the host display refresh. The emulated frame is already ~565K cycles - // (33.8688 MHz / 59.94); without wall-clock pacing the VSync-only present - // runs the whole machine at the monitor's rate (2x on a 120 Hz ProMotion - // panel) and overruns/drops the audio queue. The f64 accumulator avoids - // the drift an integer-ms target would introduce. + const IDLE_TRIGGER: u32 = 180; + const MIN_POLY_DRAWN: u32 = 200; const FRAME_MS: f64 = 1000.0 / 59.94; var nextFrameMs: f64 = sdlTicks() as f64; while (!quit) { - // Game-EXE side-load. Two trigger paths: // 1. BIOS jumps to 0x80030000 (real-disc boot handoff). // 2. BIOS shell is parked at the kernel idle loop with no disc; // after IDLE_TRIGGER frames stuck there we sideload. - // Sideload trigger 1: BIOS jumps anywhere in the disc-boot handoff - // region (0x80030000-0x80040000). Real-disc boot lands at 0x80030000 - // exactly, but the BIOS bootloader can also park at other addresses - // in this range when no disc is present (e.g. 0x8003D78C = - // ps1-tests run path). if (pendingGameExe && c.pc >= 0x80030000 && c.pc < 0x80040000) { loadExe(gamePath, bus, regs.ptr, c); pendingGameExe = false; @@ -562,21 +397,13 @@ fn main() { } const frameSamples: u32 = runOneFrame(c, bus, regs.ptr, cop0.ptr, audioDev, ringPtr); - // Audio: runOneFrame pushed the per-frame sample batch into the - // ring; SDL's callback (psone_audio_cb) drains it at the host's - // 44.1 kHz pull rate. var vram: *mut[] u8 = bus.vram.ptr; - // Read the current display origin out of the GPU state buffer - // (g[63] disp_x, g[64] disp_y - set by GP1 0x05). The BIOS - // moves these around during double-buffered animation. var gpuState: *mut[] u32 = bus.gpu.bufPtr(); const dispX: i32 = gpuState[63] as i32; const dispY: i32 = gpuState[64] as i32; // GPUSTAT bit 21 (mode bit 4) = 24bpp display. // GPUSTAT bit 23 = display disabled (black screen). const dm: u32 = gpuState[47]; - // Refresh Timer 0's dot-clock divisor from the current display mode - // (used when timer 0's clock source is the GPU dot-clock). bus.timer.setDotclock(dm); const is24bpp: bool = (dm & 0x10) != 0; const displayOff: bool = (gpuState[46] & 0x00800000) != 0; @@ -590,22 +417,8 @@ fn main() { is24bpp, displayOff); } quit = sdlPump(btnPtr) > 0; - // Push the latest keyboard state into the SDA pad. Cheap (2-byte - // write); doing it once per frame is enough since controller - // polling runs at VBlank cadence in the BIOS. padSetButtons(bus.pad.ptr, buttons[0]); - // Pace this frame by the AUDIO it produced: frameSamples / 44100 s. - // The SPU emits one sample per 768 CPU cycles, so this makes the - // emulator advance at exactly 33.8688 MHz - the rate at which - // production matches the host's 44.1 kHz playback, so the audio ring - // never drifts full (the old fixed-59.94 Hz target emitted ~737 - // samples/frame = 44178 Hz, a 0.18% excess that crept the ring to - // overrun every few seconds). The renderer runs WITHOUT vsync (see - // sdlInit - vsync would block the present on an unfocused window), so - // this wall-clock limiter is the sole pacer. Fall back to the fixed - // target only if a frame somehow produced no audio (avoids a busy - // spin); a normal frame is ~735–737 samples ≈ 16.7 ms. var frameMs: f64 = FRAME_MS; if (frameSamples > 0) { frameMs = (frameSamples as f64) * 1000.0 / 44100.0; @@ -615,8 +428,6 @@ fn main() { if ((nowMs as f64) < nextFrameMs) { sdlDelay((nextFrameMs - (nowMs as f64)) as u32); } else { - // Fell behind (slow frame) - resync so we never burst-catch-up - // faster than real time. nextFrameMs = nowMs as f64; } } diff --git a/mcd.jam b/mcd.jam index 34eb745..25be9c2 100644 --- a/mcd.jam +++ b/mcd.jam @@ -1,13 +1,3 @@ -// Memory Card (PSX BU01). -// -// Memory cards live on SIO0 as a second device alongside the joypad. -// When the host writes 0x81 to SIO0_DATA the bus selects the memcard; -// subsequent bytes drive an SDA-style state machine that reads/writes -// 128 bytes per frame. Our pad.jam handles slot selection - it routes -// 0x81 dest writes through here. -// -// State machine (MCD_STATE_*): -// // TX_HIZ -> 0xFF on first read (line high-Z) // TX_FLG -> flag byte (0x08 = "first access since power-on") // TX_ID1 / TX_ID2 -> 0x5A / 0x5D identification bytes @@ -16,18 +6,12 @@ // W_* -> WRITE subprotocol: take MSB/LSB + 128 data bytes // + checksum, then ACK + 'G' end marker // S_* -> STATUS subprotocol (rarely used) -// -// We model a single-slot card with an in-memory 128 KB buffer; the -// buffer is kept as a separate heap allocation (Bus.mcdRam) so callers -// can load/save it from disk without owning the device state. const { Vec } = import("std/collections"); -// File I/O via std.fs (its libc externs are the single source). const { File } = import("std/fs"); -const MCD_MEMORY_SIZE: u64 = 0x20000; // 128 KB +const MCD_MEMORY_SIZE: u64 = 0x20000; -// MCD state-machine states. const MCD_TX_HIZ: u32 = 0; const MCD_TX_FLG: u32 = 1; const MCD_TX_ID1: u32 = 2; @@ -56,14 +40,6 @@ const MCD_S_TX_DAT1: u32 = 24; const MCD_S_TX_DAT2: u32 = 25; const MCD_S_TX_DAT3: u32 = 26; -// Memory-card device state. Fields mirror the named slots of the old -// 32-byte state buffer (S_STATE, S_TX_DATA, …). `addr` and `pend` -// are 16-bit values used during the read/write subprotocols. -// -// `pad` pushes the struct above kByValueMaxBytes (16), forcing the -// ByPointer ABI so a `mut Mcd` parameter takes a pointer to the -// caller's storage - mutations through `mcd.X` reach the live card, -// rather than landing in a snapshot copy and being discarded. pub const Mcd = struct { state: u8, txData: u8, @@ -85,7 +61,7 @@ pub const Mcd = struct { rxData: 0, txReady: 0, mode: 0, - flag: 0x08, // "first access since power-on" + flag: 0x08, msb: 0, lsb: 0, addr: 0, @@ -104,17 +80,11 @@ pub const Mcd = struct { return self.txReady != 0; } - // Read one byte from the card. The state - // advances after each byte; the host clocks 8 bits per call. - // Returns the byte the host should latch into SIO0's RX register. pub fn read(self: mut Self, ram: *mut[] u8) u32 { const st: u32 = self.state as u32; match (st) { MCD_TX_HIZ { self.txData = 0xFF; } MCD_TX_FLG { - // If the host's last RX was 0x81/0x01 the card - // bails out - game-side probe code expects 0xFF when it - // tries to ping us by sending the dest byte twice. const rx: u32 = self.rxData as u32; if (rx == 0x81 || rx == 0x01) { self.txReady = 1; @@ -123,20 +93,21 @@ pub const Mcd = struct { return 0xFF; } self.txData = self.flag; - self.flag = 0; // "first access" flag is one-shot + self.flag = 0; } MCD_TX_ID1 { self.txData = 0x5A; } MCD_TX_ID2 { - // Choose subprotocol based on the mode byte the host sent - // earlier. self.txReady = 1; self.txData = 0x5D; const m: u32 = self.mode as u32; - if (m == 0x52) { // 'R' = read + // 'R' = read + if (m == 0x52) { self.state = MCD_R_RX_MSB as u8; - } else if (m == 0x57) { // 'W' = write + // 'W' = write + } else if (m == 0x57) { self.state = MCD_W_RX_MSB as u8; - } else if (m == 0x53) { // 'S' = status + // 'S' = status + } else if (m == 0x53) { self.state = MCD_S_TX_ACK1 as u8; } else { // Unknown mode - drop the slot. @@ -176,7 +147,8 @@ pub const Mcd = struct { MCD_R_TX_MEB { self.txReady = 0; self.state = MCD_TX_HIZ as u8; - return 0x47; // 'G' = good end marker + // 'G' = good end marker + return 0x47; } MCD_W_RX_MSB { self.txData = 0x00; } MCD_W_RX_LSB { @@ -210,8 +182,6 @@ pub const Mcd = struct { return self.txData as u32; } - // Latch a byte the host wrote. Some states use the byte (mode - // select, address MSB/LSB, write data); others ignore it. pub fn write(self: mut Self, data: u32) { const d: u8 = (data & 0xFF) as u8; self.rxData = d; @@ -229,17 +199,10 @@ pub const Mcd = struct { } }; -// 128 KB memory-card backing store, zero-filled via typed slot writes -// (Buf.filled is safe for any T; optimizer collapses the u8 loop to a -// memset at -O1+). Owned by the Bus's `mcdRam` Buf - its drop frees on -// scope exit, no mcdRamFree helper needed. pub fn createMcdRam() Vec(u8) { return Vec(u8).filled(0, MCD_MEMORY_SIZE as u32); } -// Read a 128 KB memory-card image from `path` into `ram`. Returns 1 on -// success, 0 if the file is absent or unreadable. We use access(R_OK=4) -// first because Jam v0.1.0 can't null-check fopen's return. pub fn mcdRamLoad(ram: *mut[] u8, path: []u8) bool { match (File.open(path)) { Some(f) { @@ -252,8 +215,6 @@ pub fn mcdRamLoad(ram: *mut[] u8, path: []u8) bool { return false; } -// Write the 128 KB memory-card image at `ram` to `path`. Used at -// shutdown so player progress isn't lost across emulator runs. pub fn mcdRamSave(ram: *mut[] u8, path: []u8) bool { match (File.create(path)) { Some(f) { diff --git a/mdec.jam b/mdec.jam index 086a19d..a346901 100644 --- a/mdec.jam +++ b/mdec.jam @@ -1,38 +1,21 @@ -// MDEC - Motion Decoder. -// -// PSX intros (Musashi SQUARESOFT logo, Harvest Moon opening, BIOS PS-X -// startup splash, etc.) decompress YUV macroblock data through this -// chip. The game's flow per frame: -// +// MDEC is Motion Decoder. +// basically: // 1. CdlReadS streams compressed sectors into RAM. // 2. DMA channel 0 copies input -> MDEC's input FIFO (0x1F801820). // 3. MDEC runs RLE-decode -> inverse-quantisation -> iDCT -> YUV->RGB. // 4. DMA channel 1 reads decoded RGB -> RAM. // 5. GPU display VRAM blit of the decoded frame. -// -// Without MDEC, step 4 hands the game zeros / never fires the DMA-done -// IRQ -> game stalls on its FMV-wait loop. -// -// Layout: state is a flat struct of typed fields. Input/output buffers -// stay heap-allocated separately (256KB + 1MB) so we avoid per-command -// malloc/free. const { Vec } = import("std/collections"); -const MDEC_INPUT_SIZE: u32 = 0x40000; // 256 KB -const MDEC_OUTPUT_SIZE: u32 = 0x100000; // 1 MB - -// Command-table indices (top 3 bits of cmd word). +const MDEC_INPUT_SIZE: u32 = 0x40000; +const MDEC_OUTPUT_SIZE: u32 = 0x100000; const MDEC_CMD_NOP: u32 = 0; const MDEC_CMD_DECODE: u32 = 1; const MDEC_CMD_SET_QT: u32 = 2; const MDEC_CMD_SET_ST: u32 = 3; - -// Output depth values. const DEPTH_4BIT: u32 = 0; const DEPTH_15BIT: u32 = 3; - -// Zig-zag table - index into yblk in iDCT-input order. const ZAGZIG_INIT: [64]u8 = [ 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, @@ -44,13 +27,6 @@ const ZAGZIG_INIT: [64]u8 = [ 53, 60, 61, 54, 47, 55, 62, 63, ]; -// Aggregate MDEC state. Scalars are u32 (matching the old buffer -// layout for ease of bit-fiddling); tables are typed arrays -// addressable via `self.field` or `self.field.asMutPtr()`. -// -// yBlk / crBlk / cbBlk are 128-element i16 arrays - the iDCT -// ping-pongs between the first 64 entries (live block) and the next -// 64 (scratch), using a "blk + 128" scratch offset. pub const Mdec = struct { cmd: u32, wordsRem: u32, @@ -127,28 +103,13 @@ pub const Mdec = struct { }; } - // iDCT - // - // Two-pass 8×8 transform. The block (blk) provides both the input - // and final output; we ping-pong through the scratch slot at - // blk + 64 (i.e. the second half of the 128-element block array). pub fn realIdct(self: Self, blk: *mut[] i16) { idctPassPtr(blk, blk, 64, self.scale.asMutPtr()); idctPassPtr(blk, blk, 64, self.scale.asMutPtr()); - // Note: the iDCT ping-pongs blk↔scratch via 64-entry offset. - // Our 128-entry block holds both. After the two passes the - // final result lives in blk[0..64]. } - // RLE block decode - // - // Reads 16-bit words from `input` starting at `inIdx`. Decodes one - // block into `blk` (a 128-entry i16 array; lower 64 are the live - // block, upper 64 the iDCT scratch). `quant` selects Y or UV. - // Returns the new input word index. pub fn rlDecodeBlock(self: Self, input: *mut[] u8, inIdx: u32, blk: *mut[] i16, quant: *mut[] u8) u32 { - // Clear the live block. var i: u32 = 0; while (i < 64) { blk[i] = 0; @@ -193,7 +154,6 @@ pub const Mdec = struct { return idx; } - // YUV -> RGB pub fn yuvToRgb(self: Self, output: *mut[] u8, outBase: u32, xx: u32, yy: u32) { const depth: u32 = self.outputDepth; @@ -208,8 +168,6 @@ pub const Mdec = struct { const rRaw: i32 = self.crBlk[cri] as i32; const bRaw: i32 = self.cbBlk[cri] as i32; - // YUV->RGB coefficients (float): g = -0.3437*b + -0.7143*r; - // r = 1.402*r; b = 1.772*b. const rf: f64 = rRaw as f64; const bf: f64 = bRaw as f64; const gFloat: f64 = (0.0 - 0.3437) * bf + (0.0 - 0.7143) * rf; @@ -251,13 +209,11 @@ pub const Mdec = struct { } } - // decode macroblock pub fn decodeMacroblock(self: mut Self, input: *mut[] u8, output: *mut[] u8) { const depth: u32 = self.outputDepth; const inputBytes: u32 = self.inputSizeB; if (depth < 2) { - // 4-bit / 8-bit mono - single Y block per macroblock. self.rlDecodeBlock(input, 0, self.yBlk.asMutPtr(), self.yQuant.asMutPtr()); var i: u32 = 0; @@ -266,20 +222,22 @@ pub const Mdec = struct { output[i] = (yv & 0xFF) as u8; i = i + 1; } - var words: u32 = 16; // 64 bytes / 4 - if (depth == DEPTH_4BIT) { words = 8; } + var words: u32 = 16; + if (depth == DEPTH_4BIT) { + words = 8; + } self.outputWords = words; self.outputEmpty = 0; self.outputIndex = 0; return; } - // 24-bit (depth=2) / 15-bit (depth=3) - full YUV macroblock. - var blockSize: u32 = 768; // 16×16×3 (24-bit) - if (depth == DEPTH_15BIT) { blockSize = 512; } // 16×16×2 + var blockSize: u32 = 768; + if (depth == DEPTH_15BIT) { + blockSize = 512; + } - var idx: u32 = 0; // word index into input buffer + var idx: u32 = 0; var outBytes: u32 = 0; - // Each macroblock: Cr, Cb, Y0, Y1, Y2, Y3 -> 16×16 RGB. while (idx * 2 < inputBytes) { const outBase: u32 = outBytes; idx = self.rlDecodeBlock(input, idx, @@ -313,7 +271,6 @@ pub const Mdec = struct { self.outputIndex = 0; } - // command dispatch (write to 0x1F801820) pub fn setQt(self: mut Self, input: *mut[] u8) { var i: u32 = 0; while (i < 64) { @@ -335,7 +292,6 @@ pub const Mdec = struct { const lo: u32 = input[i * 2] as u32; const hi: u32 = input[i * 2 + 1] as u32; var v: u32 = (hi << 8) | lo; - // Sign-extend the 16-bit value into the i16 field. if ((v & 0x8000) != 0) { v = v | 0xFFFF0000; } self.scale[i] = v as i16; i = i + 1; @@ -360,10 +316,8 @@ pub const Mdec = struct { } } - // bus dispatch pub fn read32(self: mut Self, output: *mut[] u8, off: u32) u32 { if (off == 0) { - // Data port - drain one word from the output buffer. const wordsRem: u32 = self.outputWords; if (wordsRem > 0) { const idx: u32 = self.outputIndex; @@ -378,28 +332,14 @@ pub const Mdec = struct { return 0xAAAAAAAA; } if (off == 4) { - // Status register. // Bits 0-15 = "number of parameter words remaining MINUS 1" // (nocash: FFFFh = none). DuckStation `(remaining/2)-1`, Avocado - // `(paramCount-1)&0xffff`, mednafen's 0xFFFF sentinel all agree; - // the old jam code reported the raw count, so a game - // that polls for (status & 0xFFFF)==0xFFFF after a SET_QT/SET_ST - // upload to confirm "no params pending" never saw it and never - // advanced to DECODE. wordsRem is u32, so 0-1 wraps to 0xFFFF here. + // `(paramCount-1)&0xffff`, mednafen's 0xFFFF sentinel all agree var st: u32 = (self.wordsRem - 1) & 0xFFFF; st = st | ((self.curBlock & 0x7) << 16); if (self.outputBit15 != 0) { st = st | (1 << 23); } if (self.outputSigned != 0) { st = st | (1 << 24); } st = st | ((self.outputDepth & 3) << 25); - // Request bits 27/28 and Data-Out FIFO Empty (31) are LEVELS - // recomputed here, NOT latched: bit 27 = DMA1 enabled AND output - // has data; bit 28 = DMA0 enabled AND more input wanted; bit 31 = - // output drained. Matches nocash psx-spx, DuckStation - // (`enable_dma_out && !out_fifo.empty()`) and Avocado. Latching - // outputReq at the final decode word instead would mean a game that - // enables DMA1 AFTER the decode reads bit 27 = 0 forever and never - // DMAs the MDEC output back out -- the frame decodes but is never - // read (FMV skip). if (self.enableDma1 != 0 && self.outputWords > 0) { st = st | (1 << 27); } if (self.enableDma0 != 0 && self.wordsRem > 0) { st = st | (1 << 28); } if (self.busy != 0) { st = st | (1 << 29); } @@ -421,8 +361,6 @@ pub const Mdec = struct { pub fn write32(self: mut Self, input: *mut[] u8, output: *mut[] u8, off: u32, val: u32) { if (off == 0) { - // Data port. Either streaming command parameters or the - // command word itself when wordsRem == 0. const wordsRem: u32 = self.wordsRem; if (wordsRem > 0) { const idx: u32 = self.inputIndex; @@ -431,12 +369,6 @@ pub const Mdec = struct { self.wordsRem = wordsRem - 1; if (wordsRem - 1 == 0) { self.outputEmpty = 0; - // dispatch() consumes the whole input FIFO synchronously, - // so by the time the guest reads status the data-in FIFO is - // empty (bit30 = 0), NOT full. The old `inputFull = 1` left - // bit30 stuck at 1 after a SET_QT/SET_ST upload, so a game - // that polls "in-FIFO not full" before issuing the next - // command (the DECODE) would wait forever. Refs report 0. self.inputFull = 0; self.inputReq = 0; self.busy = 0; @@ -449,10 +381,6 @@ pub const Mdec = struct { self.cmd = val; self.outputReq = 0; self.outputEmpty = 1; - // Clear any stale output count from a prior decode so status - // bit 31 (out-FIFO-empty) and bit 27 (out-request) read correctly - // after a SET_QT/SET_ST that doesn't write output (duckstation - // clears the out FIFO on every command parse, mdec.cpp:433). self.outputWords = 0; self.outputBit15 = (val >> 25) & 1; self.outputSigned = (val >> 26) & 1; @@ -468,7 +396,11 @@ pub const Mdec = struct { newWords = val & 0xFFFF; } else if (op == MDEC_CMD_SET_QT) { self.recvColor = val & 1; - if ((val & 1) != 0) { newWords = 32; } else { newWords = 16; } + if ((val & 1) != 0) { + newWords = 32; + } else { + newWords = 16; + } } else if (op == MDEC_CMD_SET_ST) { newWords = 32; } @@ -483,10 +415,17 @@ pub const Mdec = struct { } if (off == 4) { // Control register. - if ((val & 0x40000000) != 0) { self.enableDma0 = 1; } - else { self.enableDma0 = 0; } - if ((val & 0x20000000) != 0) { self.enableDma1 = 1; } - else { self.enableDma1 = 0; } + if ((val & 0x40000000) != 0) { + self.enableDma0 = 1; + } else { + self.enableDma0 = 0; + } + + if ((val & 0x20000000) != 0) { + self.enableDma1 = 1; + } else { + self.enableDma1 = 0; + } if ((val & 0x80000000) != 0) { // Reset - status = 0x80040000. self.busy = 0; @@ -498,10 +437,6 @@ pub const Mdec = struct { self.outputReq = 0; self.inputFull = 0; self.outputEmpty = 1; - // Status bit 31 (out-FIFO-empty) is computed from outputWords, - // so it must be cleared here too or a reset issued with - // undrained output would report not-empty (duckstation SoftReset - // clears the out FIFO, mdec.cpp:330-344). self.outputWords = 0; self.outputIndex = 0; self.curBlock = 4; @@ -521,13 +456,13 @@ pub const Mdec = struct { } }; -// input/output buffer allocation (still heap-resident) - -// Owned MDEC IO buffers. Auto-drop with the Bus. -pub fn createMdecInput() Vec(u8) { return Vec(u8).filled(0, MDEC_INPUT_SIZE); } -pub fn createMdecOutput() Vec(u8) { return Vec(u8).filled(0, MDEC_OUTPUT_SIZE); } +pub fn createMdecInput() Vec(u8) { + return Vec(u8).filled(0, MDEC_INPUT_SIZE); +} -// free helpers (no state required) +pub fn createMdecOutput() Vec(u8) { + return Vec(u8).filled(0, MDEC_OUTPUT_SIZE); +} pub fn exts10(v: u32) i32 { const t: u32 = (v << 22) & 0xFFFFFFFF; @@ -540,10 +475,6 @@ pub fn clampS10(v: i32) i32 { return v; } -// Wrap an i32 to int16. The RLE dequant product is held in an int16_t, -// so it TRUNCATES before the CLAMP(-0x400,0x3ff). For products past -// int16 range the wrap can flip the sign and clamp to the opposite -// bound - must be applied pre-clamp. pub fn mdecTrunc16(v: i32) i32 { return ((v & 0xFFFF) ^ 0x8000) - 0x8000; } @@ -572,14 +503,8 @@ pub fn writeU32(buf: *mut[] u8, off: u32, v: u32) { buf[off + 3] = ((v >> 24) & 0xFF) as u8; } -// iDCT pass operating on i16 pointers + scale table. The block is -// 128 entries - the live block lives in [0..64], scratch in [64..128]. -// `scratchOff` is the index offset (64) into the same buffer for the -// secondary pass. We pingpong by indexing through that offset. pub fn idctPassPtr(blk: *mut[] i16, _dst: *mut[] i16, _scratchOff: u32, scale: *mut[] i16) { - // Pass: write into the scratch half, then copy back. This is the - // two-pass iDCT via a 64-entry temp. var scratch: [64]i16 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, diff --git a/pad.jam b/pad.jam index c342cae..be504a0 100644 --- a/pad.jam +++ b/pad.jam @@ -1,58 +1,34 @@ -// SIO0 joypad controller (SDA digital pad protocol). Register file at -// 0x1F801040..0x1F80104F: -// 0x40 JOY_TX_FIFO (write) / JOY_RX_FIFO (read) -// 0x44 JOY_STAT (read) -// 0x48 JOY_MODE -// 0x4A JOY_CTRL -// 0x4E JOY_BAUD -// -// Minimum protocol the BIOS exercises during PAD driver init and every -// VSync: write 0x01 to TX (start poll), then write 0x42 (read switches) -// and clock out 0x41 / 0x5A / SW low / SW high - the BIOS keeps the -// last two bytes as the cached pad state. The /ACK line and the IRQ7 -// edge are modelled via a `cycles_until_irq` countdown; when it -// expires, IC_JOY (0x080) is raised and JOY_STAT.IRQ7 is set. -// -// We only support slot 0 with a fixed "digital pad, no buttons pressed" -// state. That's enough for the BIOS shell to detect "no controller" -// gracefully and for games to advance past `PadInit` even without real -// input. Once we wire SDL keyboard events into the `sw` bitmask we can -// drive a real X-press. - const { irqRaise, IC_JOY } = import("irq"); const { Mcd } = import("mcd"); - const { Vec } = import("std/collections"); const PAD_STATE_SIZE: u32 = 32; -const P_STAT_LO: u32 = 0; // u16 +const P_STAT_LO: u32 = 0; const P_STAT_HI: u32 = 1; -const P_CTRL_LO: u32 = 2; // u16 +const P_CTRL_LO: u32 = 2; const P_CTRL_HI: u32 = 3; const P_MODE_LO: u32 = 4; const P_MODE_HI: u32 = 5; const P_BAUD_LO: u32 = 6; const P_BAUD_HI: u32 = 7; -const P_DEST: u32 = 8; // 0=none, 1=joy, 0x81=mcd +const P_DEST: u32 = 8; const P_SDA_STATE: u32 = 9; -const P_SW_LO: u32 = 10; // u16, active-low button mask +const P_SW_LO: u32 = 10; const P_SW_HI: u32 = 11; -const P_SDA_TXR: u32 = 12; // 1 = sda still has bytes to send +const P_SDA_TXR: u32 = 12; const P_IRQ_BIT: u32 = 13; -const P_CYC_LO: u32 = 14; // u32 cycles_until_irq +const P_CYC_LO: u32 = 14; const P_CYC_HI1: u32 = 15; const P_CYC_HI2: u32 = 16; const P_CYC_HI3: u32 = 17; -// SDA protocol states (digital pad, no analog support). const SDA_HIZ: u32 = 0; const SDA_IDL: u32 = 1; const SDA_IDH: u32 = 2; const SDA_SWL: u32 = 3; const SDA_SWH: u32 = 4; -// CTRL bits. const CTRL_TXEN: u32 = 0x0001; const CTRL_JOUT: u32 = 0x0002; const CTRL_RXEN: u32 = 0x0004; @@ -60,33 +36,24 @@ const CTRL_ACKN: u32 = 0x0010; const CTRL_ACIE: u32 = 0x1000; const CTRL_SLOT: u32 = 0x2000; -// STAT bits. const STAT_TXR1: u32 = 0x0001; const STAT_RXNE: u32 = 0x0002; const STAT_TXR2: u32 = 0x0004; -const STAT_ACKL: u32 = 0x0080; // /ACK input level - controller response pulse +const STAT_ACKL: u32 = 0x0080; const STAT_IRQ7: u32 = 0x0200; -// Destinations on first TX byte. const DEST_JOY: u32 = 0x01; const DEST_MCD: u32 = 0x81; const JOY_IRQ_DELAY: u32 = 512; -// Owned PAD-state buffer (32 bytes). Auto-drops with the Bus. pub fn createPad() Vec(u8) { var b: Vec(u8) = Vec(u8).filled(0, PAD_STATE_SIZE); - // sw defaults to 0xFFFF (no buttons pressed). b[P_SW_LO] = 0xFF; b[P_SW_HI] = 0xFF; return b; } -// Update the SDA digital-pad button mask (active-low: bit clear = -// pressed). PSX bit layout: 0=Select, 3=Start, 4=Up, 5=Right, 6=Down, -// 7=Left, 8=L2, 9=R2, 10=L1, 11=R1, 12=Triangle, 13=Circle, 14=Cross, -// 15=Square. The next controller poll picks the new value up via -// sdaRead's SDA_SWL/SDA_SWH states. pub fn padSetButtons(p: *mut[] u8, mask: u32) { p[P_SW_LO] = (mask & 0xFF) as u8; p[P_SW_HI] = ((mask >> 8) & 0xFF) as u8; @@ -115,9 +82,6 @@ pub fn padSetU32(p: *mut[] u8, off: u32, v: u32) { p[off + 3] = ((v >> 24) & 0xFF) as u8; } -// SDA read - return the next byte in the digital-pad sequence and -// advance the state machine. After SWH the sequence is complete; the -// pad returns to HIZ and tx_ready clears. pub fn sdaRead(p: *mut[] u8) u32 { const st: u32 = p[P_SDA_STATE] as u32; var data: u32 = 0xFF; @@ -126,7 +90,7 @@ pub fn sdaRead(p: *mut[] u8) u32 { p[P_SDA_STATE] = SDA_IDL as u8; p[P_SDA_TXR] = 1; } else if (st == SDA_IDL) { - data = 0x41; // digital pad model + data = 0x41; p[P_SDA_STATE] = SDA_IDH as u8; p[P_SDA_TXR] = 1; } else if (st == SDA_IDH) { @@ -140,14 +104,11 @@ pub fn sdaRead(p: *mut[] u8) u32 { } else if (st == SDA_SWH) { data = p[P_SW_HI] as u32; p[P_SDA_STATE] = SDA_HIZ as u8; - p[P_SDA_TXR] = 0; // sequence complete + p[P_SDA_TXR] = 0; } return data; } -// SDA write - handles 0x43 (toggle analog config mode) by resetting the -// state machine. Other writes are ignored (the digital pad doesn't -// respond to any other commands). pub fn sdaWrite(p: *mut[] u8, data: u32) { if ((data & 0xFF) == 0x43) { p[P_SDA_STATE] = SDA_HIZ as u8; @@ -155,9 +116,6 @@ pub fn sdaWrite(p: *mut[] u8, data: u32) { } } -// padReadRx: returns the next byte the controller has staged in -// response to the last TX. When CTRL.JOUT and RXEN are -// both clear, the line is high-Z and reads return 0xFFFFFFFF. pub fn padReadRx(p: *mut[] u8, mcd: mut Mcd, mcdRam: *mut[] u8) u32 { const ctrl: u32 = padGetU16(p, P_CTRL_LO); if (p[P_DEST] == 0) { return 0xFFFFFFFF; } @@ -177,20 +135,10 @@ pub fn padReadRx(p: *mut[] u8, mcd: mut Mcd, mcdRam: *mut[] u8) u32 { return 0xFFFFFFFF; } -// pad_write_tx: route a TX byte to the active destination. The very -// first byte after a reset selects the destination (0x01=joy, 0x81=mcd). -// Subsequent bytes are routed to whatever destination was selected. If -// ACIE is enabled in CTRL, we schedule an IRQ7 ack JOY_IRQ_DELAY cycles -// out. pub fn padWriteTx(p: *mut[] u8, mcd: mut Mcd, data: u32) { const ctrl: u32 = padGetU16(p, P_CTRL_LO); if ((ctrl & CTRL_TXEN) == 0) { return; } if (p[P_DEST] == 0) { - // First TX byte selects the destination (0x01 = joy, 0x81 = mcd). - // Sets dest, schedules the ACK-pulse countdown - // if CTRL_ACIE - but does NOT set irq_bit on the first byte. The - // BIOS expects IC_JOY to assert while STAT.IRQ7 stays 0 until a - // *subsequent* byte writes through. const b: u32 = data & 0xFF; if (b == DEST_JOY || b == DEST_MCD) { p[P_DEST] = b as u8; @@ -209,14 +157,9 @@ pub fn padWriteTx(p: *mut[] u8, mcd: mut Mcd, data: u32) { } if (p[P_DEST] == DEST_JOY as u8) { sdaWrite(p, data); - // If the controller has nothing more to send, - // drop the slot so the next first byte re-selects. if (p[P_SDA_TXR] == 0) { p[P_DEST] = 0; } } else if (p[P_DEST] == DEST_MCD as u8) { mcd.write(data); - // MCD always sets irq_bit and uses a 1024- - // cycle delay (vs JOY's 512). The longer ACK reflects the MCD's - // slower response time. if ((ctrl & CTRL_ACIE) != 0) { p[P_IRQ_BIT] = 1; padSetU32(p, P_CYC_LO, 1024); @@ -233,18 +176,12 @@ pub fn padWriteTx(p: *mut[] u8, mcd: mut Mcd, data: u32) { } } -// padHandleCtrlWrite: write to JOY_CTRL. When JOUT is -// cleared, also clear the slot bit; bit 4 (ACKN) is a one-shot that -// clears STAT bits 3, 4, 5, 9 and self-clears. pub fn padHandleCtrlWrite(p: *mut[] u8, mcd: mut Mcd, value: u32) { padSetU16(p, P_CTRL_LO, value & 0xFFFF); if ((value & CTRL_JOUT) == 0) { var ctrl: u32 = padGetU16(p, P_CTRL_LO); ctrl = ctrl & (~CTRL_SLOT); padSetU16(p, P_CTRL_LO, ctrl); - // !JOUT also resets the slot's MCD state so - // the next 0x81 select starts a fresh transaction. Without this - // the memcard stays in mid-protocol after a chip-deselect. mcd.reset(); } if ((value & CTRL_ACKN) != 0) { @@ -257,14 +194,10 @@ pub fn padHandleCtrlWrite(p: *mut[] u8, mcd: mut Mcd, value: u32) { } } -// JOY_STAT is computed: low 3 bits always set (TXRDY1 | RXNE | TXRDY2); -// store sticky bits in STAT and OR them in here. pub fn padReadStat(p: *mut[] u8) u32 { return padGetU16(p, P_STAT_LO) | 0x07; } -// public IO surface - pub fn padRead32(p: *mut[] u8, mcd: mut Mcd, mcdRam: *mut[] u8, off: u32) u32 { match (off) { 0 { return padReadRx(p, mcd, mcdRam); } @@ -302,9 +235,6 @@ pub fn padWrite8(p: *mut[] u8, mcd: mut Mcd, off: u32, val: u32) { padWrite32(p, mcd, off, val & 0xFF); } -// Tick the IRQ-delay countdown. When it reaches 0 we raise IC_JOY so -// the BIOS handler advances the PAD state machine on the next IRQ -// dispatch. `cyc` is the CPU-cycle delta since the last update. pub fn padUpdate(p: *mut[] u8, ic: *mut[] u32, cop0: *mut[] u32, cyc: u32) { var cd: u32 = padGetU32(p, P_CYC_LO); if (cd == 0) { return; } @@ -313,10 +243,6 @@ pub fn padUpdate(p: *mut[] u8, ic: *mut[] u32, cop0: *mut[] u32, cyc: u32) { return; } padSetU32(p, P_CYC_LO, 0); - // Raises IC_JOY unconditionally on countdown - // expiry, and ONLY sets STAT.IRQ7 (bit 9) if irq_bit was set by - // a subsequent-byte TX. The first byte raises IC_JOY without - // latching IRQ7 - BIOS sees the ACK pulse but no data-IRQ yet. if (p[P_IRQ_BIT] != 0) { var stat: u32 = padGetU16(p, P_STAT_LO); stat = stat | STAT_IRQ7; diff --git a/sio1.jam b/sio1.jam index 57bb3b2..3fd6a0a 100644 --- a/sio1.jam +++ b/sio1.jam @@ -1,51 +1,5 @@ -// SIO1 serial port (link cable / serial-port) at 0x1F801050..0x1F80105F. -// We build the real device per psx-spx so games -// that wait on its events (Harvest Moon opens class 0xF0000009 spec 0x20 -// = SIO COMMAND_COMPLETE) can complete the SIO transaction loop. -// -// Register map: -// 0x00 SIO_DATA R/W (RX FIFO read / TX FIFO write) 8-bit -// 0x04 SIO_STAT R Status register 32-bit -// 0x08 SIO_MODE R/W Baud / parity / character length 16-bit -// 0x0A SIO_CTRL R/W Control register 16-bit -// 0x0E SIO_BAUD R/W Baud reload value 16-bit -// -// SIO_STAT bits (psx-spx): -// 0 TX_RDY1 TX FIFO not full -// 1 RX_NE RX FIFO has data -// 2 TX_RDY2 TX shift idle (TX FIFO empty) -// 3 RX_PERR parity error (sticky; cleared by CTRL.ACK) -// 4 RX_OVERR FIFO overrun (sticky) -// 5 RX_BSB bad stop bit (sticky) -// 6 RX_INPUT current SI/RX input level -// 7 DSR_LVL DSR input level (0 = no remote) -// 8 CTS_LVL CTS input level -// 9 IRQ interrupt request (set by HW, cleared by CTRL.ACK) -// 10 reserved -// 11+ baud-timer countdown bits -// -// SIO_CTRL bits: -// 0 TX_EN transmit enable -// 1 DTR_LVL DTR output level -// 2 RX_EN receive enable -// 3 SO_LVL TX manual output level (loopback) -// 4 ACK_IRQ write 1 -> clear STAT[3,4,5,9]; self-clears -// 5 RTS_LVL RTS output level -// 6 RESET master reset -// 7 reserved -// 8-9 RX_IM RX interrupt mode (bytes to receive before IRQ) -// 10 TX_IE TX interrupt enable (IRQ when TX FIFO empties) -// 11 RX_IE RX interrupt enable (IRQ at RX threshold) -// 12 DSR_IE DSR interrupt enable -// 13-15 reserved -// -// With no link cable connected, every TX byte echoes 0xFF onto the RX -// line after a short delay; if the appropriate CTRL.x_IE bit is set, we -// raise IC_SIO. - const { irqRaise, IC_SIO } = import("irq"); -// CTRL bits. const CTRL_TXEN: u32 = 0x0001; const CTRL_DTR: u32 = 0x0002; const CTRL_RXEN: u32 = 0x0004; @@ -55,7 +9,6 @@ const CTRL_TX_IE: u32 = 0x0400; const CTRL_RX_IE: u32 = 0x0800; const CTRL_DSR_IE: u32 = 0x1000; -// STAT bits. const STAT_TXR1: u32 = 0x0001; const STAT_RXNE: u32 = 0x0002; const STAT_TXR2: u32 = 0x0004; @@ -64,21 +17,8 @@ const STAT_OVERR: u32 = 0x0010; const STAT_BSB: u32 = 0x0020; const STAT_IRQ: u32 = 0x0200; -// IRQ delay (CPU cycles). 200 ≈ ~6 µs at 33 MHz, plausible for one byte -// at link-cable baud rates. Real hardware delay depends on SIO_BAUD; for -// games that only use SIO1 as a presence probe the exact value doesn't -// matter as long as IRQ eventually fires. const SIO_IRQ_DELAY: u32 = 200; -// Aggregate SIO1 state. The RX FIFO is an 8-byte ring buffer with -// 8-bit head/tail indexes (wrap is `& 0x07` for indexing; the indexes -// themselves are u8 and we compare them with the empty test -// `head == tail`). -// -// `pad` pushes the struct above kByValueMaxBytes (16), forcing the -// ByPointer ABI - `mut Sio1` parameters take a pointer to live -// storage so writes propagate. Otherwise an undersized struct would -// be passed by value and mutations would land in a snapshot. pub const Sio1 = struct { stat: u32, mode: u16, @@ -121,10 +61,6 @@ pub const Sio1 = struct { return self.rxHead == self.rxTail; } - // STAT compose: internal field carries the sticky/irq bits; on - // read we OR in the dynamic bits - TX_RDY1 always 1 (FIFO never - // full since we transmit immediately), TX_RDY2 = 1 when no TX in - // flight, RX_NE = 1 when RX FIFO has data. pub fn readStat(self: Self) u32 { var stat: u32 = self.stat; stat = stat | STAT_TXR1; @@ -133,27 +69,16 @@ pub const Sio1 = struct { return stat; } - // SIO_DATA write: push a byte through the transmit path. If - // CTRL.TX_EN is clear, drop. Otherwise schedule the IRQ deadline; - // with no remote link cable the response byte 0xFF is staged in - // the RX FIFO. pub fn writeData(self: mut Self, val: u32) { if ((self.ctrl as u32 & CTRL_TXEN) == 0) { return; } - // Stage the loopback response immediately (real HW transmits - // then latches the line level into RX; we just push 0xFF on - // the line since DSR is low). self.rxPush(0xFF); self.txPending = 1; self.cyclesUntilIrq = SIO_IRQ_DELAY; } - // SIO_CTRL write. RESET clears the buffer entirely; ACK clears - // the sticky error bits and IRQ. TX_EN / RX_EN flips just gate - // transmission. pub fn writeCtrl(self: mut Self, val: u32) { const v: u32 = val & 0xFFFF; if ((v & CTRL_RESET) != 0) { - // Preserve MODE/BAUD, clear everything else. const mode: u16 = self.mode; const baud: u16 = self.baud; self.stat = 0; @@ -169,10 +94,7 @@ pub const Sio1 = struct { } self.ctrl = v as u16; if ((v & CTRL_ACKN) != 0) { - // Clear sticky STAT bits PERR/OVERR/BSB/IRQ. self.stat = self.stat & (~(STAT_PERR | STAT_OVERR | STAT_BSB | STAT_IRQ)); - // Self-clear the ACK bit so a re-read of CTRL doesn't - // keep signaling. self.ctrl = self.ctrl & ((~CTRL_ACKN) as u16); } } @@ -204,10 +126,6 @@ pub const Sio1 = struct { pub fn write16(self: mut Self, off: u32, val: u32) { self.write32(off, val & 0xFFFF); } pub fn write8(self: mut Self, off: u32, val: u32) { self.write32(off, val & 0xFF); } - // Per-cycle tick. Mirrors pad's pattern: count down cycles_until_irq, - // then raise STAT.IRQ and the IC_SIO line. We respect the - // CTRL.x_IE bits so a game that didn't enable interrupts doesn't - // see spurious IRQs. pub fn update(self: mut Self, ic: *mut[] u32, cop0: *mut[] u32, cyc: u32) { if (self.cyclesUntilIrq == 0) { return; } if (self.cyclesUntilIrq > cyc) { @@ -217,8 +135,6 @@ pub const Sio1 = struct { self.cyclesUntilIrq = 0; self.txPending = 0; const ctrl: u32 = self.ctrl as u32; - // TX completion triggers if TX_IE or RX_IE is enabled (the - // response byte we staged in writeData is "received"). if ((ctrl & (CTRL_TX_IE | CTRL_RX_IE)) != 0) { self.stat = self.stat | STAT_IRQ; irqRaise(ic, cop0, IC_SIO); diff --git a/spu.jam b/spu.jam index d3e25d0..aceb8b7 100644 --- a/spu.jam +++ b/spu.jam @@ -1,20 +1,3 @@ -// SPU - Sound Processing Unit. -// -// Layout of the byte buffer returned by createSpu: -// 0x000..0x3FF : register file (the voice[24] register slots + globals) -// 0x400.. : 24 × VS_SIZE per-voice runtime data (playing, counter, -// current_addr, ADSR state, decoded buf[28], etc.) -// 0x1C00.. : global runtime state (taddr, tfifo, even_cycle, revbaddr) -// -// Not yet wired up right now: -// - No reverb (the whole reverb-sample path is skipped) -// - No Gaussian interpolation (nearest-neighbor: `out = s[0]`) -// - No SDL audio callback yet - spuGetSample is exported but unused -// -// What is in: ADPCM block decoder, ADSR (Attack/Decay/Sustain/Release), -// KON/KOFF, ENDX, voice mixing, master volume. Enough to produce real -// samples; we'll wire SDL audio + reverb after this lands clean. - const { irqRaise, IC_SPU } = import("irq"); const { Vec } = import("std/collections"); @@ -23,21 +6,14 @@ const SPU_REG_SIZE: u32 = 0x400; const VS_SIZE: u32 = 256; const VOICE_COUNT: u32 = 24; const VOICE_BASE: u32 = 0x400; -const GLOBAL_BASE: u32 = 0x400 + 24 * 256; // 0x1C00 -// CD-audio FIFO for XA samples decoded by cdrom.jam. Producer = cdrom -// XA path, consumer = spuGetSample (one stereo sample per call). 1024 -// stereo entries × 4 bytes = 4 KB, lives at 0x1D00. +const GLOBAL_BASE: u32 = 0x400 + 24 * 256; const CD_FIFO_BASE: u32 = 0x1D00; -const CD_FIFO_CAP: u32 = 1024; // entries (power of two) +const CD_FIFO_CAP: u32 = 1024; const CD_FIFO_MASK: u32 = 1023; -// Gaussian-interpolation coefficient table - 512 i16 entries = -// 1024 bytes. The standard PSX Gaussian table. Populated by -// spuGaussInit at createSpu time so we don't need a runtime fopen. const SPU_GAUSS_OFF: u32 = 0x6D00; -const SPU_STATE_SIZE: u32 = 0x6D00 + 1024; // 0x7100 +const SPU_STATE_SIZE: u32 = 0x6D00 + 1024; const SPU_RAM_SIZE: u32 = 0x80000; -// Voice-register offsets inside a 16-byte voice slot. const VR_VOLUMEL: u32 = 0x0; const VR_VOLUMER: u32 = 0x2; const VR_ADSAMPR: u32 = 0x4; @@ -47,7 +23,6 @@ const VR_ENVCTL2: u32 = 0xA; const VR_ENVCVOL: u32 = 0xC; const VR_ADRADDR: u32 = 0xE; -// Global register offsets (relative to 0x1F801C00). const R_MAINLVOL: u32 = 0x180; const R_MAINRVOL: u32 = 0x182; const R_VLOUT: u32 = 0x184; @@ -56,10 +31,10 @@ const R_KONL: u32 = 0x188; const R_KONH: u32 = 0x18A; const R_KOFFL: u32 = 0x18C; const R_KOFFH: u32 = 0x18E; -const R_PMONL: u32 = 0x190; // pitch-modulation enable (voices 1..15) -const R_PMONH: u32 = 0x192; // (voices 16..23) -const R_NONL: u32 = 0x194; // noise-mode enable (voices 0..15) -const R_NONH: u32 = 0x196; // (voices 16..23) +const R_PMONL: u32 = 0x190; +const R_PMONH: u32 = 0x192; +const R_NONL: u32 = 0x194; +const R_NONH: u32 = 0x196; const R_EONL: u32 = 0x198; const R_EONH: u32 = 0x19A; const R_ENDXL: u32 = 0x19C; @@ -74,10 +49,6 @@ const R_SPUSTAT: u32 = 0x1AE; const R_CDVOLL: u32 = 0x1B0; const R_CDVOLR: u32 = 0x1B2; -// Reverb work-area registers (nocash psx-spx). All i16 values, two -// flavours: 'd*' / 'm*' are byte offsets into SPU RAM (multiplied by 8 -// in the reverb-sample path) and 'v*' are i16 fixed-point -// coefficients in the range [-0x8000, 0x7FFF]. const R_DAPF1: u32 = 0x1C0; const R_DAPF2: u32 = 0x1C2; const R_VIIR: u32 = 0x1C4; @@ -111,7 +82,6 @@ const R_MRAPF2: u32 = 0x1FA; const R_VLIN: u32 = 0x1FC; const R_VRIN: u32 = 0x1FE; -// Per-voice runtime offsets (added to VOICE_BASE + v * VS_SIZE). const D_PLAYING: u32 = 0x00; const D_COUNTER: u32 = 0x04; const D_CURADDR: u32 = 0x08; @@ -122,10 +92,10 @@ const D_S1: u32 = 0x18; const D_S2: u32 = 0x1C; const D_S3: u32 = 0x20; const D_BFLAGS: u32 = 0x24; -const D_BUF0: u32 = 0x28; // 28 × i16 -> 56 bytes through 0x60 +const D_BUF0: u32 = 0x28; const D_H0: u32 = 0x60; const D_H1: u32 = 0x64; -const D_LVOL: u32 = 0x68; // kept as i32 (volumel); a float would also work +const D_LVOL: u32 = 0x68; const D_RVOL: u32 = 0x6C; const D_CVOL: u32 = 0x70; const D_APHASE: u32 = 0x74; @@ -139,34 +109,24 @@ const D_APENDSTEP: u32 = 0x90; const D_ASUSLVL: u32 = 0x94; const D_ENVCTL: u32 = 0x98; -// Global runtime offsets (added to GLOBAL_BASE). const G_TADDR: u32 = 0x00; -const G_TFIFO: u32 = 0x04; // 32 × u16 = 64 bytes -> through 0x44 +const G_TFIFO: u32 = 0x04; const G_TFIFO_IDX: u32 = 0x44; const G_EVEN_CYC: u32 = 0x48; const G_REVBADDR: u32 = 0x4C; const G_LRSL: u32 = 0x50; const G_LRSR: u32 = 0x54; -// CD-audio FIFO head/tail. 32-bit ring indices that mod-mask down to -// CD_FIFO_MASK for the slot. Producer-only writes head; consumer-only -// writes tail. Equal head == tail -> empty. const G_CD_HEAD: u32 = 0x60; const G_CD_TAIL: u32 = 0x64; -// Noise generator state (free space before CD_FIFO_BASE=0x1D00): the 16-bit -// LFSR level and the 32-bit fractional clock accumulator. Ticked once per -// output sample by spuStepNoise; voices with their NON bit set output the -// LFSR level instead of their ADPCM sample. const G_NOISE_LEVEL: u32 = 0x70; const G_NOISE_COUNT: u32 = 0x74; -// ADSR phases. const ADSR_ATTACK: u32 = 0; const ADSR_DECAY: u32 = 1; const ADSR_SUSTAIN: u32 = 2; const ADSR_RELEASE: u32 = 3; const ADSR_END: u32 = 4; -// ADPCM filter coefficient tables. const ADPCM_POS_0: i32 = 0; const ADPCM_POS_1: i32 = 60; const ADPCM_POS_2: i32 = 115; @@ -196,11 +156,6 @@ pub fn adpcmNeg(f: u32) i32 { } } -// noise generator (Avocado/DuckStation/nocash) -// -// SPUCNT bits 8-13 are the noise clock: shift = bits 10-13, step = bits 8-9. -// noiseFreqAdd is the {0,84,140,180,210} half-cycle table (index 4 = 210 is -// the threshold, inlined below). fn noiseFreqAdd(step: u32) u32 { match (step) { 1 { return 84; } @@ -210,17 +165,12 @@ fn noiseFreqAdd(step: u32) u32 { } } -// The 64-entry noise waveform LUT, indexed by (level >> 10) & 0x3F. It is two -// 32-entry halves: the low half repeats {1,0,0,1,0,1,1,0} (bitmask 0x69 per -// 8 entries), the high half its complement (0x96). fn noiseWaveAdd(idx: u32) u32 { const j: u32 = idx & 7; if ((idx & 0x20) != 0) { return (0x96 >> j) & 1; } return (0x69 >> j) & 1; } -// Advance the LFSR once per output sample. freq = (0x8000>>shift)<<16 is -// always a power of two, so the modulo is a mask. fn spuStepNoise(s: *mut[] u8, spucnt: u32) { const shift: u32 = (spucnt >> 10) & 0xF; const step: u32 = (spucnt >> 8) & 3; @@ -239,11 +189,6 @@ fn spuStepNoise(s: *mut[] u8, spucnt: u32) { setU32(s, GLOBAL_BASE + G_NOISE_COUNT, count); } -// allocation - -// Owned SPU-state buffer (~29KB). Initialised via setU16 + spuGaussInit -// (reset values: all voices ended, irq9addr = 0xFFFF). Auto-drops -// with the Bus. pub fn createSpu() Vec(u8) { var b: Vec(u8) = Vec(u8).filled(0, SPU_STATE_SIZE); var p: *mut[] u8 = b.ptr; @@ -254,52 +199,28 @@ pub fn createSpu() Vec(u8) { return b; } -// Owned SPU sample-RAM buffer (512 KB), zero-initialised. Auto-drops -// with the Bus. pub fn createSpuRam() Vec(u8) { return Vec(u8).filled(0, SPU_RAM_SIZE); } -// CD-audio FIFO -// -// XA samples decoded by cdrom.jam land here as packed (left, right) i16 -// pairs. spuGetSample drains one entry per call and adds it (volume- -// scaled by R_CDVOLL / R_CDVOLR) into the main mix. Producer drops -// samples on full to keep the consumer in lock-step with the output -// rate - favouring "current" audio over a runaway backlog. - pub fn spuPushCdSample(s: *mut[] u8, leftI16: u32, rightI16: u32) { const head: u32 = getU32(s, GLOBAL_BASE + G_CD_HEAD); const tail: u32 = getU32(s, GLOBAL_BASE + G_CD_TAIL); - if ((head - tail) >= CD_FIFO_CAP) { return; } // full -> drop + if ((head - tail) >= CD_FIFO_CAP) { return; } const slot: u32 = CD_FIFO_BASE + ((head & CD_FIFO_MASK) << 2); setU16(s, slot, leftI16 & 0xFFFF); setU16(s, slot + 2, rightI16 & 0xFFFF); setU32(s, GLOBAL_BASE + G_CD_HEAD, head + 1); } -// reverb -// -// PSX reverb is a 22-tap filter living in the tail of SPU RAM starting -// at MBASE×8. The work-area is referenced through a rotating cursor -// (revbaddr) that wraps within [mbase, 0x80000). All 32 control regs -// (vIIR/vCOMB1..4/dAPF1/.../mLAPF2/...) are written by the CPU into -// the SPU register file at 0x1C0..0x1FF - we just read them back here -// and feed the filter. - -// Saturate a 64-bit accumulator into a signed 16-bit value. pub fn sat16i32(v: i32) i32 { if (v < -32768) { return -32768; } if (v > 32767) { return 32767; } return v; } -// Multiply 16-bit signed by 16-bit signed coef and shift down by 15 -// (== / 32768). This is the standard (vXxx * sample) / 32768 idiom. pub fn mulVol(v: i32, vol: i32) i32 { return (v * vol) >> 15; } -// Read one i16 from the reverb work area at (mbase + addr + revbaddr) -// mod (0x80000 - mbase), then masked to 0x7FFFE for halfword alignment. pub fn spuReadReverb(s: *mut[] u8, ram: *mut[] u8, addr: u32) i32 { const mbase: u32 = getU16(s, R_MBASE) << 3; const revb: u32 = getU32(s, GLOBAL_BASE + G_REVBADDR); @@ -324,15 +245,8 @@ pub fn spuWriteReverb(s: *mut[] u8, ram: *mut[] u8, addr: u32, v: i32) { ram[wrapped + 1] = ((w >> 8) & 0xFF) as u8; } -// Run one reverb sample. inL/inR are the SPU's pre-reverb voice mix. -// Returns packed (outL | outR<<16) - both already volume-scaled by -// vLOUT / vROUT. Called only when SPUCNT.bit7 is set (reverb master -// enable) and on the alternating "even cycle" - the reverb-sample -// computation is gated on the even cycle. pub fn spuGetReverbSample(s: *mut[] u8, ram: *mut[] u8, inL: i32, inR: i32) u32 { - // All d*/m* values are <<3 to convert from "halfword index" to - // byte offset (the `<< 3`). const dapf1: u32 = (getU16(s, R_DAPF1)) << 3; const dapf2: u32 = (getU16(s, R_DAPF2)) << 3; const mlsame: u32 = (getU16(s, R_MLSAME)) << 3; @@ -372,7 +286,6 @@ pub fn spuGetReverbSample(s: *mut[] u8, ram: *mut[] u8, const lin: i32 = mulVol(inL, vlin); const rin: i32 = mulVol(inR, vrin); - // Same-side reflection (L->L, R->R). const sLsamePrev: i32 = spuReadReverb(s, ram, mlsame - 2); const sRsamePrev: i32 = spuReadReverb(s, ram, mrsame - 2); const sLsameNow: i32 = sat16i32(lin @@ -386,7 +299,6 @@ pub fn spuGetReverbSample(s: *mut[] u8, ram: *mut[] u8, spuWriteReverb(s, ram, mlsame, sLsameNow); spuWriteReverb(s, ram, mrsame, sRsameNow); - // Cross-side reflection (L->R, R->L). const sLdiffPrev: i32 = spuReadReverb(s, ram, mldiff - 2); const sRdiffPrev: i32 = spuReadReverb(s, ram, mrdiff - 2); const sLdiffNow: i32 = sat16i32(lin @@ -400,7 +312,6 @@ pub fn spuGetReverbSample(s: *mut[] u8, ram: *mut[] u8, spuWriteReverb(s, ram, mldiff, sLdiffNow); spuWriteReverb(s, ram, mrdiff, sRdiffNow); - // Early-echo (4-tap comb). var l: i32 = sat16i32( mulVol(spuReadReverb(s, ram, mlcomb1), vcomb1) + mulVol(spuReadReverb(s, ram, mlcomb2), vcomb2) @@ -412,7 +323,6 @@ pub fn spuGetReverbSample(s: *mut[] u8, ram: *mut[] u8, + mulVol(spuReadReverb(s, ram, mrcomb3), vcomb3) + mulVol(spuReadReverb(s, ram, mrcomb4), vcomb4)); - // Late reverb - all-pass filter 1. l = sat16i32(l - sat16i32(mulVol(spuReadReverb(s, ram, mlapf1 - dapf1), vapf1))); r = sat16i32(r - sat16i32(mulVol(spuReadReverb(s, ram, mrapf1 - dapf1), vapf1))); spuWriteReverb(s, ram, mlapf1, l); @@ -420,7 +330,6 @@ pub fn spuGetReverbSample(s: *mut[] u8, ram: *mut[] u8, l = sat16i32(mulVol(l, vapf1) + spuReadReverb(s, ram, mlapf1 - dapf1)); r = sat16i32(mulVol(r, vapf1) + spuReadReverb(s, ram, mrapf1 - dapf1)); - // Late reverb - all-pass filter 2. l = sat16i32(l - sat16i32(mulVol(spuReadReverb(s, ram, mlapf2 - dapf2), vapf2))); r = sat16i32(r - sat16i32(mulVol(spuReadReverb(s, ram, mrapf2 - dapf2), vapf2))); spuWriteReverb(s, ram, mlapf2, l); @@ -428,11 +337,9 @@ pub fn spuGetReverbSample(s: *mut[] u8, ram: *mut[] u8, l = sat16i32(mulVol(l, vapf2) + spuReadReverb(s, ram, mlapf2 - dapf2)); r = sat16i32(mulVol(r, vapf2) + spuReadReverb(s, ram, mrapf2 - dapf2)); - // Output stage - scale by vLOUT/vROUT. const outL: i32 = sat16i32(mulVol(l, vlout)); const outR: i32 = sat16i32(mulVol(r, vrout)); - // Advance the rotating reverb cursor by 2 bytes (one i16). const mbase: u32 = getU16(s, R_MBASE) << 3; var revb: u32 = getU32(s, GLOBAL_BASE + G_REVBADDR); revb = (revb + 2) & 0x7FFFE; @@ -442,8 +349,6 @@ pub fn spuGetReverbSample(s: *mut[] u8, ram: *mut[] u8, return ((outL as u32) & 0xFFFF) | (((outR as u32) & 0xFFFF) << 16); } -// Pop one stereo sample. Returns packed left|right<<16; on empty -// returns 0 (both channels silent). pub fn spuPopCdSample(s: *mut[] u8) u32 { const head: u32 = getU32(s, GLOBAL_BASE + G_CD_HEAD); const tail: u32 = getU32(s, GLOBAL_BASE + G_CD_TAIL); @@ -455,8 +360,6 @@ pub fn spuPopCdSample(s: *mut[] u8) u32 { return l | (r << 16); } -// byte-buffer access helpers - pub fn getU16(s: *mut[] u8, off: u32) u32 { return (s[off] as u32) | ((s[off + 1] as u32) << 8); } @@ -504,9 +407,6 @@ pub fn sat16(v: i32) i32 { return v; } -// Wrap an i32 to int16 (low 16 bits, sign-extended). The ADPCM -// reconstruction assigns the sum to an int16_t, which TRUNCATES (wraps) -// rather than saturates (a b) { return a; } return b; } -// ADPCM block decoder - pub fn spuReadBlock(s: *mut[] u8, ram: *mut[] u8, v: u32) { const base: u32 = voiceRTBase(v); const addr: u32 = getU32(s, base + D_CURADDR); @@ -540,12 +438,10 @@ pub fn spuReadBlock(s: *mut[] u8, ram: *mut[] u8, v: u32) { while (j < 28) { const byte: u32 = ram[addr + 2 + (j >> 1)] as u32; const nibble: u32 = (byte >> ((j & 1) * 4)) & 0xF; - // Sign-extend the 4-bit nibble. var t: i32 = nibble as i32; if ((nibble & 0x8) != 0) { t = t - 16; } const tShifted: i32 = t << shift; const pred: i32 = ((h0 * f0) + (h1 * f1) + 32) / 64; - // wraps to int16 here (not saturates) - see wrap16. const sample: i32 = wrap16(tShifted + pred); h1 = h0; @@ -558,8 +454,6 @@ pub fn spuReadBlock(s: *mut[] u8, ram: *mut[] u8, v: u32) { setI32(s, base + D_H1, h1); } -// ADSR - pub fn adsrCalcValues(s: *mut[] u8, v: u32) { const base: u32 = voiceRTBase(v); const shift: i32 = getI32(s, base + D_ASHIFT); @@ -661,17 +555,13 @@ pub fn spuHandleAdsr(s: *mut[] u8, v: u32) { setU32(s, base + D_PLAYING, 0); } } else { - // ADSR_END setU32(s, base + D_PLAYING, 0); } - // Mirror the envelope volume into the voice register so games can read it. setU16(s, voiceRegBase(v) + VR_ENVCVOL, getI32(s, base + D_CVOL) as u32); setI32(s, base + D_ACYCLES, getI32(s, base + D_ACYRELOAD)); } -// KON / KOFF - pub fn spuKon(s: *mut[] u8, ram: *mut[] u8, value: u32) { var i: u32 = 0; while (i < VOICE_COUNT) { @@ -681,11 +571,6 @@ pub fn spuKon(s: *mut[] u8, ram: *mut[] u8, value: u32) { setU32(s, rt + D_PLAYING, 1); setU32(s, rt + D_CURADDR, getU16(s, vr + VR_ADSADDR) << 3); setU32(s, rt + D_REPADDR, getU16(s, vr + VR_ADRADDR) << 3); - // Store volumes as signed i32 (a float would also work; the - // multiplier matters more than the type). The volume scales by x2: - // lvol = (volumel / 32767) * 2. Bake the x2 - // in here so the >>15 sample path matches reference amplitude - // (without it every voice plays ~6 dB too quiet). setI32(s, rt + D_LVOL, getI16(s, vr + VR_VOLUMEL) * 2); setI32(s, rt + D_RVOL, getI16(s, vr + VR_VOLUMER) * 2); setI32(s, rt + D_ASUSLVL, @@ -711,8 +596,6 @@ pub fn spuKoff(s: *mut[] u8, value: u32) { } } -// write-side effects - pub fn spuHandleWrite(s: *mut[] u8, ram: *mut[] u8, off: u32, val: u32) i32 { if (off == R_KONL || off == R_KONH) { if (val == 0) { return 1; } @@ -759,12 +642,6 @@ pub fn spuHandleWrite(s: *mut[] u8, ram: *mut[] u8, off: u32, val: u32) i32 { var stat: u32 = getU16(s, R_SPUSTAT); stat = (stat & 0xFFC0) | (val & 0x3F); setU16(s, R_SPUSTAT, stat); - // Drain any pending TFIFO data on every SPUCNT write - covers both - // a flush on entering an active mode AND the - // hardware-spec behavior (flush on transition to Stop). Critically, - // this puts the flush BEFORE the next setStartAddress in - // setupDMARead, so data lands at the original TADDR (ps1-tests - // spu/memory-transfer testDMAWriteToSpuRam). if (true) { var ta: u32 = getU32(s, GLOBAL_BASE + G_TADDR); const idx: u32 = getU32(s, GLOBAL_BASE + G_TFIFO_IDX); @@ -789,8 +666,6 @@ pub fn spuHandleWrite(s: *mut[] u8, ram: *mut[] u8, off: u32, val: u32) i32 { return 0; } -// read TFIFO - pub fn spuReadTfifo(s: *mut[] u8, ram: *mut[] u8) u32 { var ta: u32 = getU32(s, GLOBAL_BASE + G_TADDR); const lo: u32 = ram[ta] as u32; @@ -799,8 +674,6 @@ pub fn spuReadTfifo(s: *mut[] u8, ram: *mut[] u8) u32 { return lo | (hi << 8); } -// bus dispatch - pub fn spuRead8(s: *mut[] u8, ram: *mut[] u8, off: u32) u32 { if (off >= SPU_REG_SIZE) { return 0; } return s[off] as u32; @@ -825,7 +698,6 @@ pub fn spuWrite8(s: *mut[] u8, ram: *mut[] u8, off: u32, val: u32) { pub fn spuWrite16(s: *mut[] u8, ram: *mut[] u8, off: u32, val: u32) { if (off + 1 >= SPU_REG_SIZE) { return; } if (spuHandleWrite(s, ram, off, val & 0xFFFF) != 0) { return; } - // Skip writes at offset 0x0C inside the voice slot (volumer mirror). if ((off & 0xF) == 0xC && off < 0x180) { return; } setU16(s, off, val & 0xFFFF); } @@ -840,8 +712,6 @@ pub fn spuWrite32(s: *mut[] u8, ram: *mut[] u8, off: u32, val: u32) { setU16(s, off + 2, (val >> 16) & 0xFFFF); } -// sample mixer - pub fn spuGetSample(s: *mut[] u8, ram: *mut[] u8, ic: *mut[] u32, cop0: *mut[] u32) u32 { const even: u32 = getU32(s, GLOBAL_BASE + G_EVEN_CYC) ^ 1; @@ -850,26 +720,19 @@ pub fn spuGetSample(s: *mut[] u8, ram: *mut[] u8, var left: i32 = 0; var right: i32 = 0; - // KON/KOFF are one-shot - zero them after dispatch each tick. setU16(s, R_KONL, 0); setU16(s, R_KONH, 0); setU16(s, R_KOFFL, 0); setU16(s, R_KOFFH, 0); - // Per-voice mode bitmasks (pitch-mod / noise / reverb), read once. const pmon: u32 = getU16(s, R_PMONL) | (getU16(s, R_PMONH) << 16); const non: u32 = getU16(s, R_NONL) | (getU16(s, R_NONH) << 16); const eon: u32 = getU16(s, R_EONL) | (getU16(s, R_EONH) << 16); - // Advance the noise LFSR once per output sample; noise-mode voices emit - // this level (read sign-extended for use as a sample). const cnt0: u32 = getU16(s, R_SPUCNT); spuStepNoise(s, cnt0); const noiseLvl: i32 = getI16(s, GLOBAL_BASE + G_NOISE_LEVEL); - // Reverb input accumulators - only EON voices feed the reverb. - // prevOut carries the prior voice's ADSR-scaled output - // for pitch modulation. var revL: i32 = 0; var revR: i32 = 0; var prevOut: i32 = 0; @@ -912,14 +775,12 @@ pub fn spuGetSample(s: *mut[] u8, ram: *mut[] u8, setU16(s, vr + VR_ENVCVOL, 0); adsrLoadRelease(s, v); } else { - // lp == 3 setEndx(s, endx(s) | (1 << v)); setU32(s, rt + D_CURADDR, getU32(s, rt + D_REPADDR)); } spuReadBlock(s, ram, v); } - // Shuffle history if sample index advanced. const prev: u32 = getU32(s, rt + D_PREVSI); if (prev != sampleIdx) { setI32(s, rt + D_S3, getI32(s, rt + D_S2)); @@ -928,7 +789,6 @@ pub fn spuGetSample(s: *mut[] u8, ram: *mut[] u8, } const cur: i32 = getI16(s, rt + D_BUF0 + sampleIdx * 2); setI32(s, rt + D_S0, cur); - // 4-tap Gaussian interpolation. const gIdx: u32 = (counter >> 4) & 0xFF; const g0: i32 = spuGaussLookup(s, 0x0FF - gIdx); const g1: i32 = spuGaussLookup(s, 0x1FF - gIdx); @@ -939,39 +799,28 @@ pub fn spuGetSample(s: *mut[] u8, ram: *mut[] u8, out = out + ((g2 * getI32(s, rt + D_S1)) >> 15); out = out + ((g3 * cur) >> 15); - // Noise-mode voices output the LFSR level instead of the sample. if (((non >> v) & 1) != 0) { out = noiseLvl; } const envc: i32 = getI16(s, vr + VR_ENVCVOL); const lvol: i32 = getI32(s, rt + D_LVOL); const rvol: i32 = getI32(s, rt + D_RVOL); - // Conceptually: samplel = (out * lvol) * (envcvol / 32767) - // i64 fused form: (out * vol * envcvol) >> 30 in one step. A - // single float multiply would also work; doing it in i64 avoids - // the precision loss of truncating after an intermediate >>15. const sl: i32 = (((out as i64) * (lvol as i64) * (envc as i64)) >> 30) as i32; const sr: i32 = (((out as i64) * (rvol as i64) * (envc as i64)) >> 30) as i32; left = left + sl; right = right + sr; - // EON voices also feed the reverb input. if (((eon >> v) & 1) != 0) { revL = revL + sl; revR = revR + sr; } var step: u32 = getU16(s, vr + VR_ADSAMPR); - // Pitch modulation: scale the step by the previous voice's - // ADSR-scaled output (DuckStation/Avocado). Voice 0 has no - // predecessor, so PMON has no effect there. if (v > 0 && ((pmon >> v) & 1) != 0) { const factor: i32 = prevOut + 0x8000; var sStep: i32 = step as i32; if ((step & 0x8000) != 0) { sStep = (step | 0xFFFF0000) as i32; } step = ((((sStep as i64) * (factor as i64)) >> 15) as u32) & 0xFFFF; } - // Clamp the pitch step to 0x3FFF (all four reference emulators). if (step > 0x3FFF) { step = 0x3FFF; } setU32(s, rt + D_PREVSI, counter >> 12); setU32(s, rt + D_COUNTER, counter + step); - // This voice's ADSR-scaled output, for the next voice's PMON. thisMod = (((out as i64) * (envc as i64)) >> 15) as i32; } prevOut = thisMod; @@ -980,15 +829,10 @@ pub fn spuGetSample(s: *mut[] u8, ram: *mut[] u8, const spucnt: u32 = getU16(s, R_SPUCNT); if ((spucnt & 0x4000) == 0) { - // SPU disabled - still drain the CD FIFO so the producer - // doesn't back up, and return silence to the audio callback. const _drain: u32 = spuPopCdSample(s); return 0; } - // Mix one CD-audio sample into the voice sum. SPUCNT bit 0 = CD audio - // enable; when clear, the FIFO is drained (so producer doesn't back - // up) but the sample is muted before scaling. const cd: u32 = spuPopCdSample(s); if ((spucnt & 0x0001) != 0) { var cdl: i32 = (cd & 0xFFFF) as i32; @@ -1001,20 +845,12 @@ pub fn spuGetSample(s: *mut[] u8, ram: *mut[] u8, const cdMixR: i32 = (cdr * cdvR) >> 15; left = left + cdMixL; right = right + cdMixR; - // CD audio also feeds the reverb input when SPUCNT bit 2 is set. if ((spucnt & 0x0004) != 0) { revL = revL + cdMixL; revR = revR + cdMixR; } } - // Reverb pass - gated on SPUCNT.bit 7 (REVERB master enable). The - // filter chews two samples per turn (the rotating revbaddr advances - // by 2 bytes per call) so it only fires on the even cycle and - // caches the result through to the odd cycle. We do the same here: - // recompute lrsl/lrsr when even, reuse the cached value otherwise. if ((spucnt & 0x0080) != 0) { const even: u32 = getU32(s, GLOBAL_BASE + G_EVEN_CYC); if (even != 0) { - // Reverb input is the EON-voice sum (+ CD when enabled), not the - // full mix - matches Avocado/DuckStation. const sl16: i32 = sat16(revL); const sr16: i32 = sat16(revR); const rv: u32 = spuGetReverbSample(s, ram, sl16, sr16); @@ -1037,15 +873,7 @@ pub fn spuGetSample(s: *mut[] u8, ram: *mut[] u8, return packed; } -// per-scanline tick -// -// PS1 CPU is 33.8688 MHz, SPU runs at 44.1 kHz, so 1 sample ≈ 768 CPU -// cycles. We accumulate cycles and drain one spuGetSample per bucket. -// This advances ADSR + voice playback at the correct rate; ENDX flips -// naturally when a voice's release finishes. Samples are discarded for -// now - actual audio output needs an SDL audio device. - -const G_SAMPLE_ACC: u32 = 0x58; // u32, after G_LRSR +const G_SAMPLE_ACC: u32 = 0x58; const CYC_PER_SAMPLE: u32 = 768; pub fn spuUpdate(s: *mut[] u8, ram: *mut[] u8, @@ -1058,19 +886,6 @@ pub fn spuUpdate(s: *mut[] u8, ram: *mut[] u8, setU32(s, GLOBAL_BASE + G_SAMPLE_ACC, acc); } -// Gaussian interpolation -// -// Replaces the previous nearest-neighbor sample lookup with the 4-tap -// Gaussian filter PSX hardware uses. Lookup table lives at offset -// SPU_GAUSS_OFF inside the SPU state buffer, populated once at -// createSpu via spuGaussInit. Per-sample math: -// -// idx = (counter >> 4) & 0xff -// out = (table[0x0FF - idx] * s3 -// + table[0x1FF - idx] * s2 -// + table[0x100 + idx] * s1 -// + table[0x000 + idx] * s0) >> 15 - pub fn spuGaussRow(s: *mut[] u8, off: u32, a: i32, b: i32, c: i32, d: i32, e: i32, f: i32, g: i32, h: i32) { @@ -1151,9 +966,3 @@ pub fn spuGaussInit(s: *mut[] u8) { spuGaussRow(s, 496, 0x593A, 0x5949, 0x5958, 0x5965, 0x5971, 0x597C, 0x5986, 0x598F); spuGaussRow(s, 504, 0x5997, 0x599E, 0x59A4, 0x59A9, 0x59AD, 0x59B0, 0x59B2, 0x59B3); } - -// NOTE: the SDL audio callback (psone_audio_cb) lives in sdl.jam now. -// The SPU is clocked CPU-synchronously from runOneFrame (one sample -// per 768 cycles) for deterministic envelope state; those samples are -// pushed into an SPSC ring buffer that the callback drains. The SPU -// has no audio-thread coupling anymore. diff --git a/tests.jam b/tests.jam index 6c30811..8a6b193 100644 --- a/tests.jam +++ b/tests.jam @@ -1,9 +1,3 @@ -// CPU + bus unit tests. -// -// `jam.out test tests.jam` discovers every `tfn` and runs it against -// hand-assembled MIPS programs staged into RAM. Lives in its own file -// so test builds don't have to link SDL2. - const { assert } = import("test"); const { @@ -22,8 +16,6 @@ const { encR, encI, encJ } = import("cpu"); -// Force device modules into this module's codegen context so bus.jam's -// createBus body (which calls Gpu.init / createDma / etc.) resolves. const { Gpu } = import("gpu"); const { createGte, gteExec, gteDivide, gteDataRead, gteDataWrite, gteCtrlWrite, gteCtrlRead, @@ -40,14 +32,11 @@ const { Timer } = import("timer"); const { Mcd } = import("mcd"); const { Mdec } = import("mdec"); -// File I/O via std.fs - no raw libc file ABI in the boot harness. const { File, exists } = import("std/fs"); const C0_SR: u32 = 12; const SR_BEV: u32 = 0x00400000; -// Re-declare the same shapes as cpu.jam - structural equality lets us -// pass values across module boundaries. const Cpu = struct { pc: u32, nextPc: u32, @@ -63,10 +52,6 @@ const Cpu = struct { cycles: u64, }; - - -// Bundles the four moving pieces of a fresh emulator so tests can -// allocate / free with one call each. const Harness = struct { cpu: Cpu, regs: Vec(u32), @@ -335,29 +320,20 @@ tfn psxSyscallVector() { assert(tSyscallVector(), 0xBFC00180); } tfn psxLoadDelay() { assert(tLoadDelay(), 0x0000002A); } tfn psxLwlLwr() { assert(tLwlLwr(), 0xBBCCDD11); } -// GTE (COP2) unit tests -// -// createGte gives a standalone register buffer, so the math ops can be -// exercised without a full Bus. These lock in the GTE fixes: -// RTPS stores MAC >> sf (not the raw 44-bit sum), MFC2 sign-extends the -// 16-bit registers, and the perspective divide uses the UNR LUT. - -// Identity rotation matrix (4096 = 1.0 in s1.3.12), zero translation. fn gteSetIdentity(g: *mut[] u32) { - gteCtrlWrite(g, 0, 0x00001000); // RT11=4096, RT12=0 - gteCtrlWrite(g, 1, 0x00000000); // RT13=0, RT21=0 - gteCtrlWrite(g, 2, 0x00001000); // RT22=4096, RT23=0 - gteCtrlWrite(g, 3, 0x00000000); // RT31=0, RT32=0 - gteCtrlWrite(g, 4, 0x00001000); // RT33=4096 + gteCtrlWrite(g, 0, 0x00001000); + gteCtrlWrite(g, 1, 0x00000000); + gteCtrlWrite(g, 2, 0x00001000); + gteCtrlWrite(g, 3, 0x00000000); + gteCtrlWrite(g, 4, 0x00001000); } fn tGteRtpsMac() u32 { var g: Vec(u32) = createGte(); gteSetIdentity(g.ptr); - gteDataWrite(g.ptr, 0, (200 << 16) | 100); // V0 = (100, 200, z) - gteDataWrite(g.ptr, 1, 300); // V0.z = 300 - gteExec(g.ptr, 0x4A080001); // RTPS, sf=12 - // MAC1 = ((TR<<12) + 4096*100) >> 12 = 100 (pre-fix it was 409600). + gteDataWrite(g.ptr, 0, (200 << 16) | 100); + gteDataWrite(g.ptr, 1, 300); + gteExec(g.ptr, 0x4A080001); const mac1: u32 = gteDataRead(g.ptr, 25); return mac1; } @@ -368,157 +344,121 @@ fn tGteRtpsIr() u32 { gteDataWrite(g.ptr, 0, (200 << 16) | 100); gteDataWrite(g.ptr, 1, 300); gteExec(g.ptr, 0x4A080001); - const ir3: u32 = gteDataRead(g.ptr, 11); // clamp(MAC3=300) = 300 + const ir3: u32 = gteDataRead(g.ptr, 11); return ir3; } fn tGteIrSext() u32 { var g: Vec(u32) = createGte(); - gteDataWrite(g.ptr, 9, 0x0000FFFF); // IR1 = -1 (16-bit) - const v: u32 = gteDataRead(g.ptr, 9); // MFC2 sign-extends -> -1 + gteDataWrite(g.ptr, 9, 0x0000FFFF); + const v: u32 = gteDataRead(g.ptr, 9); return v; } fn tGteDivide() u32 { var g: Vec(u32) = createGte(); - const q: u32 = gteDivide(g.ptr, 0x800, 0x1000); // 2048/4096 -> 0x8000 + const q: u32 = gteDivide(g.ptr, 0x800, 0x1000); return q; } fn tGteDivideOvf() u32 { var g: Vec(u32) = createGte(); - const q: u32 = gteDivide(g.ptr, 0x2000, 0x1000); // n >= 2d -> saturates + const q: u32 = gteDivide(g.ptr, 0x2000, 0x1000); return q; } -// FLAG (reg 63) verification. CFC2 r63 reads g[32+31] masked to -// 0x7FFFF000 with bit 31 synthesized from the 0x7F87E000 error summary. - fn tGteSqrFlag() u32 { var g: Vec(u32) = createGte(); - gteDataWrite(g.ptr, 9, 0x7FFF); // IR1 = 32767 - gteExec(g.ptr, 0x4A080028); // SQR, sf=12 -> MAC1=262143, IR1 saturates - const f: u32 = gteCtrlRead(g.ptr, 31); // bit 24 (IR1 sat) + bit 31 summary + gteDataWrite(g.ptr, 9, 0x7FFF); + gteExec(g.ptr, 0x4A080028); + const f: u32 = gteCtrlRead(g.ptr, 31); return f; } fn tGteDivideFlag() u32 { var g: Vec(u32) = createGte(); - const q: u32 = gteDivide(g.ptr, 0x2000, 0x1000); // n>=2d -> bit 17 (divide ovf) - const f: u32 = gteCtrlRead(g.ptr, 31); // bit 17 + bit 31 summary + const q: u32 = gteDivide(g.ptr, 0x2000, 0x1000); + const f: u32 = gteCtrlRead(g.ptr, 31); return f; } fn tGteNoFlag() u32 { var g: Vec(u32) = createGte(); - gteDataWrite(g.ptr, 9, 100); // IR1 = 100 - gteExec(g.ptr, 0x4A080028); // SQR sf=12 -> MAC1=2, no saturation - const f: u32 = gteCtrlRead(g.ptr, 31); // FLAG must be clean + gteDataWrite(g.ptr, 9, 100); + gteExec(g.ptr, 0x4A080028); + const f: u32 = gteCtrlRead(g.ptr, 31); return f; } -// RTPS IR3 via gte_clamp_ir_z: with sf=0, MAC3 = 4096*256 = 0x100000. -// clamp_ir_z keys FLAG bit 22 off macRaw3>>12 = 256 (in IR range) so it -// stays clear, even though the returned IR3 clamps 0x100000 -> 0x7FFF. The -// old plain IR clamp keyed bit 22 off the unshifted 0x100000 and wrongly -// set it (FLAG = 0x400000; bit 22 is excluded from the bit-31 summary). fn tGteRtpsClampIrZ() u32 { var g: Vec(u32) = createGte(); gteSetIdentity(g.ptr); - gteDataWrite(g.ptr, 0, 0); // V0 = (0, 0, z) - gteDataWrite(g.ptr, 1, 256); // V0.z = 256 - gteExec(g.ptr, 0x4A000001); // RTPS, sf=0 + gteDataWrite(g.ptr, 0, 0); + gteDataWrite(g.ptr, 1, 256); + gteExec(g.ptr, 0x4A000001); const f: u32 = gteCtrlRead(g.ptr, 31); return f; } -// MVMVA nested 44-bit truncation. TRX<<12 (≈2^43) + RT11*VX overflows 44 -// bits, so the nested gte_check_mac raises FLAG bit 30 (MAC1 positive -// overflow) - even though the full MAC1 sum (RT12=-0x8000 pulls it back) -// stays in 44-bit range, which is all the old single clamp_mac saw. fn tGteMvmvaNestedOvf() u32 { var g: Vec(u32) = createGte(); - gteCtrlWrite(g.ptr, 5, 0x7FFFFFFF); // TRX = max - gteCtrlWrite(g.ptr, 0, 0x80007FFF); // RT11 = 0x7FFF, RT12 = -0x8000 - gteCtrlWrite(g.ptr, 1, 0); // RT13 = 0 - gteDataWrite(g.ptr, 0, 0x7FFF7FFF); // V0 = (0x7FFF, 0x7FFF) - gteDataWrite(g.ptr, 1, 0); // V0.z = 0 - gteExec(g.ptr, 0x4A080012); // MVMVA: rot, V0, TR, sf=12, lm=0 + gteCtrlWrite(g.ptr, 5, 0x7FFFFFFF); + gteCtrlWrite(g.ptr, 0, 0x80007FFF); + gteCtrlWrite(g.ptr, 1, 0); + gteDataWrite(g.ptr, 0, 0x7FFF7FFF); + gteDataWrite(g.ptr, 1, 0); + gteExec(g.ptr, 0x4A080012); const f: u32 = gteCtrlRead(g.ptr, 31) & 0x40000000; return f; } -// IRGB/ORGB (reg 28) repacks IR1/2/3 (>>7, clamped 0..0x1F) into 15-bit RGB. -// IR is stored zero-extended in the low 16 bits and must be sign-extended -// before the >>7, so a negative IR clamps to 0 (DuckStation -// gte.cpp:343). The old `(g[9] as i32)` bit-cast read IR1=-1 as +65535 -> 0x1F; -// with all three IR = -1 the packed result must be 0, not 0x7FFF. fn tGteIrgbNeg() u32 { var g: Vec(u32) = createGte(); - gteDataWrite(g.ptr, 9, 0x0000FFFF); // IR1 = -1 - gteDataWrite(g.ptr, 10, 0x0000FFFF); // IR2 = -1 - gteDataWrite(g.ptr, 11, 0x0000FFFF); // IR3 = -1 - return gteDataRead(g.ptr, 28); // each channel clamps to 0 -> 0 + gteDataWrite(g.ptr, 9, 0x0000FFFF); + gteDataWrite(g.ptr, 10, 0x0000FFFF); + gteDataWrite(g.ptr, 11, 0x0000FFFF); + return gteDataRead(g.ptr, 28); } -// Positive packing still works after the sign-extension fix: IR1=3968->31, -// IR2=128->1, IR3=0 ⇒ 0x1F | (1<<5) | 0 = 0x3F. fn tGteIrgbPos() u32 { var g: Vec(u32) = createGte(); - gteDataWrite(g.ptr, 9, 0x0F80); // IR1 = 3968 -> >>7 = 31 - gteDataWrite(g.ptr, 10, 0x0080); // IR2 = 128 -> >>7 = 1 - gteDataWrite(g.ptr, 11, 0x0000); // IR3 = 0 + gteDataWrite(g.ptr, 9, 0x0F80); + gteDataWrite(g.ptr, 10, 0x0080); + gteDataWrite(g.ptr, 11, 0x0000); return gteDataRead(g.ptr, 28); } -// RTPT runs the depth-cue (DQ) tail only on the LAST vertex. -// Construct V0/V1 with a small Z (-> large divide -> the DQ's -// IR0 = clamp(DQA*div>>12) saturates, setting FLAG bit 12) and V2 with a large -// Z (-> small divide -> no IR0 saturation). Bit 12 (IR0 sat) is set ONLY by the -// DQ tail's gte_clamp_ir0 and is excluded from the bit-31 summary, so it -// cleanly isolates whether intermediate vertices wrongly ran the DQ. With the -// fix only V2's DQ runs -> bit 12 clear. (Pre-fix: V0/V1 saturate it -> 0x1000.) fn tGteRtptDqGate() u32 { var g: Vec(u32) = createGte(); gteSetIdentity(g.ptr); - gteCtrlWrite(g.ptr, 26, 2); // H = 2 - gteCtrlWrite(g.ptr, 27, 0x1000); // DQA = 4096 - gteCtrlWrite(g.ptr, 28, 0); // DQB = 0 - gteDataWrite(g.ptr, 0, 0); gteDataWrite(g.ptr, 1, 16); // V0.z = 16 -> div 0x2000, IR0 sat - gteDataWrite(g.ptr, 2, 0); gteDataWrite(g.ptr, 3, 16); // V1.z = 16 - gteDataWrite(g.ptr, 4, 0); gteDataWrite(g.ptr, 5, 0x1000); // V2.z = 4096 -> div 32, no sat - gteExec(g.ptr, 0x4A000030); // RTPT, sf=0 - return gteCtrlRead(g.ptr, 31) & 0x1000; // FLAG bit 12 = IR0 saturation (DQ tail only) -} - -// NCDS pushes the final colour via the FLAG-aware RGB clamp, -// so a channel that saturates >255 sets FLAG bit 21/20/19 -// (R/G/B). Drive only the R far-colour large and IR0 to max so stage-5 -// MAC1 = IR0*ir1f hugely overflows 255 on R alone; bits 19/20/21 masked must -// read 0x200000 (R). The old inline clampU8 set none of these (returned 0). + gteCtrlWrite(g.ptr, 26, 2); + gteCtrlWrite(g.ptr, 27, 0x1000); + gteCtrlWrite(g.ptr, 28, 0); + gteDataWrite(g.ptr, 0, 0); gteDataWrite(g.ptr, 1, 16); + gteDataWrite(g.ptr, 2, 0); gteDataWrite(g.ptr, 3, 16); + gteDataWrite(g.ptr, 4, 0); gteDataWrite(g.ptr, 5, 0x1000); + gteExec(g.ptr, 0x4A000030); + return gteCtrlRead(g.ptr, 31) & 0x1000; +} + fn tGteNcdsRgbSat() u32 { var g: Vec(u32) = createGte(); - gteCtrlWrite(g.ptr, 21, 0x00010000); // RFC large -> ir1f saturates high - gteDataWrite(g.ptr, 8, 0x7FFF); // IR0 = max - gteExec(g.ptr, 0x4A000013); // NCDS, sf=0 - return gteCtrlRead(g.ptr, 31) & 0x00380000; // RGB-saturation FLAG bits 19/20/21 + gteCtrlWrite(g.ptr, 21, 0x00010000); + gteDataWrite(g.ptr, 8, 0x7FFF); + gteExec(g.ptr, 0x4A000013); + return gteCtrlRead(g.ptr, 31) & 0x00380000; } -// DQA (control reg 27) is s16 (DuckStation gte_types.h:117), -// so the RTPS depth-cue must sign-extend its low 16 bits, not read all 32. With -// DQA=0x00010001 the low half is 1: depth-cue MAC0 = DQB + DQA*div = 1*0x10000, -// IR0 = clamp(0x10000>>12) = 16. The old s32 read used 65537, giving a huge -// MAC0 that saturated IR0 to 0x1000. H=SZ3=4096 ⇒ div = 0x10000. fn tGteDqaWidth() u32 { var g: Vec(u32) = createGte(); gteSetIdentity(g.ptr); - gteCtrlWrite(g.ptr, 26, 0x1000); // H = 4096 - gteCtrlWrite(g.ptr, 27, 0x00010001); // DQA: low16=1 (s16), high bits set - gteCtrlWrite(g.ptr, 28, 0); // DQB = 0 + gteCtrlWrite(g.ptr, 26, 0x1000); + gteCtrlWrite(g.ptr, 27, 0x00010001); + gteCtrlWrite(g.ptr, 28, 0); gteDataWrite(g.ptr, 0, 0); - gteDataWrite(g.ptr, 1, 0x1000); // V0.z = 4096 -> SZ3 = 4096 - gteExec(g.ptr, 0x4A000001); // RTPS, sf=0 - return gteDataRead(g.ptr, 8); // IR0 = clamp((DQA_s16 * div) >> 12) = 16 + gteDataWrite(g.ptr, 1, 0x1000); + gteExec(g.ptr, 0x4A000001); + return gteDataRead(g.ptr, 8); } tfn psxGteSqrFlag() { assert(tGteSqrFlag(), 0x81000000); } @@ -539,11 +479,6 @@ tfn psxGteRtptDqGate() { assert(tGteRtptDqGate(), 0); } tfn psxGteNcdsRgbSat() { assert(tGteNcdsRgbSat(), 0x00200000); } tfn psxGteDqaWidth() { assert(tGteDqaWidth(), 16); } -// headless emulator boot tests -// -// These tests boot the real BIOS (and optionally a disc image) into a -// fully-allocated Bus without SDL, then run N frames and assert state. - const C0_PRID: u32 = 15; fn fileExists(path: []u8) bool { @@ -569,9 +504,6 @@ const CYC_PER_SCANLINE: u32 = 2172; const SCANLINES_VDRAW: u32 = 240; const SCANLINES_TOTAL: u32 = 263; -// Mirror main.jam's per-cycle peripheral tick (post-Phase-3). Tests -// should run the same scheduler as production so they catch real -// regressions. fn emuRunOneFrame(c: mut Cpu, bus: Bus, regs: Vec(u32), cop0: Vec(u32)) { var line: u32 = 0; var lineCyc: u32 = 0; @@ -637,10 +569,6 @@ tfn emuBiosShellBoots() { assert(emuPcInRamOrScratchpad(c.pc), true); } -// With game.bin attached, BIOS bootstrap reads the disc and hands off to -// the game's EXEC entry. After 120 frames (2 sec emulated) the CPU -// should be executing in RAM, not stuck looping in the BIOS shell at -// 0x80059Exx (the "no disc" idle loop in main.jam:467). tfn emuDiscBootsExec() { var diskPath: []u8 = "game.bin"; if (!fileExists(diskPath)) { return; } @@ -653,64 +581,38 @@ tfn emuDiscBootsExec() { } discOpen(bus.disc.ptrMut(), diskPath); emuRunFrames(c, bus, regs.ptr, cop0.ptr, 120); - // Bare minimum: BIOS handed control out of ROM (0xBFC*) into RAM - // or scratchpad. Stricter "PC inside game text segment" check is - // a TODO - currently fails for the Harvest Moon SIO0-WaitEvent - // stall where PC stays in BIOS scratchpad at 0x00001EB0. assert(emuPcInRamOrScratchpad(c.pc), true); } -// With a disc attached the BIOS bootstrap-loader should issue CD reads. -// We assert by reading the response FIFO write index from cdrom state -// (offset 0x2C is C_RESP_W in cdrom.jam) - it grows every time the CD -// pushes an INT3/INT2/INT5 response, so a non-zero value proves the CD -// state machine got exercised. - -// variable per-instruction cycle model -// -// step() charges `fetchCyc + instrCyc` per instruction: -// fetchCyc = 18 when the opcode is read from BIOS ROM (BIOS bus delay) -// else 0; instrCyc = 2 except COP2 math ops, which cost their GTE op time. -// These deltas drive device timing (CDROM/DMA/timers) at the right rate. - -// gteOpCycles selects the GTE op cycle count from the low 6 bits. -fn tGteCycRtps() u32 { return gteOpCycles(0x4A000001); } // RTPS -> 15 -fn tGteCycNcdt() u32 { return gteOpCycles(0x4A000016); } // NCDT -> 44 -fn tGteCycRtpt() u32 { return gteOpCycles(0x4A000030); } // RTPT -> 23 -fn tGteCycNcct() u32 { return gteOpCycles(0x4A00003F); } // NCCT -> 39 -fn tGteCycDflt() u32 { return gteOpCycles(0x4A000000); } // non-math -> 2 +fn tGteCycRtps() u32 { return gteOpCycles(0x4A000001); } +fn tGteCycNcdt() u32 { return gteOpCycles(0x4A000016); } +fn tGteCycRtpt() u32 { return gteOpCycles(0x4A000030); } +fn tGteCycNcct() u32 { return gteOpCycles(0x4A00003F); } +fn tGteCycDflt() u32 { return gteOpCycles(0x4A000000); } -// A plain instruction fetched from cached RAM costs 2 (0 fetch + 2 base). fn tCycRam() u32 { - var h: Harness = createHarness(); // pc = 0x80000000 (KSEG0) - placeOp(h.bus, h.cop0.ptr, 0x80000000, encI(0x0D, 0, 8, 0x1234)); // ORI + var h: Harness = createHarness(); + placeOp(h.bus, h.cop0.ptr, 0x80000000, encI(0x0D, 0, 8, 0x1234)); step(h.cpu, h.bus, h.regs.ptr, h.cop0.ptr); const v: u32 = h.cpu.cycles as u32; return v; } -// Same instruction class fetched from BIOS ROM costs 18 + 2 = 20. The -// base is charged before dispatch, so the cost is independent of what -// the (zeroed) BIOS actually decodes to, as long as it isn't a GTE op. fn tCycBios() u32 { var regs: Vec(u32) = createRegFile(32); var cop0: Vec(u32) = createRegFile(32); var bus: Bus = createBus(); - var cpu: Cpu = freshCpuAt(0xBFC00000); // KSEG1 BIOS entry + var cpu: Cpu = freshCpuAt(0xBFC00000); var h: Harness = Harness { cpu: cpu, regs: regs, cop0: cop0, bus: bus }; step(h.cpu, h.bus, h.regs.ptr, h.cop0.ptr); const v: u32 = h.cpu.cycles as u32; return v; } -// A GTE math op (RTPS) fetched from RAM costs its full 15 cycles. fn tCycGteRtps() u32 { var h: Harness = createHarness(); - // Enable COP2 (SR.CU2, bit 30 of C0_SR=12) - dispatch raises a - // coprocessor-unusable exception for COP2 ops otherwise, and the GTE - // cycle cost would never be charged. cop0Write(h.cop0.ptr, 12, 0x40000000); - placeOp(h.bus, h.cop0.ptr, 0x80000000, 0x4A000001); // RTPS (COP2 imm) + placeOp(h.bus, h.cop0.ptr, 0x80000000, 0x4A000001); step(h.cpu, h.bus, h.regs.ptr, h.cop0.ptr); const v: u32 = h.cpu.cycles as u32; return v; diff --git a/timer.jam b/timer.jam index 75e9b07..6e9a55e 100644 --- a/timer.jam +++ b/timer.jam @@ -1,15 +1,4 @@ -// Three 16-bit timers at 0x1F801100 / 0x1F801110 / 0x1F801120, each -// exposing three registers (counter, mode, target). They count at -// different clock sources - system clock, dotclock, hblank - and can -// raise IRQs (lines 4 / 5 / 6 = TIMER0 / TIMER1 / TIMER2) on -// target-hit or 0xFFFF wraparound. - const { irqRaise, IC_TIMER0, IC_TIMER1, IC_TIMER2 } = import("irq"); - -// Per-timer state. Each channel has its own counter, target, and a -// pile of mode-register bits (sync / IRQ / clock-source). The 0/1 -// flag bits stay u32 so they round-trip cleanly through the mode -// register's pack/unpack without per-field width juggling. pub const TimerChannel = struct { counter: f32, target: u32, @@ -38,9 +27,6 @@ pub const TimerChannel = struct { } }; -// Aggregate timer state for the chip. Two shared blank-line flags -// (`hblank` / `vblank`) drive sync-mode behavior; `channels` carries the -// three 16-bit timers. pub const Timer = struct { hblank: u32, vblank: u32, @@ -62,9 +48,6 @@ pub const Timer = struct { }; } - // Refresh the Timer 0 dot-clock divisor from the GPU display mode - // (GP1 08h, low 24 bits). hdiv table {256:10, 320:8, 512:5, 640:4}, - // or 7 for the 368-wide (bit 6) mode. pub fn setDotclock(self: mut Self, dm: u32) { if ((dm & 0x40) != 0) { self.dotDiv = 11.0 / 7.0 / 7.0; @@ -78,9 +61,6 @@ pub const Timer = struct { self.dotDiv = 11.0 / 7.0 / div; } - // Compose the mode register from individual flag fields. - // Side effect: clears - // `targetReached` and `maxReached` (they latch until read). pub fn getMode(self: mut Self, idx: u32) u32 { var v: u32 = 0; v = v | ((self.channels[idx].syncEnable & 1) << 0); @@ -99,10 +79,6 @@ pub const Timer = struct { return v; } - // Mode write - decompose the value into the per-flag fields, - // then reset side-state (counter, irq, fired, etc.). - // Also seeds `paused` from hblank/vblank for sync - // modes. pub fn setMode(self: mut Self, idx: u32, val: u32) { self.channels[idx].syncEnable = (val >> 0) & 1; self.channels[idx].syncMode = (val >> 1) & 3; @@ -145,17 +121,8 @@ pub const Timer = struct { } } - // IRQ delivery logic - call after every counter advance and - // every register write. Sets targetReached / maxReached as - // appropriate, fires the matching IRQ line if its mask bit is - // enabled, honours one-shot vs repeat, and resets the counter - // on target-hit if resetTarget is set. pub fn handleIrq(self: mut Self, idx: u32, ic: *mut[] u32, cop0: *mut[] u32) { - // Compare the FLOAT counter directly to the target and to 65535.0f - // with strict `>`. Using `>=` (toward hardware/DuckStation) would - // fire the timer IRQ one tick earlier on every exact landing - a - // per-IRQ delivery-timing divergence. Stick with `>`. const counter: f32 = self.channels[idx].counter; const target: u32 = self.channels[idx].target; var fireIrq: u32 = 0; @@ -203,7 +170,6 @@ pub const Timer = struct { } } - // Bus reads - register layout per timer is: // +0 counter (16-bit) // +4 mode (16-bit, read also clears targetReached / maxReached) // +8 target (16-bit) @@ -246,22 +212,12 @@ pub const Timer = struct { self.write32(ic, cop0, off, val & 0xFF); } - // Per-cycle advance. The caller passes how many CPU cycles - // have just been executed. Timer 1 with clk_source=1 (hblank) - // is driven by `hblankBegin` instead; timer 0 has a dotclock - // mode we approximate as system clock. pub fn update(self: mut Self, ic: *mut[] u32, cop0: *mut[] u32) { self.updateCyc(ic, cop0, 2); } pub fn updateCyc(self: mut Self, ic: *mut[] u32, cop0: *mut[] u32, cyc: u32) { - // IGNORE the cyc arg and advance all three timers by a hardcoded 2 - // per instruction - NOT the real per-instruction cycle count. - // Feeding the real `cyc` (fetchCyc+2 = 20 for BIOS-resident code) - // makes the sysclk timers run ~10× too fast in BIOS/kernel loops, - // drifting any timer value the game reads. Pass 2. (`cyc` kept for - // signature parity.) self.updateOne(0, 2, ic, cop0); self.updateOne(1, 2, ic, cop0); self.updateOne(2, 2, ic, cop0); @@ -273,13 +229,6 @@ pub const Timer = struct { // Timer 1 sourced from hblank is incremented in hblankBegin. if (idx == 1 && (self.channels[idx].clkSource & 1) != 0) { return; } const cf: f32 = cyc as f32; - // Advance the float counter. Timer 2 with clk_source>=2 ticks at - // sysclk/8 (`+= cyc/8`). Timer 0 with clk_source bit0 set ticks at - // the GPU dot-clock (`+= cyc * dotDiv`, dotDiv = (11/7)/hdiv) instead - // of full sysclk - matches the reference emulators (the dot-clock is - // slower than sysclk, so a plain `+= cyc` would run T0 several× too - // fast). No integer subtick or leap-past-target adjustment: the float - // overshoot/reset is handled directly by handleIrq. if (idx == 2 && self.channels[idx].clkSource >= 2) { self.channels[idx].counter = self.channels[idx].counter + cf / 8.0; } else if (idx == 0 && (self.channels[idx].clkSource & 1) != 0) { @@ -290,15 +239,11 @@ pub const Timer = struct { self.handleIrq(idx, ic, cop0); } - // Hblank / vblank entry/exit hooks called by the GPU's - // frame-pacing. pub fn hblankBegin(self: mut Self, ic: *mut[] u32, cop0: *mut[] u32) { self.hblank = 1; if ((self.channels[1].clkSource & 1) != 0 && self.channels[1].paused == 0) { - // Increment the hblank-sourced T1 counter by 1.0 each - // hblank; handleIrq's float overflow/target checks reset it. self.channels[1].counter = self.channels[1].counter + 1.0; self.handleIrq(1, ic, cop0); }