From 3aa0a23ed4b7a2d73ccde861175f0a1f7b4a6856 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Fri, 5 Jun 2026 11:14:18 +0200 Subject: [PATCH] add real user cases in tests --- tests/unit/test_user_case_nes_ram_mirror.jam | 76 +++++++++++++++++++ .../test_user_case_ps1_fmv_decode_math.jam | 55 ++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 tests/unit/test_user_case_nes_ram_mirror.jam create mode 100644 tests/unit/test_user_case_ps1_fmv_decode_math.jam diff --git a/tests/unit/test_user_case_nes_ram_mirror.jam b/tests/unit/test_user_case_nes_ram_mirror.jam new file mode 100644 index 0000000..f9856fa --- /dev/null +++ b/tests/unit/test_user_case_nes_ram_mirror.jam @@ -0,0 +1,76 @@ +// NES CPU RAM: 2 KiB of physical memory mirrored across $0000-$1FFF. +// https://problemkaputt.de/everynes.htm#memorymaps +// +// Every address in the 8 KiB window maps onto the same 2 KiB cells via +// `address & RAM_MASK`, so $0000, $0800, $1000 and $1800 are four views +// of cell 0. Also a regression test for comptime array sizes in type +// position: `[RAM_PHYSICAL_END]u8` must fold the module const to a +// 2048-byte array, and `[0; RAM_PHYSICAL_END]` must fold the repeat +// count the same way. + +const { assert } = import("test"); + +const RAM_PHYSICAL_START = 0x0000; +const RAM_PHYSICAL_END = 0x0800; +const RAM_MASK = RAM_PHYSICAL_END - 1; +const RAM_BASE = RAM_PHYSICAL_START; + +pub const RAM = struct { + cells: [RAM_PHYSICAL_END]u8, // 2k + + pub fn read(self: Self, address: u16) u8 { + return self.cells[(address - RAM_BASE) & RAM_MASK]; + } + + pub fn write(self: mut Self, address: u16, value: u8) { + self.cells[(address - RAM_BASE) & RAM_MASK] = value; + } +}; + +tfn freshRamReadsZero() { + var r = RAM{cells: [0; RAM_PHYSICAL_END]}; + assert(r.read(0x0000), 0); + assert(r.read(0x07FF), 0); + assert(r.read(0x1FFF), 0); +} + +tfn basicWriteAndRead() { + var r = RAM{cells: [0; 0x800]}; + r.write(0x0000, 0x42); + assert(r.read(0x0000), 0x42); +} + +tfn mirroredWriteAndRead() { + var r = RAM{cells: [0; 0x800]}; + r.write(0x0800, 0x42); + // $0800 & $07FF = 0: the write lands in cell 0, so both the base + // address and the mirror observe the same value. + assert(r.read(0x0000), 0x42); + assert(r.read(0x0800), 0x42); +} + +tfn allMirrorImagesAlias() { + var r = RAM{cells: [0; 0x800]}; + r.write(0x0123, 0x5A); + assert(r.read(0x0923), 0x5A); + assert(r.read(0x1123), 0x5A); + assert(r.read(0x1923), 0x5A); +} + +tfn mirrorWriteOverwritesBase() { + var r = RAM{cells: [0; 0x800]}; + r.write(0x0000, 0x11); + r.write(0x1800, 0x22); + assert(r.read(0x0000), 0x22); +} + +tfn maskBoundaryDistinctCells() { + var r = RAM{cells: [0; 0x800]}; + // $07FF is the last physical cell; $0800 wraps to cell 0. They must + // stay independent — an off-by-one in the mask would collapse them. + r.write(0x07FF, 0x01); + r.write(0x0800, 0x02); + assert(r.read(0x07FF), 0x01); + assert(r.read(0x0000), 0x02); + assert(r.read(0x1FFF), 0x01); +} diff --git a/tests/unit/test_user_case_ps1_fmv_decode_math.jam b/tests/unit/test_user_case_ps1_fmv_decode_math.jam new file mode 100644 index 0000000..e6e5363 --- /dev/null +++ b/tests/unit/test_user_case_ps1_fmv_decode_math.jam @@ -0,0 +1,55 @@ +// Exercises the exact language constructs the jamstation FMV decode path +// relies on but the rest of the emulator (and the simple float tests) do +// NOT: compound f64 YUV->RGB arithmetic with negative coefficients + +// f64->i32 truncation, and i64 wide multiply/shift (>32-bit products). +// If any of these is miscompiled, FMV decodes to garbage/black while +// integer-only games (Crash) are unaffected. + +const { assert } = import("test"); + +fn clampSByte(v: i32) i32 { + if (v < (0 - 128)) { return 0 - 128; } + if (v > 127) { return 127; } + return v; +} + +// Replicates mdec.jam yuvToRgb for one pixel (unsigned output). +fn yuvR(l: i32, rRaw: i32) i32 { + const rf: f64 = rRaw as f64; + const rFloat: f64 = 1.402 * rf; + var r: i32 = clampSByte(l + (rFloat as i32)); + r = r ^ 0x80; + return r & 0xFF; +} +fn yuvG(l: i32, rRaw: i32, bRaw: i32) i32 { + 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; + var g: i32 = clampSByte(l + (gFloat as i32)); + g = g ^ 0x80; + return g & 0xFF; +} + +tfn yuvKnownPixel() { + // Y=128, Cr=64: rFloat = 1.402*64 = 89.728 -> 89; r = clamp(217) = 127; + // ^0x80 = 255. + assert(yuvR(128, 64) as u32, 255); + // Cr=64, Cb=-32: gFloat = -0.3437*-32 + -0.7143*64 = 10.9984 - 45.7152 + // = -34.7168 -> -34; g = clamp(94) = 94; ^0x80 = 222. + assert(yuvG(128, 64, 0 - 32) as u32, 222); +} + +// i64 wide multiply (product > 2^32) + wide shift — the SPU volume mix and +// the MDEC 44-bit clamp depend on i64 not silently truncating to 32 bits. +fn i64MulShift(a: i64, b: i64, c: i64, sh: u32) i64 { + return (a * b * c) >> sh; +} + +tfn i64WideArithmetic() { + // 1 << 43 = 0x0000_0800_0000_0000: low word 0, bits 43..32 -> 0x800 (2048). + var x: i64 = (1 as i64) << 43; + assert((x >> 32) as u32, 2048); + assert((x & 0xFFFFFFFF) as u32, 0); + // 1000*16384*32767 = 536_854_528_000 (>2^32); >>30 = 499 (floors below 500). + assert(i64MulShift(1000, 16384, 32767, 30) as u32, 499); +} -- 2.51.2