#include "MiniTest.h" #include "runtime/ps2_memory.h" #include "ps2_runtime.h" #include "ps2_stubs.h" #include "ps2_syscalls.h" #include "runtime/ps2_gs_gpu.h" #include "runtime/ee_scheduler.h" #include "runtime/ps2_gs_memory.h" #include "runtime/ps2_gs_rasterizer.h" #include "runtime/ps2_gs_psmct32.h" #include "runtime/ps2_gs_psmt4.h" #include "runtime/ps2_gs_psmt8.h" #include "Stubs/Helpers/Support.h" #include "Stubs/GS.h" #include #include #include #include #include #include using namespace ps2_syscalls; namespace { std::atomic g_gsSyncCallbackHits{0u}; std::atomic g_gsSyncCallbackLastTick{0u}; std::atomic g_gsSyncFirstField{-1}; std::atomic g_gsSyncSecondField{-1}; std::atomic g_gsSyncCallbackSp{0u}; std::atomic g_gsSyncCallbackGp{0u}; std::atomic g_gsSyncCallbackPrevious{0u}; constexpr uint32_t kGsSyncWait0Pc = 0x0011F000u; constexpr uint32_t kGsSyncResume0Pc = 0x0011F010u; constexpr uint32_t kGsSyncWait1Pc = 0x0011F020u; constexpr uint32_t kGsSyncResume1Pc = 0x0011F030u; constexpr uint32_t kGsCallbackMainPc = 0x0011F040u; constexpr uint32_t kGsCallbackResumePc = 0x0011F050u; constexpr uint32_t kGsCallbackPc = 0x00120000u; constexpr uint32_t kGsCallbackGp = 0x0036A7F0u; constexpr uint32_t kGsCallbackCallerSp = 0x00123450u; static_assert(sizeof(GsImageMem) == 12, "GsImageMem size mismatch"); void setRegU32(R5900Context &ctx, int reg, uint32_t value) { ctx.r[reg] = _mm_set_epi64x(0, static_cast(value)); } void setRegU64(R5900Context &ctx, int reg, uint64_t value) { ctx.r[reg] = _mm_set_epi64x(0, static_cast(value)); } uint32_t getRegU32Test(const R5900Context &ctx, int reg) { return ::getRegU32(&ctx, reg); } uint64_t getReturnU64(const R5900Context &ctx) { const uint64_t lo = static_cast(getRegU32Test(ctx, 2)); const uint64_t hi = static_cast(getRegU32Test(ctx, 3)); return lo | (hi << 32); } uint64_t makeGifTag(uint16_t nloop, uint8_t flg, uint8_t nreg, bool eop = true) { uint64_t tag = static_cast(nloop & 0x7FFFu); if (eop) tag |= (1ull << 15); tag |= (static_cast(flg & 0x3u) << 58); tag |= (static_cast(nreg & 0xFu) << 60); return tag; } void appendU64(std::vector &dst, uint64_t value) { const size_t pos = dst.size(); dst.resize(pos + sizeof(uint64_t)); std::memcpy(dst.data() + pos, &value, sizeof(uint64_t)); } void appendGifAd(std::vector &dst, uint64_t value, uint64_t reg) { appendU64(dst, value); appendU64(dst, reg); } template bool waitUntil(Predicate pred, std::chrono::milliseconds timeout) { const auto deadline = std::chrono::steady_clock::now() + timeout; while (std::chrono::steady_clock::now() < deadline) { if (pred()) { return true; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } return pred(); } void testGsSyncVCallback(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { (void)rdram; (void)runtime; g_gsSyncCallbackLastTick.store(getRegU32(ctx, 4), std::memory_order_relaxed); g_gsSyncCallbackSp.store(getRegU32(ctx, 29), std::memory_order_relaxed); g_gsSyncCallbackGp.store(getRegU32(ctx, 28), std::memory_order_relaxed); g_gsSyncCallbackHits.fetch_add(1u, std::memory_order_relaxed); ctx->pc = 0u; } void testGsSyncWait0(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { ctx->pc = kGsSyncResume0Pc; ps2_stubs::sceGsSyncV(rdram, ctx, runtime); } void testGsSyncResume0(uint8_t *, R5900Context *ctx, PS2Runtime *) { g_gsSyncFirstField.store(static_cast(getRegU32(ctx, 2)), std::memory_order_release); ctx->pc = kGsSyncWait1Pc; } void testGsSyncWait1(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { ctx->pc = kGsSyncResume1Pc; ps2_stubs::sceGsSyncV(rdram, ctx, runtime); } void testGsSyncResume1(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) { g_gsSyncSecondField.store(static_cast(getRegU32(ctx, 2)), std::memory_order_release); ctx->pc = 0u; runtime->requestStop(); } void testGsCallbackMain(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { setRegU32(*ctx, 4, kGsCallbackPc); setRegU32(*ctx, 28, kGsCallbackGp); setRegU32(*ctx, 29, kGsCallbackCallerSp); ctx->pc = kGsCallbackResumePc; ps2_stubs::sceGsSyncVCallback(rdram, ctx, runtime); g_gsSyncCallbackPrevious.store(getRegU32(ctx, 2), std::memory_order_release); ps2_syscalls::WaitVSyncTick(rdram, ctx, runtime, -1); } void testGsCallbackResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) { ctx->pc = 0u; runtime->requestStop(); } void writeGsImageTest(uint8_t *rdram, uint32_t addr, const GsImageMem &image) { std::memcpy(rdram + addr, &image, sizeof(image)); } void writeGsImageTest(std::vector &rdram, uint32_t addr, const GsImageMem &image) { writeGsImageTest(rdram.data(), addr, image); } void writePSMT4Texel(std::vector &vram, uint32_t tbp, uint32_t tbw, uint32_t x, uint32_t y, uint8_t index) { const uint32_t nibbleAddr = GSPSMT4::addrPSMT4(tbp, tbw, x, y); const uint32_t byteOff = nibbleAddr >> 1; uint8_t &packed = vram[byteOff]; if ((nibbleAddr & 1u) != 0u) { packed = static_cast((packed & 0x0Fu) | ((index & 0x0Fu) << 4)); } else { packed = static_cast((packed & 0xF0u) | (index & 0x0Fu)); } } uint32_t referenceAddrPSMT4(uint32_t block, uint32_t width, uint32_t x, uint32_t y) { static constexpr uint8_t kBlockTable4[8][4] = { {0, 2, 8, 10}, {1, 3, 9, 11}, {4, 6, 12, 14}, {5, 7, 13, 15}, {16, 18, 24, 26}, {17, 19, 25, 27}, {20, 22, 28, 30}, {21, 23, 29, 31}, }; static constexpr uint16_t kColumnTable4[16][32] = { {0, 8, 32, 40, 64, 72, 96, 104, 2, 10, 34, 42, 66, 74, 98, 106, 4, 12, 36, 44, 68, 76, 100, 108, 6, 14, 38, 46, 70, 78, 102, 110}, {16, 24, 48, 56, 80, 88, 112, 120, 18, 26, 50, 58, 82, 90, 114, 122, 20, 28, 52, 60, 84, 92, 116, 124, 22, 30, 54, 62, 86, 94, 118, 126}, {65, 73, 97, 105, 1, 9, 33, 41, 67, 75, 99, 107, 3, 11, 35, 43, 69, 77, 101, 109, 5, 13, 37, 45, 71, 79, 103, 111, 7, 15, 39, 47}, {81, 89, 113, 121, 17, 25, 49, 57, 83, 91, 115, 123, 19, 27, 51, 59, 85, 93, 117, 125, 21, 29, 53, 61, 87, 95, 119, 127, 23, 31, 55, 63}, {192, 200, 224, 232, 128, 136, 160, 168, 194, 202, 226, 234, 130, 138, 162, 170, 196, 204, 228, 236, 132, 140, 164, 172, 198, 206, 230, 238, 134, 142, 166, 174}, {208, 216, 240, 248, 144, 152, 176, 184, 210, 218, 242, 250, 146, 154, 178, 186, 212, 220, 244, 252, 148, 156, 180, 188, 214, 222, 246, 254, 150, 158, 182, 190}, {129, 137, 161, 169, 193, 201, 225, 233, 131, 139, 163, 171, 195, 203, 227, 235, 133, 141, 165, 173, 197, 205, 229, 237, 135, 143, 167, 175, 199, 207, 231, 239}, {145, 153, 177, 185, 209, 217, 241, 249, 147, 155, 179, 187, 211, 219, 243, 251, 149, 157, 181, 189, 213, 221, 245, 253, 151, 159, 183, 191, 215, 223, 247, 255}, {256, 264, 288, 296, 320, 328, 352, 360, 258, 266, 290, 298, 322, 330, 354, 362, 260, 268, 292, 300, 324, 332, 356, 364, 262, 270, 294, 302, 326, 334, 358, 366}, {272, 280, 304, 312, 336, 344, 368, 376, 274, 282, 306, 314, 338, 346, 370, 378, 276, 284, 308, 316, 340, 348, 372, 380, 278, 286, 310, 318, 342, 350, 374, 382}, {321, 329, 353, 361, 257, 265, 289, 297, 323, 331, 355, 363, 259, 267, 291, 299, 325, 333, 357, 365, 261, 269, 293, 301, 327, 335, 359, 367, 263, 271, 295, 303}, {337, 345, 369, 377, 273, 281, 305, 313, 339, 347, 371, 379, 275, 283, 307, 315, 341, 349, 373, 381, 277, 285, 309, 317, 343, 351, 375, 383, 279, 287, 311, 319}, {448, 456, 480, 488, 384, 392, 416, 424, 450, 458, 482, 490, 386, 394, 418, 426, 452, 460, 484, 492, 388, 396, 420, 428, 454, 462, 486, 494, 390, 398, 422, 430}, {464, 472, 496, 504, 400, 408, 432, 440, 466, 474, 498, 506, 402, 410, 434, 442, 468, 476, 500, 508, 404, 412, 436, 444, 470, 478, 502, 510, 406, 414, 438, 446}, {385, 393, 417, 425, 449, 457, 481, 489, 387, 395, 419, 427, 451, 459, 483, 491, 389, 397, 421, 429, 453, 461, 485, 493, 391, 399, 423, 431, 455, 463, 487, 495}, {401, 409, 433, 441, 465, 473, 497, 505, 403, 411, 435, 443, 467, 475, 499, 507, 405, 413, 437, 445, 469, 477, 501, 509, 407, 415, 439, 447, 471, 479, 503, 511}, }; const uint32_t pagesPerRow = ((width >> 1u) != 0u) ? (width >> 1u) : 1u; const uint32_t page = (block >> 5u) + (y >> 7u) * pagesPerRow + (x >> 7u); const uint32_t blockId = (block & 0x1Fu) + kBlockTable4[(y >> 4u) & 7u][(x >> 5u) & 3u]; const uint32_t pageOffset = (blockId >> 5u) << 14u; const uint32_t localBlock = blockId & 0x1Fu; return (page << 14u) + pageOffset + localBlock * 512u + kColumnTable4[y & 0x0Fu][x & 0x1Fu]; } void writeReferencePSMT4Texel(std::vector &vram, uint32_t tbp, uint32_t tbw, uint32_t x, uint32_t y, uint8_t index) { const uint32_t nibbleAddr = referenceAddrPSMT4(tbp, tbw, x, y); const uint32_t byteOff = nibbleAddr >> 1; uint8_t &packed = vram[byteOff]; if ((nibbleAddr & 1u) != 0u) { packed = static_cast((packed & 0x0Fu) | ((index & 0x0Fu) << 4)); } else { packed = static_cast((packed & 0xF0u) | (index & 0x0Fu)); } } uint32_t referenceAddrPSMT8(uint32_t block, uint32_t width, uint32_t x, uint32_t y) { static constexpr uint8_t kBlockTable8[4][8] = { {0, 1, 4, 5, 16, 17, 20, 21}, {2, 3, 6, 7, 18, 19, 22, 23}, {8, 9, 12, 13, 24, 25, 28, 29}, {10, 11, 14, 15, 26, 27, 30, 31}, }; static constexpr uint8_t kColumnTable8[16][16] = { {0, 4, 16, 20, 32, 36, 48, 52, 2, 6, 18, 22, 34, 38, 50, 54}, {8, 12, 24, 28, 40, 44, 56, 60, 10, 14, 26, 30, 42, 46, 58, 62}, {33, 37, 49, 53, 1, 5, 17, 21, 35, 39, 51, 55, 3, 7, 19, 23}, {41, 45, 57, 61, 9, 13, 25, 29, 43, 47, 59, 63, 11, 15, 27, 31}, {96, 100, 112, 116, 64, 68, 80, 84, 98, 102, 114, 118, 66, 70, 82, 86}, {104, 108, 120, 124, 72, 76, 88, 92, 106, 110, 122, 126, 74, 78, 90, 94}, {65, 69, 81, 85, 97, 101, 113, 117, 67, 71, 83, 87, 99, 103, 115, 119}, {73, 77, 89, 93, 105, 109, 121, 125, 75, 79, 91, 95, 107, 111, 123, 127}, {128, 132, 144, 148, 160, 164, 176, 180, 130, 134, 146, 150, 162, 166, 178, 182}, {136, 140, 152, 156, 168, 172, 184, 188, 138, 142, 154, 158, 170, 174, 186, 190}, {161, 165, 177, 181, 129, 133, 145, 149, 163, 167, 179, 183, 131, 135, 147, 151}, {169, 173, 185, 189, 137, 141, 153, 157, 171, 175, 187, 191, 139, 143, 155, 159}, {224, 228, 240, 244, 192, 196, 208, 212, 226, 230, 242, 246, 194, 198, 210, 214}, {232, 236, 248, 252, 200, 204, 216, 220, 234, 238, 250, 254, 202, 206, 218, 222}, {193, 197, 209, 213, 225, 229, 241, 245, 195, 199, 211, 215, 227, 231, 243, 247}, {201, 205, 217, 221, 233, 237, 249, 253, 203, 207, 219, 223, 235, 239, 251, 255}, }; const uint32_t pagesPerRow = ((width >> 1u) != 0u) ? (width >> 1u) : 1u; const uint32_t page = (block >> 5u) + (y >> 6u) * pagesPerRow + (x >> 7u); const uint32_t blockId = (block & 0x1Fu) + kBlockTable8[(y >> 4u) & 3u][(x >> 4u) & 7u]; const uint32_t pageOffset = (blockId >> 5u) << 13u; const uint32_t localBlock = blockId & 0x1Fu; return (page << 13u) + pageOffset + localBlock * 256u + kColumnTable8[y & 0x0Fu][x & 0x0Fu]; } uint32_t referenceAddrPSMCT32(uint32_t block, uint32_t width, uint32_t x, uint32_t y) { static constexpr uint8_t kBlockTable32[4][8] = { {0, 1, 4, 5, 16, 17, 20, 21}, {2, 3, 6, 7, 18, 19, 22, 23}, {8, 9, 12, 13, 24, 25, 28, 29}, {10, 11, 14, 15, 26, 27, 30, 31}, }; static constexpr uint8_t kColumnTable32[8][8] = { {0, 1, 4, 5, 8, 9, 12, 13}, {2, 3, 6, 7, 10, 11, 14, 15}, {16, 17, 20, 21, 24, 25, 28, 29}, {18, 19, 22, 23, 26, 27, 30, 31}, {32, 33, 36, 37, 40, 41, 44, 45}, {34, 35, 38, 39, 42, 43, 46, 47}, {48, 49, 52, 53, 56, 57, 60, 61}, {50, 51, 54, 55, 58, 59, 62, 63}, }; const uint32_t pagesPerRow = (width != 0u) ? width : 1u; const uint32_t page = (block >> 5u) + (y >> 5u) * pagesPerRow + (x >> 6u); const uint32_t blockId = (block & 0x1Fu) + kBlockTable32[(y >> 3u) & 3u][(x >> 3u) & 7u]; const uint32_t pageOffset = (blockId >> 5u) << 13u; const uint32_t localBlock = blockId & 0x1Fu; return (page << 13u) + pageOffset + localBlock * 256u + static_cast(kColumnTable32[y & 0x7u][x & 0x7u]) * 4u; } void writeReferencePSMCT32Pixel(std::vector &vram, uint32_t fbp, uint32_t fbw, uint32_t x, uint32_t y, uint32_t pixel) { const uint32_t off = referenceAddrPSMCT32(fbp, (fbw != 0u) ? fbw : 1u, x, y); std::memcpy(vram.data() + off, &pixel, sizeof(pixel)); } uint32_t readReferencePSMCT32Pixel(const std::vector &vram, uint32_t fbp, uint32_t fbw, uint32_t x, uint32_t y) { const uint32_t off = referenceAddrPSMCT32(fbp, (fbw != 0u) ? fbw : 1u, x, y); uint32_t pixel = 0u; std::memcpy(&pixel, vram.data() + off, sizeof(pixel)); return pixel; } uint32_t frameBaseToBlock(uint32_t fbp) { return fbp << 5u; } void writeReferenceFramePSMCT32Pixel(std::vector &vram, uint32_t fbp, uint32_t fbw, uint32_t x, uint32_t y, uint32_t pixel) { writeReferencePSMCT32Pixel(vram, frameBaseToBlock(fbp), fbw, x, y, pixel); } uint32_t readReferenceFramePSMCT32Pixel(const std::vector &vram, uint32_t fbp, uint32_t fbw, uint32_t x, uint32_t y) { return readReferencePSMCT32Pixel(vram, frameBaseToBlock(fbp), fbw, x, y); } void expectGuestHeapReusable(TestCase &t, PS2Runtime &runtime, const char *message) { const uint32_t expectedBase = runtime.guestHeapBase(); const uint32_t probe = runtime.guestMalloc(16u, 16u); t.Equals(probe, expectedBase, message); runtime.guestFree(probe); } struct GsPixelTestResult { uint32_t framebuffer = 0u; uint32_t depth = 0u; }; GsPixelTestResult drawGsPixelForTests(uint8_t framePsm, uint64_t testReg, bool zmask, uint32_t initialFramebuffer, uint32_t initialDepth, uint8_t sourceAlpha) { constexpr uint32_t kFrameBlock = 0u; constexpr uint32_t kDepthBlock = 32u; constexpr uint32_t kSourceDepth = 0x22222222u; std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); gs.WriteVram(framePsm, kFrameBlock, 1u, 0u, 0u, initialFramebuffer); gs.WriteVram(GS_PSM_Z32, kDepthBlock, 1u, 0u, 0u, initialDepth); const uint64_t frame = (1ull << 16) | (static_cast(framePsm) << 24); const uint64_t zbuf = 1ull | (static_cast(zmask ? 1u : 0u) << 32); const uint64_t rgbaq = (0x12ull << 0) | (0x34ull << 8) | (0x56ull << 16) | (static_cast(sourceAlpha) << 24) | (0x3F800000ull << 32); gs.writeRegister(GS_REG_FRAME_1, frame); gs.writeRegister(GS_REG_ZBUF_1, zbuf); gs.writeRegister(GS_REG_SCISSOR_1, 0ull); gs.writeRegister(GS_REG_TEST_1, testReg); gs.writeRegister(GS_REG_PRIM, static_cast(GS_PRIM_POINT)); gs.writeRegister(GS_REG_RGBAQ, rgbaq); gs.writeRegister(GS_REG_XYZ2, static_cast(kSourceDepth) << 32); return { gs.ReadVram(framePsm, kFrameBlock, 1u, 0u, 0u), gs.ReadVram(GS_PSM_Z32, kDepthBlock, 1u, 0u, 0u), }; } } void register_ps2_gs_tests() { MiniTest::Case("PS2GS", [](TestCase &tc) { tc.Run("GS CSR/IMR support coherent 64-bit and 32-bit access", [](TestCase &t) { PS2Memory mem; t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); constexpr uint32_t kGsCsr = 0x12001000u; constexpr uint32_t kGsImr = 0x12001010u; const uint64_t csrPattern = 0xA1B2C3D4E5F60718ull; mem.write64(kGsCsr, csrPattern); t.Equals(mem.read64(kGsCsr), csrPattern, "64-bit CSR read should match prior 64-bit write"); t.Equals(mem.read32(kGsCsr), static_cast(csrPattern & 0xFFFFFFFFull), "CSR low dword read should match"); t.Equals(mem.read32(kGsCsr + 4u), static_cast(csrPattern >> 32), "CSR high dword read should match"); mem.write32(kGsCsr, 0x11223344u); t.Equals(mem.read64(kGsCsr), 0xA1B2C3D411223344ull, "32-bit low write should preserve CSR high dword"); mem.write32(kGsCsr + 4u, 0x55667788u); t.Equals(mem.read64(kGsCsr), 0x5566778811223344ull, "32-bit high write should preserve CSR low dword"); const uint64_t imrPattern = 0x0123456789ABCDEFull; mem.write64(kGsImr, imrPattern); t.Equals(mem.read64(kGsImr), imrPattern, "IMR 64-bit read should match prior write"); t.Equals(mem.read32(kGsImr), 0x89ABCDEFu, "IMR low dword should match"); t.Equals(mem.read32(kGsImr + 4u), 0x01234567u, "IMR high dword should match"); }); tc.Run("unknown GS privileged offsets are no-op and read as zero", [](TestCase &t) { PS2Memory mem; t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); constexpr uint32_t kKnownBusdir = 0x12001040u; constexpr uint32_t kUnknown = 0x12001008u; // inside GS priv range, but not mapped by gsRegPtr. mem.write64(kKnownBusdir, 0xCAFEBABE12345678ull); const uint64_t before = mem.read64(kKnownBusdir); mem.write32(kUnknown, 0xDEADBEEFu); t.Equals(mem.read32(kUnknown), 0u, "unknown GS offset should read as zero"); t.Equals(mem.read64(kKnownBusdir), before, "unknown GS writes should not corrupt mapped GS registers"); }); tc.Run("GS writeIORegister increments GS write counter", [](TestCase &t) { PS2Memory mem; t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); constexpr uint32_t kGsPmode = 0x12000000u; constexpr uint32_t kGsImr = 0x12001010u; const uint64_t countBefore = mem.gsWriteCount(); t.IsTrue(mem.writeIORegister(kGsPmode, 0x11u), "writeIORegister PMODE should succeed"); t.IsTrue(mem.writeIORegister(kGsImr, 0x22u), "writeIORegister IMR should succeed"); t.Equals(mem.gsWriteCount(), countBefore + 2ull, "GS IO writes should increment GS write counter"); t.Equals(mem.readIORegister(kGsPmode), 0x11u, "writeIORegister PMODE value should be readable"); t.Equals(mem.readIORegister(kGsImr), 0x22u, "writeIORegister IMR value should be readable"); }); tc.Run("GsPutIMR and GsGetIMR roundtrip old and new values", [](TestCase &t) { PS2Runtime runtime; t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); runtime.memory().gs().imr = 0xAAAABBBBCCCCDDDDull; std::vector rdram(PS2_RAM_SIZE, 0u); R5900Context ctx{}; setRegU64(ctx, 4, 0x3333444411112222ull); GsPutIMR(rdram.data(), &ctx, &runtime); const uint64_t oldImr = getReturnU64(ctx); t.Equals(oldImr, 0xAAAABBBBCCCCDDDDull, "GsPutIMR should return previous IMR"); t.Equals(runtime.memory().gs().imr, 0x3333444411112222ull, "GsPutIMR should update GS IMR"); std::memset(&ctx, 0, sizeof(ctx)); GsGetIMR(rdram.data(), &ctx, &runtime); const uint64_t currentImr = getReturnU64(ctx); t.Equals(currentImr, 0x3333444411112222ull, "GsGetIMR should return current GS IMR"); }); tc.Run("GsSetCrt updates SMODE2 for host presentation mode", [](TestCase &t) { PS2Runtime runtime; t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); std::vector rdram(PS2_RAM_SIZE, 0u); R5900Context ctx{}; setRegU32(ctx, 4, 1u); // interlaced setRegU32(ctx, 5, 0u); // NTSC setRegU32(ctx, 6, 0u); // field mode runtime.memory().gs().pmode = 0u; runtime.memory().gs().smode2 = 0u; GsSetCrt(rdram.data(), &ctx, &runtime); t.Equals(runtime.memory().gs().smode2, 0x1ull, "GsSetCrt should publish interlaced field mode through SMODE2"); t.Equals(runtime.memory().gs().pmode & 0x3ull, 0x1ull, "GsSetCrt should leave CRT1 enabled for presentation"); t.Equals(getRegU32Test(ctx, 2), 0u, "GsSetCrt should return success"); }); tc.Run("sceGsSetDefDBuffDc seeds display envs and swap applies the selected page", [](TestCase &t) { PS2Runtime runtime; t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); std::vector rdram(PS2_RAM_SIZE, 0u); constexpr uint32_t kEnvAddr = 0x4000u; constexpr uint32_t kDispEnvSize = 40u; constexpr uint32_t kDBuffSize = 0x330u; constexpr uint32_t kDispFbOffset = 16u; constexpr uint32_t kDisplayOffset = 24u; constexpr uint32_t kDraw01Offset = 0x60u; constexpr uint32_t kFrame1Offset = kDraw01Offset + 0x00u; constexpr uint32_t kFrame1AddrOffset = kDraw01Offset + 0x08u; constexpr uint32_t kXYOffset1Offset = kDraw01Offset + 0x20u; constexpr uint32_t kXYOffset1AddrOffset = kDraw01Offset + 0x28u; R5900Context ctx{}; setRegU32(ctx, 4, kEnvAddr); setRegU32(ctx, 5, 0u); setRegU32(ctx, 6, 640u); setRegU32(ctx, 7, 448u); std::memset(rdram.data() + kEnvAddr, 0xCD, kDBuffSize); ps2_stubs::sceGsSetDefDBuffDc(rdram.data(), &ctx, &runtime); uint64_t dispfb0 = 0u; uint64_t display0 = 0u; uint64_t frame10 = 0u; uint64_t frame10Addr = 0u; uint64_t xyoffset10 = 0u; uint64_t xyoffset10Addr = 0u; std::memcpy(&dispfb0, rdram.data() + kEnvAddr + kDispFbOffset, sizeof(dispfb0)); std::memcpy(&display0, rdram.data() + kEnvAddr + kDisplayOffset, sizeof(display0)); std::memcpy(&frame10, rdram.data() + kEnvAddr + kFrame1Offset, sizeof(frame10)); std::memcpy(&frame10Addr, rdram.data() + kEnvAddr + kFrame1AddrOffset, sizeof(frame10Addr)); std::memcpy(&xyoffset10, rdram.data() + kEnvAddr + kXYOffset1Offset, sizeof(xyoffset10)); std::memcpy(&xyoffset10Addr, rdram.data() + kEnvAddr + kXYOffset1AddrOffset, sizeof(xyoffset10Addr)); t.Equals((dispfb0 >> 9) & 0x3Fu, 10ull, "dbuff display env should seed FBW from width"); t.Equals((display0 >> 32) & 0x0FFFull, 639ull, "dbuff display env should seed DW from width"); t.Equals((display0 >> 44) & 0x07FFull, 447ull, "dbuff display env should seed DH from height"); t.Equals((frame10 >> 16) & 0x3Full, 10ull, "dbuff draw env should seed FRAME FBW from width"); t.Equals(frame10Addr, 0x4Cull, "dbuff draw env should seed FRAME_1 register id"); t.Equals(xyoffset10 & 0xFFFFull, 0x6C00ull, "dbuff draw env should seed OFX in 12.4 fixed point"); t.Equals((xyoffset10 >> 32) & 0xFFFFull, 0x7200ull, "dbuff draw env should seed OFY in 12.4 fixed point"); t.Equals(xyoffset10Addr, 0x18ull, "dbuff draw env should seed XYOFFSET_1 register id"); dispfb0 = (dispfb0 & ~0x1FFull) | 150ull; std::memcpy(rdram.data() + kEnvAddr + kDispFbOffset, &dispfb0, sizeof(dispfb0)); uint64_t dispfb1 = dispfb0; dispfb1 = (dispfb1 & ~0x1FFull) | 151ull; std::memcpy(rdram.data() + kEnvAddr + kDispEnvSize + kDispFbOffset, &dispfb1, sizeof(dispfb1)); std::memset(&ctx, 0, sizeof(ctx)); setRegU32(ctx, 4, kEnvAddr); setRegU32(ctx, 5, 1u); ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, &runtime); t.Equals(runtime.memory().gs().dispfb1 & 0x1FFull, 151ull, "sceGsSwapDBuffDc should program GS to the selected display page"); t.Equals((runtime.memory().gs().display1 >> 32) & 0x0FFFull, 639ull, "sceGsSwapDBuffDc should preserve the display width from the seeded env"); }); tc.Run("sceGsSetDefDBuffDc seeds a clear packet and swap clears the draw buffer", [](TestCase &t) { PS2Runtime runtime; t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); std::vector rdram(PS2_RAM_SIZE, 0u); constexpr uint32_t kEnvAddr = 0x5000u; constexpr uint32_t kDBuffSize = 0x330u; constexpr uint32_t kClear0Offset = 0x160u; constexpr uint32_t kTestAAddrOffset = kClear0Offset + 0x08u; constexpr uint32_t kPrimAddrOffset = kClear0Offset + 0x18u; constexpr uint32_t kRgbaqOffset = kClear0Offset + 0x20u; constexpr uint32_t kRgbaqAddrOffset = kClear0Offset + 0x28u; constexpr uint32_t kXyz2AAddrOffset = kClear0Offset + 0x38u; constexpr uint32_t kXyz2BAddrOffset = kClear0Offset + 0x48u; constexpr uint32_t kTestBAddrOffset = kClear0Offset + 0x58u; constexpr uint32_t kClearColor = 0x80402010u; constexpr uint32_t kStackAddr = 0x900u; const uint32_t kZTest = 2u; const uint32_t kEnableClear = 1u; R5900Context ctx{}; setRegU32(ctx, 4, kEnvAddr); setRegU32(ctx, 5, 0u); setRegU32(ctx, 6, 640u); setRegU32(ctx, 7, 448u); setRegU32(ctx, 29, kStackAddr); std::memset(rdram.data() + kEnvAddr, 0xCD, kDBuffSize); std::memcpy(rdram.data() + kStackAddr + 16u, &kZTest, sizeof(kZTest)); std::memcpy(rdram.data() + kStackAddr + 24u, &kEnableClear, sizeof(kEnableClear)); std::memset(runtime.memory().getGSVRAM(), 0xAB, 16u); ps2_stubs::sceGsSetDefDBuffDc(rdram.data(), &ctx, &runtime); uint64_t testAAddr = 0u; uint64_t primAddr = 0u; uint64_t rgbaqAddr = 0u; uint64_t xyz2AAddr = 0u; uint64_t xyz2BAddr = 0u; uint64_t testBAddr = 0u; std::memcpy(&testAAddr, rdram.data() + kEnvAddr + kTestAAddrOffset, sizeof(testAAddr)); std::memcpy(&primAddr, rdram.data() + kEnvAddr + kPrimAddrOffset, sizeof(primAddr)); std::memcpy(&rgbaqAddr, rdram.data() + kEnvAddr + kRgbaqAddrOffset, sizeof(rgbaqAddr)); std::memcpy(&xyz2AAddr, rdram.data() + kEnvAddr + kXyz2AAddrOffset, sizeof(xyz2AAddr)); std::memcpy(&xyz2BAddr, rdram.data() + kEnvAddr + kXyz2BAddrOffset, sizeof(xyz2BAddr)); std::memcpy(&testBAddr, rdram.data() + kEnvAddr + kTestBAddrOffset, sizeof(testBAddr)); t.Equals(testAAddr, 0x47ull, "dbuff clear packet should program TEST_1 before clearing"); t.Equals(primAddr, 0x00ull, "dbuff clear packet should program PRIM before clearing"); t.Equals(rgbaqAddr, 0x01ull, "dbuff clear packet should expose RGBAQ for runtime color updates"); t.Equals(xyz2AAddr, 0x05ull, "dbuff clear packet should seed the first clear vertex as XYZ2"); t.Equals(xyz2BAddr, 0x05ull, "dbuff clear packet should seed the second clear vertex as XYZ2"); t.Equals(testBAddr, 0x47ull, "dbuff clear packet should restore TEST_1 after clearing"); uint64_t rgbaq = static_cast(kClearColor); std::memcpy(rdram.data() + kEnvAddr + kRgbaqOffset, &rgbaq, sizeof(rgbaq)); std::memset(&ctx, 0, sizeof(ctx)); setRegU32(ctx, 4, kEnvAddr); setRegU32(ctx, 5, 0u); ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, &runtime); uint32_t clearedPixel = 0u; std::memcpy(&clearedPixel, runtime.memory().getGSVRAM(), sizeof(clearedPixel)); t.Equals(clearedPixel, kClearColor, "sceGsSwapDBuffDc should execute the seeded clear packet against the active draw buffer"); constexpr uint32_t kMidX = 320u; constexpr uint32_t kMidY = 200u; const uint32_t kMidOffset = ((kMidY * 640u) + kMidX) * 4u; uint32_t clearedMidPixel = 0u; std::memcpy(&clearedMidPixel, runtime.memory().getGSVRAM() + kMidOffset, sizeof(clearedMidPixel)); t.Equals(clearedMidPixel, kClearColor, "sceGsSwapDBuffDc should clear the interior of the active draw buffer, not just the first pixel"); }); tc.Run("sceGsSetDefDBuffDc accepts trailing args from the recompiler register ABI", [](TestCase &t) { PS2Runtime runtime; t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); std::vector rdram(PS2_RAM_SIZE, 0u); constexpr uint32_t kEnvAddr = 0x5400u; constexpr uint32_t kDBuffSize = 0x330u; constexpr uint32_t kClear0Offset = 0x160u; constexpr uint32_t kRgbaqOffset = kClear0Offset + 0x20u; constexpr uint32_t kRgbaqAddrOffset = kClear0Offset + 0x28u; constexpr uint32_t kStackAddr = 0xA00u; constexpr uint32_t kClearColor = 0x40201008u; R5900Context ctx{}; setRegU32(ctx, 4, kEnvAddr); setRegU32(ctx, 5, 0u); setRegU32(ctx, 6, 640u); setRegU32(ctx, 7, 448u); setRegU32(ctx, 8, 2u); setRegU32(ctx, 9, 58u); setRegU32(ctx, 10, 1u); setRegU32(ctx, 29, kStackAddr); std::memset(rdram.data() + kEnvAddr, 0xCD, kDBuffSize); std::memset(runtime.memory().getGSVRAM(), 0xAB, 16u); ps2_stubs::sceGsSetDefDBuffDc(rdram.data(), &ctx, &runtime); uint64_t rgbaqAddr = 0u; std::memcpy(&rgbaqAddr, rdram.data() + kEnvAddr + kRgbaqAddrOffset, sizeof(rgbaqAddr)); t.Equals(rgbaqAddr, 0x01ull, "sceGsSetDefDBuffDc should seed the clear packet when trailing args arrive in t0-t2"); const uint64_t rgbaq = static_cast(kClearColor); std::memcpy(rdram.data() + kEnvAddr + kRgbaqOffset, &rgbaq, sizeof(rgbaq)); std::memset(&ctx, 0, sizeof(ctx)); setRegU32(ctx, 4, kEnvAddr); setRegU32(ctx, 5, 0u); ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, &runtime); uint32_t clearedPixel = 0u; std::memcpy(&clearedPixel, runtime.memory().getGSVRAM(), sizeof(clearedPixel)); t.Equals(clearedPixel, kClearColor, "sceGsSwapDBuffDc should honor a clear packet seeded from register-based trailing args"); }); tc.Run("clearFramebufferContext clears the requested context even if another context is active", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kCtx0Color = 0x11223344u; constexpr uint32_t kCtx1Sentinel = 0xAABBCCDDu; gs.writeRegister(GS_REG_FRAME_1, (1ull << 16)); // FBP=0, FBW=1, PSMCT32 gs.writeRegister(GS_REG_SCISSOR_1, (0ull << 0) | (0ull << 16) | (1ull << 32) | (1ull << 48)); gs.writeRegister(GS_REG_FRAME_2, 150ull | (1ull << 16)); gs.writeRegister(GS_REG_SCISSOR_2, (0ull << 0) | (0ull << 16) | (1ull << 32) | (1ull << 48)); gs.writeRegister(GS_REG_PRIM, static_cast(GS_PRIM_POINT) | (1ull << 9)); writeReferenceFramePSMCT32Pixel(vram, 150u, 1u, 0u, 1u, kCtx1Sentinel); t.IsTrue(gs.clearFramebufferContext(0u, kCtx0Color), "context-targeted clear should succeed for a configured CT32 framebuffer"); const uint32_t ctx0Pixel = readReferenceFramePSMCT32Pixel(vram, 0u, 1u, 0u, 1u); t.Equals(ctx0Pixel, kCtx0Color, "context-targeted clear should write the requested context even when PRIM.ctxt points elsewhere"); const uint32_t ctx1Pixel = readReferenceFramePSMCT32Pixel(vram, 150u, 1u, 0u, 1u); t.Equals(ctx1Pixel, kCtx1Sentinel, "context-targeted clear should leave the other context framebuffer untouched"); }); tc.Run("XYZ3 culls a triangle strip primitive without desynchronizing the vertex queue", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kColor = 0xFF0000FFu; constexpr uint64_t kFrame = (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kScissor = (6ull << 16) | (6ull << 48); auto xyz = [](uint32_t x, uint32_t y) -> uint64_t { return static_cast(x * 16u) | (static_cast(y * 16u) << 16); }; gs.writeRegister(GS_REG_FRAME_1, kFrame); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, kScissor); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_PRIM, static_cast(GS_PRIM_TRISTRIP)); gs.writeRegister(GS_REG_RGBAQ, kColor); // ABC is rejected by XYZ3. D must then draw BCD, not stale ABC. gs.writeRegister(GS_REG_XYZ2, xyz(0u, 0u)); gs.writeRegister(GS_REG_XYZ2, xyz(6u, 0u)); gs.writeRegister(GS_REG_XYZ3, xyz(0u, 6u)); gs.writeRegister(GS_REG_XYZ2, xyz(6u, 6u)); t.Equals(readReferencePSMCT32Pixel(vram, 0u, 1u, 1u, 1u), 0u, "XYZ3 should suppress the completed ABC triangle"); t.Equals(readReferencePSMCT32Pixel(vram, 0u, 1u, 4u, 4u), kColor, "the next XYZ2 should draw BCD from the advanced strip queue"); }); tc.Run("GS fog blends the shaded color toward FOGCOL before framebuffer blending", [](TestCase &t) { auto renderFoggedPoint = [](bool fogEnabled, uint8_t fog, uint32_t fogColor = 0u) -> uint32_t { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint64_t kFrame = (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kWhite = 0x80FFFFFFull; gs.writeRegister(GS_REG_FRAME_1, kFrame); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, 0ull); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_FOGCOL, fogColor); gs.writeRegister( GS_REG_PRIM, static_cast(GS_PRIM_POINT) | (static_cast(fogEnabled ? 1u : 0u) << 5)); gs.writeRegister(GS_REG_RGBAQ, kWhite); gs.writeRegister(GS_REG_FOG, static_cast(fog) << 56); gs.writeRegister(GS_REG_XYZ2, 0ull); return readReferencePSMCT32Pixel(vram, 0u, 1u, 0u, 0u); }; t.Equals(renderFoggedPoint(false, 0x80u), 0x80FFFFFFu, "FOG and FOGCOL must not affect primitives with FGE disabled"); t.Equals(renderFoggedPoint(true, 0x80u), 0x807F7F7Fu, "F=0x80 over black FOGCOL should halve the point RGB and preserve alpha"); t.Equals(renderFoggedPoint(true, 0x00u), 0x80000000u, "F=0 should replace the point RGB with black FOGCOL"); t.Equals(renderFoggedPoint(true, 0x00u, 0x00302010u), 0x802F1F0Fu, "F=0 should replace point RGB with the programmed FOGCOL"); }); tc.Run("PRMODE supplies primitive attributes while PRMODECONT AC is clear", [](TestCase &t) { auto renderPoint = [](bool usePrmodeAttributes) -> uint32_t { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint64_t kFrame = (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); gs.writeRegister(GS_REG_FRAME_1, kFrame); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, 0ull); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_FOGCOL, 0ull); gs.writeRegister(GS_REG_RGBAQ, 0x80FFFFFFull); gs.writeRegister(GS_REG_FOG, 0ull); gs.writeRegister(GS_REG_PRMODE, 1ull << 5); gs.writeRegister(GS_REG_PRMODECONT, usePrmodeAttributes ? 0ull : 1ull); // FGE is clear in PRIM. AC decides whether that clear bit or // PRMODE's set bit supplies the effective fog enable. gs.writeRegister(GS_REG_PRIM, static_cast(GS_PRIM_POINT)); gs.writeRegister(GS_REG_XYZ2, 0ull); return readReferencePSMCT32Pixel(vram, 0u, 1u, 0u, 0u); }; t.Equals(renderPoint(true), 0x80000000u, "AC=0 should retain FGE from PRMODE across a PRIM write"); t.Equals(renderPoint(false), 0x80FFFFFFu, "AC=1 should source FGE from PRIM instead of PRMODE"); }); tc.Run("PABE bypasses alpha blend for low-alpha source pixels", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); gs.writeRegister(GS_REG_FRAME_1, (1ull << 16)); // FBW=1, PSMCT32, FBP=0 gs.writeRegister(GS_REG_ZBUF_1, (1ull << 32)); gs.writeRegister(GS_REG_SCISSOR_1, 0ull); gs.writeRegister(GS_REG_ALPHA_1, 0x6000000064ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_PRIM, static_cast(GS_PRIM_POINT) | (1ull << 6)); const uint32_t dstWhite = 0xFFFFFFFFu; std::memcpy(vram.data(), &dstWhite, sizeof(dstWhite)); gs.writeRegister(GS_REG_PABE, 0ull); gs.writeRegister(GS_REG_RGBAQ, 0x01000000ull); gs.writeRegister(GS_REG_XYZ2, 0ull); uint32_t blendedPixel = 0u; std::memcpy(&blendedPixel, vram.data(), sizeof(blendedPixel)); t.Equals(blendedPixel, 0x013F3F3Fu, "without PABE, low-alpha fullscreen copies should still apply ALPHA blending"); std::memcpy(vram.data(), &dstWhite, sizeof(dstWhite)); gs.writeRegister(GS_REG_PRIM, static_cast(GS_PRIM_POINT) | (1ull << 6)); gs.writeRegister(GS_REG_PABE, 1ull); gs.writeRegister(GS_REG_RGBAQ, 0x01000000ull); gs.writeRegister(GS_REG_XYZ2, 0ull); uint32_t pabeBypassedPixel = 0u; std::memcpy(&pabeBypassedPixel, vram.data(), sizeof(pabeBypassedPixel)); t.Equals(pabeBypassedPixel, 0x01000000u, "with PABE enabled, low-alpha source pixels should bypass ALPHA blending and overwrite the destination"); std::memcpy(vram.data(), &dstWhite, sizeof(dstWhite)); gs.writeRegister(GS_REG_PRIM, static_cast(GS_PRIM_POINT) | (1ull << 6)); gs.writeRegister(GS_REG_PABE, 1ull); gs.writeRegister(GS_REG_RGBAQ, 0x80000000ull); gs.writeRegister(GS_REG_XYZ2, 0ull); uint32_t highAlphaPixel = 0u; std::memcpy(&highAlphaPixel, vram.data(), sizeof(highAlphaPixel)); t.Equals(highAlphaPixel, 0x803F3F3Fu, "with PABE enabled, high-alpha source pixels should still use the configured ALPHA blend"); }); tc.Run("FBA forces the framebuffer alpha high bit on CT32 writes", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); gs.writeRegister(GS_REG_FRAME_1, (1ull << 16)); gs.writeRegister(GS_REG_ZBUF_1, (1ull) << 32); gs.writeRegister(GS_REG_SCISSOR_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_PRIM, static_cast(GS_PRIM_POINT)); gs.writeRegister(GS_REG_FBA_1, 0ull); gs.writeRegister(GS_REG_RGBAQ, 0x01112233ull); gs.writeRegister(GS_REG_XYZ2, 0ull); uint32_t pixelWithoutFba = 0u; std::memcpy(&pixelWithoutFba, vram.data(), sizeof(pixelWithoutFba)); t.Equals(pixelWithoutFba, 0x01112233u, "without FBA, CT32 writes should preserve the source alpha byte"); std::memset(vram.data(), 0, sizeof(uint32_t)); gs.writeRegister(GS_REG_PRIM, static_cast(GS_PRIM_POINT)); gs.writeRegister(GS_REG_FBA_1, 1ull); gs.writeRegister(GS_REG_RGBAQ, 0x01112233ull); gs.writeRegister(GS_REG_XYZ2, 0ull); uint32_t pixelWithFba = 0u; std::memcpy(&pixelWithFba, vram.data(), sizeof(pixelWithFba)); t.Equals(pixelWithFba, 0x81112233u, "with FBA enabled, CT32 writes should force the framebuffer alpha high bit"); }); tc.Run("CT32 raster writes alias cleanly into later CT32 texture sampling", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint64_t kFrame1 = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf1 = (1ull << 32); constexpr uint64_t kScissor1 = (0ull << 0) | (1ull << 16) | (0ull << 32) | (1ull << 48); constexpr uint64_t kFrame2 = (150ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf2 = (1ull << 32); constexpr uint64_t kScissor2 = 0ull; constexpr uint64_t kTex0_2 = (0ull << 0) | (1ull << 14) | (static_cast(GS_PSM_CT32) << 20) | (0ull << 26) | (1ull << 30) | (1ull << 34) | (1ull << 35); constexpr uint64_t kPointPrim = static_cast(GS_PRIM_POINT); constexpr uint64_t kCopyPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | (1ull << 8) | (1ull << 9); constexpr uint64_t kSourceColor = 0x80665544ull; constexpr uint64_t kPointXyz = (0ull << 0) | (16ull << 16); constexpr uint64_t kUvRow1 = (0ull << 0) | (16ull << 16); gs.writeRegister(GS_REG_FRAME_1, kFrame1); gs.writeRegister(GS_REG_ZBUF_1, kZbuf1); gs.writeRegister(GS_REG_SCISSOR_1, kScissor1); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_PRIM, kPointPrim); gs.writeRegister(GS_REG_RGBAQ, kSourceColor); gs.writeRegister(GS_REG_XYZ2, kPointXyz); gs.writeRegister(GS_REG_FRAME_2, kFrame2); gs.writeRegister(GS_REG_ZBUF_2, kZbuf2); gs.writeRegister(GS_REG_SCISSOR_2, kScissor2); gs.writeRegister(GS_REG_XYOFFSET_2, 0ull); gs.writeRegister(GS_REG_TEST_2, 0x30000ull); gs.writeRegister(GS_REG_TEX0_2, kTex0_2); gs.writeRegister(GS_REG_PRIM, kCopyPrim); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, kUvRow1); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, kUvRow1); gs.writeRegister(GS_REG_XYZ2, 0ull); const uint32_t dstPixel = readReferenceFramePSMCT32Pixel(vram, 150u, 1u, 0u, 0u); t.Equals(dstPixel, static_cast(kSourceColor), "CT32 primitives should land in the same local-memory layout that later CT32 texture sampling expects"); }); tc.Run("FST sprite 1:1 CT32 copies preserve source texels at the right and bottom edges", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint64_t kFrame = (150ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kScissor = (0ull << 0) | (3ull << 16) | (0ull << 32) | (3ull << 48); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_CT32) << 20) | (2ull << 26) | (2ull << 30) | (1ull << 34); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | (1ull << 8); constexpr uint64_t kXyz0 = 0ull; constexpr uint64_t kXyz1 = (static_cast(4u << 4) << 0) | (static_cast(4u << 4) << 16); constexpr uint64_t kUv0 = 0ull; constexpr uint64_t kUv1 = ((4ull * 16ull) << 0) | ((4ull * 16ull) << 16); constexpr uint32_t kSourcePixels[4] = { 0x800000FFu, 0x8000FF00u, 0x80FF0000u, 0x80FFFFFFu, }; for (uint32_t y = 0u; y < 4u; ++y) { for (uint32_t x = 0u; x < 4u; ++x) { writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, x, y, kSourcePixels[x]); } } gs.writeRegister(GS_REG_FRAME_1, kFrame); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, kScissor); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_TEX1_1, 0ull); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, kUv0); gs.writeRegister(GS_REG_XYZ2, kXyz0); gs.writeRegister(GS_REG_UV, kUv1); gs.writeRegister(GS_REG_XYZ2, kXyz1); for (uint32_t y = 0u; y < 4u; ++y) { for (uint32_t x = 0u; x < 4u; ++x) { const uint32_t pixel = readReferenceFramePSMCT32Pixel(vram, 150u, 1u, x, y); t.Equals(pixel, kSourcePixels[x], "1:1 FST sprite copies should preserve each source texel without off-by-one edge skew"); } } }); tc.Run("fullscreen display copy tracks the preferred presentation source frame", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint64_t kFrame2 = 150ull | (10ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kScissor2 = (0ull << 0) | (639ull << 16) | (0ull << 32) | (479ull << 48); constexpr uint64_t kXYOffset2 = (static_cast(1728u << 4) << 0) | (static_cast(1808u << 4) << 32); constexpr uint64_t kAlpha2 = 0x6000000064ull; constexpr uint64_t kTest2 = 0x30000ull; constexpr uint64_t kTex0_2 = (0ull << 0) | (10ull << 14) | (static_cast(GS_PSM_CT32) << 20) | (10ull << 26) | (9ull << 30) | (1ull << 34); constexpr uint64_t kPrimCopy = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | (1ull << 6) | (1ull << 8) | (1ull << 9); constexpr uint64_t kXyz0 = (static_cast(1728u << 4) << 0) | (static_cast(1808u << 4) << 16) | (256ull << 32); constexpr uint64_t kXyz1 = (static_cast(2368u << 4) << 0) | (static_cast(2288u << 4) << 16) | (256ull << 32); constexpr uint64_t kUv0 = (8ull << 0) | (8ull << 16); constexpr uint64_t kUv1 = ((8ull + (640ull * 16ull)) << 0) | ((8ull + (480ull * 16ull)) << 16); gs.writeRegister(GS_REG_FRAME_2, kFrame2); gs.writeRegister(GS_REG_SCISSOR_2, kScissor2); gs.writeRegister(GS_REG_XYOFFSET_2, kXYOffset2); gs.writeRegister(GS_REG_ALPHA_2, kAlpha2); gs.writeRegister(GS_REG_TEST_2, kTest2); gs.writeRegister(GS_REG_TEX0_2, kTex0_2); gs.writeRegister(GS_REG_PRIM, kPrimCopy); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, kUv0); gs.writeRegister(GS_REG_XYZ2, kXyz0); gs.writeRegister(GS_REG_UV, kUv1); gs.writeRegister(GS_REG_XYZ2, kXyz1); GSFrameReg preferredSource{}; uint32_t preferredDestFbp = 0u; t.IsTrue(gs.getPreferredDisplaySource(preferredSource, preferredDestFbp), "fullscreen display-copy sprites should record their source frame for host presentation"); t.Equals(preferredDestFbp, 150u, "preferred presentation tracking should target the copied display page"); t.Equals(preferredSource.fbp, 0u, "preferred presentation tracking should expose the copy source frame base"); t.Equals(preferredSource.fbw, 10u, "preferred presentation tracking should expose the copy source width"); t.Equals(static_cast(preferredSource.psm), static_cast(GS_PSM_CT32), "preferred presentation tracking should expose the copy source format"); gs.writeRegister(GS_REG_PRIM, static_cast(GS_PRIM_POINT) | (1ull << 9)); gs.writeRegister(GS_REG_RGBAQ, 0xFFFFFFFFull); gs.writeRegister(GS_REG_XYZ2, kXyz0); t.IsFalse(gs.getPreferredDisplaySource(preferredSource, preferredDestFbp), "non-copy primitives that touch the display target should invalidate the preferred presentation source"); }); tc.Run("latched host presentation frame stays stable until the next latch", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GSRegisters regs{}; regs.pmode = 1ull; regs.dispfb1 = 150ull | (10ull << 9) | (static_cast(GS_PSM_CT32) << 15); regs.display1 = (639ull << 32) | (447ull << 44); GS gs; gs.init(vram.data(), static_cast(vram.size()), ®s); constexpr uint32_t kDisplayPixel = 0x00332211u; constexpr uint32_t kSourcePixel = 0x00665544u; constexpr uint32_t kUpdatedSourcePixel = 0x00998877u; writeReferenceFramePSMCT32Pixel(vram, 150u, 10u, 0u, 0u, kDisplayPixel); std::memcpy(vram.data() + 0u, &kSourcePixel, sizeof(kSourcePixel)); constexpr uint64_t kFrame2 = 150ull | (10ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf2 = (1ull << 32); constexpr uint64_t kScissor2 = (0ull << 0) | (639ull << 16) | (0ull << 32) | (479ull << 48); constexpr uint64_t kXYOffset2 = (static_cast(1728u << 4) << 0) | (static_cast(1808u << 4) << 32); constexpr uint64_t kAlpha2 = 0x6000000064ull; constexpr uint64_t kTest2 = 0x30000ull; constexpr uint64_t kTex0_2 = (0ull << 0) | (10ull << 14) | (static_cast(GS_PSM_CT32) << 20) | (10ull << 26) | (9ull << 30) | (1ull << 34); constexpr uint64_t kPrimCopy = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | (1ull << 6) | (1ull << 8) | (1ull << 9); constexpr uint64_t kXyz0 = (static_cast(1728u << 4) << 0) | (static_cast(1808u << 4) << 16) | (256ull << 32); constexpr uint64_t kXyz1 = (static_cast(2368u << 4) << 0) | (static_cast(2288u << 4) << 16) | (256ull << 32); constexpr uint64_t kUv0 = (8ull << 0) | (8ull << 16); constexpr uint64_t kUv1 = ((8ull + (640ull * 16ull)) << 0) | ((8ull + (480ull * 16ull)) << 16); gs.writeRegister(GS_REG_FRAME_2, kFrame2); gs.writeRegister(GS_REG_ZBUF_2, kZbuf2); gs.writeRegister(GS_REG_SCISSOR_2, kScissor2); gs.writeRegister(GS_REG_XYOFFSET_2, kXYOffset2); gs.writeRegister(GS_REG_ALPHA_2, kAlpha2); gs.writeRegister(GS_REG_TEST_2, kTest2); gs.writeRegister(GS_REG_TEX0_2, kTex0_2); gs.writeRegister(GS_REG_PRIM, kPrimCopy); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, kUv0); gs.writeRegister(GS_REG_XYZ2, kXyz0); gs.writeRegister(GS_REG_UV, kUv1); gs.writeRegister(GS_REG_XYZ2, kXyz1); gs.latchHostPresentationFrame(); std::vector latchedFrame; uint32_t latchedWidth = 0u; uint32_t latchedHeight = 0u; uint32_t displayFbp = 0u; uint32_t sourceFbp = 0u; bool usedPreferred = false; t.IsTrue(gs.copyLatchedHostPresentationFrame(latchedFrame, latchedWidth, latchedHeight, &displayFbp, &sourceFbp, &usedPreferred), "host presentation latch should produce a readable frame"); t.Equals(displayFbp, 150u, "latched host presentation should remember the selected display page"); t.Equals(sourceFbp, 0u, "latched host presentation should switch to the fullscreen copy source"); t.IsTrue(usedPreferred, "latched host presentation should record when it used the preferred copy source"); t.Equals(latchedWidth, 640u, "latched host presentation should preserve display width"); t.Equals(latchedHeight, 448u, "latched host presentation should preserve display height"); t.Equals(static_cast(latchedFrame[0]), 0x44u, "latched host presentation should expose the source frame RGB data"); t.Equals(static_cast(latchedFrame[1]), 0x55u, "latched host presentation should preserve the source frame green channel"); t.Equals(static_cast(latchedFrame[2]), 0x66u, "latched host presentation should preserve the source frame blue channel"); t.Equals(static_cast(latchedFrame[3]), 0xFFu, "latched host presentation should normalize framebuffer alpha for host upload"); std::memcpy(vram.data() + 0u, &kUpdatedSourcePixel, sizeof(kUpdatedSourcePixel)); std::vector staleFrame; uint32_t staleWidth = 0u; uint32_t staleHeight = 0u; t.IsTrue(gs.copyLatchedHostPresentationFrame(staleFrame, staleWidth, staleHeight), "latched host presentation should remain readable without relatching"); t.Equals(static_cast(staleFrame[0]), 0x44u, "latched host presentation should stay stable until the next latch"); gs.latchHostPresentationFrame(); std::vector refreshedFrame; uint32_t refreshedWidth = 0u; uint32_t refreshedHeight = 0u; t.IsTrue(gs.copyLatchedHostPresentationFrame(refreshedFrame, refreshedWidth, refreshedHeight), "latched host presentation should refresh after a new latch"); t.Equals(static_cast(refreshedFrame[0]), 0x77u, "relatching should pick up the updated source frame contents"); }); tc.Run("latched host presentation frame is returned tightly packed for narrower display widths", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GSRegisters regs{}; regs.pmode = 1ull; regs.dispfb1 = 150ull | (1ull << 9) | (static_cast(GS_PSM_CT32) << 15); regs.display1 = (63ull << 32) | (63ull << 44); GS gs; gs.init(vram.data(), static_cast(vram.size()), ®s); constexpr uint32_t kTopLeft = 0xFF332211u; constexpr uint32_t kTopRight = 0xFF665544u; constexpr uint32_t kBottomLeft = 0xFF998877u; constexpr uint32_t kBottomRight = 0xFFCCBBAAu; writeReferenceFramePSMCT32Pixel(vram, 150u, 1u, 0u, 0u, kTopLeft); writeReferenceFramePSMCT32Pixel(vram, 150u, 1u, 1u, 0u, kTopRight); writeReferenceFramePSMCT32Pixel(vram, 150u, 1u, 0u, 1u, kBottomLeft); writeReferenceFramePSMCT32Pixel(vram, 150u, 1u, 1u, 1u, kBottomRight); gs.latchHostPresentationFrame(); std::vector latchedFrame; uint32_t latchedWidth = 0u; uint32_t latchedHeight = 0u; t.IsTrue(gs.copyLatchedHostPresentationFrame(latchedFrame, latchedWidth, latchedHeight), "latched host presentation should be readable for narrow display widths"); t.Equals(latchedWidth, 64u, "latched host presentation should preserve the decoded display width"); t.Equals(latchedHeight, 64u, "latched host presentation should preserve the decoded display height"); t.Equals(static_cast(latchedFrame.size()), 64u * 64u * 4u, "latched host presentation should return a tightly packed RGBA buffer"); uint32_t pixel = 0u; std::memcpy(&pixel, latchedFrame.data() + 0u, sizeof(pixel)); t.Equals(pixel, kTopLeft, "latched host presentation should keep the first row intact"); std::memcpy(&pixel, latchedFrame.data() + 4u, sizeof(pixel)); t.Equals(pixel, kTopRight, "latched host presentation should pack the first row contiguously"); std::memcpy(&pixel, latchedFrame.data() + (64u * 4u), sizeof(pixel)); t.Equals(pixel, kBottomLeft, "latched host presentation should start the second row immediately after the first"); std::memcpy(&pixel, latchedFrame.data() + (64u * 4u) + 4u, sizeof(pixel)); t.Equals(pixel, kBottomRight, "latched host presentation should preserve subsequent rows without the internal 640-pixel stride"); }); tc.Run("latched host presentation reads preferred CT32 source with GS swizzle", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GSRegisters regs{}; regs.pmode = 1ull; regs.dispfb1 = 150ull | (10ull << 9) | (static_cast(GS_PSM_CT32) << 15); regs.display1 = (639ull << 32) | (447ull << 44); GS gs; gs.init(vram.data(), static_cast(vram.size()), ®s); constexpr uint32_t kSourceTbp0 = 64u; constexpr uint32_t kSourcePixelRow1 = 0x00665544u; constexpr uint32_t kDisplayPixelRow1 = 0x00CCBBAAu; const uint32_t swizzledSourceOff = GSPSMCT32::addrPSMCT32(kSourceTbp0, 10u, 0u, 1u); std::memcpy(vram.data() + swizzledSourceOff, &kSourcePixelRow1, sizeof(kSourcePixelRow1)); writeReferenceFramePSMCT32Pixel(vram, 150u, 10u, 0u, 1u, kDisplayPixelRow1); constexpr uint64_t kFrame2 = 150ull | (10ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf2 = (static_cast(1) << 32); constexpr uint64_t kScissor2 = (0ull << 0) | (639ull << 16) | (0ull << 32) | (479ull << 48); constexpr uint64_t kXYOffset2 = (static_cast(1728u << 4) << 0) | (static_cast(1808u << 4) << 32); constexpr uint64_t kAlpha2 = 0x6000000064ull; constexpr uint64_t kTest2 = 0x30000ull; constexpr uint64_t kTex0_2 = (static_cast(kSourceTbp0) << 0) | (10ull << 14) | (static_cast(GS_PSM_CT32) << 20) | (10ull << 26) | (9ull << 30) | (1ull << 34); constexpr uint64_t kPrimCopy = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | (1ull << 6) | (1ull << 8) | (1ull << 9); constexpr uint64_t kXyz0 = (static_cast(1728u << 4) << 0) | (static_cast(1808u << 4) << 16) | (256ull << 32); constexpr uint64_t kXyz1 = (static_cast(2368u << 4) << 0) | (static_cast(2288u << 4) << 16) | (256ull << 32); constexpr uint64_t kUv0 = (8ull << 0) | (8ull << 16); constexpr uint64_t kUv1 = ((8ull + (640ull * 16ull)) << 0) | ((8ull + (480ull * 16ull)) << 16); gs.writeRegister(GS_REG_FRAME_2, kFrame2); gs.writeRegister(GS_REG_ZBUF_2, kZbuf2); gs.writeRegister(GS_REG_SCISSOR_2, kScissor2); gs.writeRegister(GS_REG_XYOFFSET_2, kXYOffset2); gs.writeRegister(GS_REG_ALPHA_2, kAlpha2); gs.writeRegister(GS_REG_TEST_2, kTest2); gs.writeRegister(GS_REG_TEX0_2, kTex0_2); gs.writeRegister(GS_REG_PRIM, kPrimCopy); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, kUv0); gs.writeRegister(GS_REG_XYZ2, kXyz0); gs.writeRegister(GS_REG_UV, kUv1); gs.writeRegister(GS_REG_XYZ2, kXyz1); gs.latchHostPresentationFrame(); std::vector latchedFrame; uint32_t latchedWidth = 0u; uint32_t latchedHeight = 0u; uint32_t displayFbp = 0u; uint32_t sourceFbp = 0u; bool usedPreferred = false; t.IsTrue(gs.copyLatchedHostPresentationFrame(latchedFrame, latchedWidth, latchedHeight, &displayFbp, &sourceFbp, &usedPreferred), "preferred-source presentation should produce a host frame"); t.IsTrue(usedPreferred, "preferred-source presentation should use the fullscreen copy source"); t.Equals(displayFbp, 150u, "preferred-source presentation should still target the display page"); t.Equals(sourceFbp, kSourceTbp0, "preferred-source presentation should report the CT32 source frame"); const size_t row1Off = 640u * 4u; t.Equals(static_cast(latchedFrame[row1Off + 0u]), 0x44u, "preferred-source presentation should read row 1 red from the swizzled CT32 source"); t.Equals(static_cast(latchedFrame[row1Off + 1u]), 0x55u, "preferred-source presentation should read row 1 green from the swizzled CT32 source"); t.Equals(static_cast(latchedFrame[row1Off + 2u]), 0x66u, "preferred-source presentation should read row 1 blue from the swizzled CT32 source"); t.Equals(static_cast(latchedFrame[row1Off + 3u]), 0xFFu, "preferred-source presentation should normalize row 1 alpha for the host frame"); }); tc.Run("latched host presentation reads direct CT32 display frames with GS swizzle", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GSRegisters regs{}; regs.pmode = 0x0001ull; regs.dispfb1 = 150ull | (10ull << 9) | (static_cast(GS_PSM_CT32) << 15); regs.display1 = (639ull << 32) | (447ull << 44); GS gs; gs.init(vram.data(), static_cast(vram.size()), ®s); constexpr uint32_t kRow1Pixel = 0x44u | (0x55u << 8) | (0x66u << 16) | (0x77u << 24); constexpr uint32_t kLinearGarbageRow1 = 0xAAu | (0xBBu << 8) | (0xCCu << 16) | (0xDDu << 24); constexpr size_t kHostRow1Off = 640u * 4u; writeReferenceFramePSMCT32Pixel(vram, 150u, 10u, 0u, 1u, kRow1Pixel); std::memcpy(vram.data() + (150u * 8192u) + kHostRow1Off, &kLinearGarbageRow1, sizeof(kLinearGarbageRow1)); gs.latchHostPresentationFrame(); std::vector latchedFrame; uint32_t latchedWidth = 0u; uint32_t latchedHeight = 0u; uint32_t displayFbp = 0u; bool usedPreferred = false; t.IsTrue(gs.copyLatchedHostPresentationFrame(latchedFrame, latchedWidth, latchedHeight, &displayFbp, nullptr, &usedPreferred), "direct CT32 display presentation should produce a host frame"); t.Equals(displayFbp, 150u, "direct CT32 presentation should report the active display page"); t.IsFalse(usedPreferred, "direct CT32 presentation should not claim it used a preferred copy source"); t.Equals(static_cast(latchedFrame[kHostRow1Off + 0u]), 0x44u, "direct CT32 presentation should read row 1 red from the GS-swizzled display page"); t.Equals(static_cast(latchedFrame[kHostRow1Off + 1u]), 0x55u, "direct CT32 presentation should read row 1 green from the GS-swizzled display page"); t.Equals(static_cast(latchedFrame[kHostRow1Off + 2u]), 0x66u, "direct CT32 presentation should read row 1 blue from the GS-swizzled display page"); t.Equals(static_cast(latchedFrame[kHostRow1Off + 3u]), 0xFFu, "direct CT32 presentation should normalize row 1 alpha for the host frame"); }); tc.Run("latched host presentation merges both enabled PMODE circuits", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GSRegisters regs{}; regs.pmode = 0x8007ull; regs.dispfb1 = 150ull | (10ull << 9) | (static_cast(GS_PSM_CT32) << 15); regs.display1 = (639ull << 32) | (447ull << 44); regs.dispfb2 = 0ull | (10ull << 9) | (static_cast(GS_PSM_CT32) << 15); regs.display2 = regs.display1; GS gs; gs.init(vram.data(), static_cast(vram.size()), ®s); constexpr uint32_t kCircuit1Pixel = 200u | (0u << 8) | (0u << 16) | (64u << 24); constexpr uint32_t kCircuit2Pixel = 0u | (0u << 8) | (200u << 16) | (255u << 24); writeReferenceFramePSMCT32Pixel(vram, 150u, 10u, 0u, 0u, kCircuit1Pixel); std::memcpy(vram.data(), &kCircuit2Pixel, sizeof(kCircuit2Pixel)); gs.latchHostPresentationFrame(); std::vector latchedFrame; uint32_t latchedWidth = 0u; uint32_t latchedHeight = 0u; uint32_t displayFbp = 0u; bool usedPreferred = false; t.IsTrue(gs.copyLatchedHostPresentationFrame(latchedFrame, latchedWidth, latchedHeight, &displayFbp, nullptr, &usedPreferred), "dual-circuit PMODE presentation should produce a host frame"); t.Equals(displayFbp, 150u, "dual-circuit presentation should still report the primary display page"); t.IsFalse(usedPreferred, "dual-circuit PMODE presentation should not bypass the first circuit with the preferred-copy shortcut"); t.Equals(latchedWidth, 640u, "dual-circuit presentation should preserve the display width"); t.Equals(latchedHeight, 448u, "dual-circuit presentation should preserve the display height"); t.Equals(static_cast(latchedFrame[0]), 100u, "dual-circuit presentation should blend the first circuit red channel over the second circuit"); t.Equals(static_cast(latchedFrame[1]), 0u, "dual-circuit presentation should preserve a zero green channel"); t.Equals(static_cast(latchedFrame[2]), 100u, "dual-circuit presentation should blend the second circuit blue channel under the first circuit"); t.Equals(static_cast(latchedFrame[3]), 0xFFu, "dual-circuit presentation should normalize the final host alpha"); }); tc.Run("latched host presentation normalizes alpha for single-circuit display", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GSRegisters regs{}; regs.pmode = 0x0001ull; regs.dispfb1 = 150ull | (10ull << 9) | (static_cast(GS_PSM_CT32) << 15); regs.display1 = (639ull << 32) | (447ull << 44); GS gs; gs.init(vram.data(), static_cast(vram.size()), ®s); constexpr uint32_t kPixel = 0x22u | (0x44u << 8) | (0x66u << 16) | (0x01u << 24); writeReferenceFramePSMCT32Pixel(vram, 150u, 10u, 0u, 0u, kPixel); gs.latchHostPresentationFrame(); std::vector latchedFrame; uint32_t latchedWidth = 0u; uint32_t latchedHeight = 0u; t.IsTrue(gs.copyLatchedHostPresentationFrame(latchedFrame, latchedWidth, latchedHeight), "single-circuit presentation should produce a host frame"); t.Equals(static_cast(latchedFrame[0]), 0x22u, "single-circuit presentation should preserve the red channel"); t.Equals(static_cast(latchedFrame[1]), 0x44u, "single-circuit presentation should preserve the green channel"); t.Equals(static_cast(latchedFrame[2]), 0x66u, "single-circuit presentation should preserve the blue channel"); t.Equals(static_cast(latchedFrame[3]), 0xFFu, "single-circuit presentation should upload an opaque host alpha"); }); tc.Run("latched host presentation preserves 480-line display height", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GSRegisters regs{}; regs.pmode = 0x0001ull; regs.dispfb1 = 150ull | (10ull << 9) | (static_cast(GS_PSM_CT32) << 15); regs.display1 = (639ull << 32) | (479ull << 44); GS gs; gs.init(vram.data(), static_cast(vram.size()), ®s); constexpr uint32_t kLastRowPixel = 0x12u | (0x34u << 8) | (0x56u << 16) | (0x78u << 24); writeReferenceFramePSMCT32Pixel(vram, 150u, 10u, 0u, 479u, kLastRowPixel); gs.latchHostPresentationFrame(); std::vector latchedFrame; uint32_t latchedWidth = 0u; uint32_t latchedHeight = 0u; t.IsTrue(gs.copyLatchedHostPresentationFrame(latchedFrame, latchedWidth, latchedHeight), "480-line single-circuit presentation should produce a host frame"); t.Equals(latchedWidth, 640u, "480-line presentation should preserve the display width"); t.Equals(latchedHeight, 480u, "480-line presentation should preserve the full display height"); const size_t lastRowOffset = static_cast(479u) * 640u * 4u; t.Equals(static_cast(latchedFrame[lastRowOffset + 0u]), 0x12u, "480-line presentation should keep the last row red channel"); t.Equals(static_cast(latchedFrame[lastRowOffset + 1u]), 0x34u, "480-line presentation should keep the last row green channel"); t.Equals(static_cast(latchedFrame[lastRowOffset + 2u]), 0x56u, "480-line presentation should keep the last row blue channel"); t.Equals(static_cast(latchedFrame[lastRowOffset + 3u]), 0xFFu, "single-circuit presentation should normalize the last row alpha"); }); tc.Run("latched host presentation line-doubles interlaced field output", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GSRegisters regs{}; regs.pmode = 0x0001ull; regs.smode2 = 0x0001ull; // interlaced, field mode regs.dispfb1 = 0ull | (10ull << 9) | (static_cast(GS_PSM_CT32) << 15); regs.display1 = (639ull << 32) | (447ull << 44); GS gs; gs.init(vram.data(), static_cast(vram.size()), ®s); constexpr uint32_t kLine0 = 0x000000FFu; constexpr uint32_t kLine1 = 0x0000FF00u; constexpr uint32_t kLine2 = 0x00FF0000u; constexpr uint32_t kLine3 = 0x00FFFF00u; writeReferencePSMCT32Pixel(vram, 0u, 10u, 0u, 0u, kLine0); writeReferencePSMCT32Pixel(vram, 0u, 10u, 0u, 1u, kLine1); writeReferencePSMCT32Pixel(vram, 0u, 10u, 0u, 2u, kLine2); writeReferencePSMCT32Pixel(vram, 0u, 10u, 0u, 3u, kLine3); gs.latchHostPresentationFrame(); std::vector latchedFrame; uint32_t latchedWidth = 0u; uint32_t latchedHeight = 0u; t.IsTrue(gs.copyLatchedHostPresentationFrame(latchedFrame, latchedWidth, latchedHeight), "interlaced field presentation should produce a host frame"); t.Equals(latchedWidth, 640u, "field presentation should preserve display width"); t.Equals(latchedHeight, 448u, "field presentation should preserve display height"); auto pixelAtRow = [&](uint32_t row) -> uint32_t { const size_t off = static_cast(row) * 640u * 4u; return static_cast(latchedFrame[off + 0u]) | (static_cast(latchedFrame[off + 1u]) << 8) | (static_cast(latchedFrame[off + 2u]) << 16); }; const uint32_t row0 = pixelAtRow(0u); const uint32_t row1 = pixelAtRow(1u); const uint32_t row2 = pixelAtRow(2u); const uint32_t row3 = pixelAtRow(3u); t.Equals(row0, row1, "field presentation should duplicate the active field into the next scanline"); t.Equals(row2, row3, "field presentation should duplicate later field scanlines as well"); t.IsTrue(row0 != row2, "field presentation should still preserve different source content across field rows"); }); tc.Run("GIF PACKED A+D writes DISPFB1 and DISPLAY1 privileged registers", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GSRegisters regs{}; GS gs; gs.init(vram.data(), static_cast(vram.size()), ®s); std::vector packet; appendU64(packet, makeGifTag(2u, GIF_FMT_PACKED, 1u, true)); appendU64(packet, 0x0Eull); // REGS[0] = A+D const uint64_t dispfb1 = 0x0123456789ABCDEFull; const uint64_t display1 = 0x1111222233334444ull; appendU64(packet, dispfb1); appendU64(packet, 0x59ull); // DISPFB1 appendU64(packet, display1); appendU64(packet, 0x5Aull); // DISPLAY1 gs.processGIFPacket(packet.data(), static_cast(packet.size())); t.Equals(regs.dispfb1, dispfb1, "A+D should write GS DISPFB1"); t.Equals(regs.display1, display1, "A+D should write GS DISPLAY1"); }); tc.Run("GIF PACKED A+D writes DISPFB2 and DISPLAY2 privileged registers", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GSRegisters regs{}; GS gs; gs.init(vram.data(), static_cast(vram.size()), ®s); std::vector packet; appendU64(packet, makeGifTag(2u, GIF_FMT_PACKED, 1u, true)); appendU64(packet, 0x0Eull); // REGS[0] = A+D const uint64_t dispfb2 = 0x2222333344445555ull; const uint64_t display2 = 0x6666777788889999ull; appendU64(packet, dispfb2); appendU64(packet, 0x5Bull); // DISPFB2 appendU64(packet, display2); appendU64(packet, 0x5Cull); // DISPLAY2 gs.processGIFPacket(packet.data(), static_cast(packet.size())); t.Equals(regs.dispfb2, dispfb2, "A+D should write GS DISPFB2"); t.Equals(regs.display2, display2, "A+D should write GS DISPLAY2"); }); tc.Run("reserved PSM 0x3F uses null VRAM handlers", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0xA5u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); t.Equals(gs.ReadVram(0x3Fu, 0u, 1u, 0u, 0u), 0u, "reserved PSM reads should use the null handler"); gs.WriteVram(0x3Fu, 0u, 1u, 0u, 0u, 0x0005180Bu); t.Equals(static_cast(vram[0]), 0xA5u, "reserved PSM writes should leave VRAM unchanged"); }); tc.Run("PSMT4 address mapping matches GS manual layout", [](TestCase &t) { constexpr uint32_t kBaseBlock = 0u; constexpr uint32_t kWidth = 2u; // One 128x128 PSMT4 page. t.Equals(GSPSMT4::addrPSMT4(kBaseBlock, kWidth, 0u, 0u), 0u, "PSMT4 origin should map to nibble offset 0"); t.Equals(GSPSMT4::addrPSMT4(kBaseBlock, kWidth, 1u, 0u), 8u, "PSMT4 x=1 should advance to the next packed nibble group"); t.Equals(GSPSMT4::addrPSMT4(kBaseBlock, kWidth, 0u, 1u), 16u, "PSMT4 second source row should follow the manual's row packing"); t.Equals(GSPSMT4::addrPSMT4(kBaseBlock, kWidth, 0u, 2u), 65u, "PSMT4 third source row should include the manual's odd-row permutation"); t.Equals(GSPSMT4::addrPSMT4(kBaseBlock, kWidth, 0u, 3u), 81u, "PSMT4 fourth source row should stay in the first block's manual column layout"); t.Equals(GSPSMT4::addrPSMT4(kBaseBlock, kWidth, 31u, 15u), 511u, "PSMT4 final texel in the first 32x16 block should land at the end of the block"); t.Equals(GSPSMT4::addrPSMT4(kBaseBlock, kWidth, 32u, 0u), 1024u, "PSMT4 x=32 should advance to the next swizzled block in the page"); t.Equals(GSPSMT4::addrPSMT4(kBaseBlock, kWidth, 32u, 16u), 1536u, "PSMT4 x=32,y=16 should follow the manual's second block-row permutation"); t.Equals(GSPSMT4::addrPSMT4(kBaseBlock, kWidth, 64u, 0u), 4096u, "PSMT4 x=64 should advance to the third swizzled block column in the page"); t.Equals(GSPSMT4::addrPSMT4(kBaseBlock, kWidth, 96u, 112u), 15872u, "PSMT4 bottom-right block origin should match the manual's page permutation"); t.Equals(GSPSMT4::addrPSMT4(kBaseBlock, kWidth, 127u, 127u), 16383u, "PSMT4 final texel in a 128x128 page should land at the end of the page"); t.Equals(GSPSMT4::addrPSMT4(kBaseBlock, kWidth, 128u, 0u), 16384u, "PSMT4 x=128 should advance to the next page of nibble addresses"); }); tc.Run("PSMT4 large atlases keep manual page layout across 512x512 textures", [](TestCase &t) { constexpr uint32_t kBaseBlock = 64u; constexpr uint32_t kWidth = 8u; // 512 pixel-wide T4 atlas, like Veronica font pages. constexpr uint32_t kCoords[][2] = { {0u, 0u}, {31u, 15u}, {32u, 0u}, {95u, 31u}, {127u, 127u}, {128u, 0u}, {255u, 127u}, {256u, 0u}, {383u, 127u}, {384u, 128u}, {511u, 511u}, }; for (const auto &coord : kCoords) { const uint32_t x = coord[0]; const uint32_t y = coord[1]; t.Equals(GSPSMT4::addrPSMT4(kBaseBlock, kWidth, x, y), referenceAddrPSMT4(kBaseBlock, kWidth, x, y), "PSMT4 512x512 atlas mapping should match the GS manual for every sampled page boundary"); } }); tc.Run("GS T4 triangle sampling reads manual-layout texels from a 512x512 atlas", [](TestCase &t) { constexpr uint32_t kTexTbp = 64u; constexpr uint32_t kClutCbp = 128u; constexpr uint64_t kFrame = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kScissor = (0ull << 0) | (4ull << 16) | (0ull << 32) | (4ull << 48); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (8ull << 14) | (static_cast(GS_PSM_T4) << 20) | (9ull << 26) | (9ull << 30) | (1ull << 34) | (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | (1ull << 55); constexpr uint64_t kPrim = static_cast(GS_PRIM_TRIANGLE) | (1ull << 4); constexpr uint64_t kRgbaq = 0x3F80000080808080ull; auto packFloat = [](float value) -> uint32_t { uint32_t bits = 0u; std::memcpy(&bits, &value, sizeof(bits)); return bits; }; auto packSt = [&](float s, float tVal) -> uint64_t { return static_cast(packFloat(s)) | (static_cast(packFloat(tVal)) << 32); }; const struct SampleCase { uint32_t x; uint32_t y; uint8_t index; uint32_t color; } cases[] = { {5u, 5u, 1u, 0xFF0000FFu}, {129u, 5u, 2u, 0xFF00FF00u}, {257u, 5u, 3u, 0xFFFF0000u}, {385u, 129u, 4u, 0xFFFFFFFFu}, }; for (const auto &sample : cases) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); writeReferencePSMT4Texel(vram, kTexTbp, 8u, sample.x, sample.y, sample.index); const uint32_t clutOff = referenceAddrPSMCT32(kClutCbp, 1u, sample.index, 0u); std::memcpy(vram.data() + clutOff, &sample.color, sizeof(sample.color)); gs.writeRegister(GS_REG_FRAME_1, kFrame); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, kScissor); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_TEX1_1, 0ull); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, kRgbaq); const float s = (static_cast(sample.x) + 0.25f) / 512.0f; const float tVal = (static_cast(sample.y) + 0.25f) / 512.0f; gs.writeRegister(GS_REG_ST, packSt(s, tVal)); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_ST, packSt(s, tVal)); gs.writeRegister(GS_REG_XYZ2, (64ull << 0) | (0ull << 16)); gs.writeRegister(GS_REG_ST, packSt(s, tVal)); gs.writeRegister(GS_REG_XYZ2, (0ull << 0) | (64ull << 16)); const uint32_t pixel = readReferencePSMCT32Pixel(vram, 0u, 1u, 1u, 1u); t.Equals(pixel, sample.color, "T4 triangle sampling should fetch the manual-layout atlas texel from the correct 128x128 page"); } }); tc.Run("PSMT8 address mapping matches Veronica Conv8to32 layout", [](TestCase &t) { constexpr uint32_t kBaseBlock = 0u; constexpr uint32_t kWidth = 2u; // One 128x64 PSMT8 page. t.Equals(GSPSMT8::addrPSMT8(kBaseBlock, kWidth, 0u, 0u), 0u, "PSMT8 origin should map to byte offset 0"); t.Equals(GSPSMT8::addrPSMT8(kBaseBlock, kWidth, 1u, 0u), 4u, "PSMT8 x=1 should follow Veronica's Conv8to32 byte interleave"); t.Equals(GSPSMT8::addrPSMT8(kBaseBlock, kWidth, 0u, 1u), 8u, "PSMT8 second source row should land on the next Conv8to32 row stride"); t.Equals(GSPSMT8::addrPSMT8(kBaseBlock, kWidth, 0u, 2u), 33u, "PSMT8 third source row should preserve Veronica's odd-row shuffle"); t.Equals(GSPSMT8::addrPSMT8(kBaseBlock, kWidth, 0u, 3u), 41u, "PSMT8 fourth source row should preserve Veronica's alternating block rows"); t.Equals(GSPSMT8::addrPSMT8(kBaseBlock, kWidth, 15u, 15u), 255u, "PSMT8 final texel in the first 16x16 block should end at byte 255"); t.Equals(GSPSMT8::addrPSMT8(kBaseBlock, kWidth, 16u, 0u), 256u, "PSMT8 x=16 should advance to the next 16x16 block"); t.Equals(GSPSMT8::addrPSMT8(kBaseBlock, kWidth, 16u, 16u), 768u, "PSMT8 x=16,y=16 should include both block-column and block-row offsets"); t.Equals(GSPSMT8::addrPSMT8(kBaseBlock, kWidth, 32u, 0u), 1024u, "PSMT8 x=32 should advance to the third block column in the page"); t.Equals(GSPSMT8::addrPSMT8(kBaseBlock, kWidth, 64u, 0u), 4096u, "PSMT8 x=64 should advance to the second page half"); t.Equals(GSPSMT8::addrPSMT8(kBaseBlock, kWidth, 96u, 48u), 7680u, "PSMT8 lower-right interior block should follow Veronica's page permutation"); t.Equals(GSPSMT8::addrPSMT8(kBaseBlock, kWidth, 127u, 63u), 8191u, "PSMT8 final texel in a 128x64 page should land at the final byte"); }); tc.Run("GIF REGLIST with odd register count consumes 128-bit padding before next tag", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); const uint64_t bitblt = (static_cast(0u) << 0) | (static_cast(1u) << 16) | (static_cast(0u) << 24) | (static_cast(0u) << 32) | (static_cast(1u) << 48) | (static_cast(0u) << 56); gs.writeRegister(GS_REG_BITBLTBUF, bitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, (4ull << 0) | (1ull << 32)); gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packet; appendU64(packet, makeGifTag(1u, GIF_FMT_REGLIST, 1u, false)); appendU64(packet, 0x0ull); // REGS[0] = PRIM appendU64(packet, 0x0000000000000006ull); // PRIM write appendU64(packet, 0xDEADBEEFCAFEBABEull); // required REGLIST pad qword appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true)); appendU64(packet, 0ull); const uint8_t payload[16] = { 0x31u, 0x32u, 0x33u, 0x34u, 0x35u, 0x36u, 0x37u, 0x38u, 0x39u, 0x3Au, 0x3Bu, 0x3Cu, 0x3Du, 0x3Eu, 0x3Fu, 0x40u, }; packet.insert(packet.end(), payload, payload + sizeof(payload)); gs.processGIFPacket(packet.data(), static_cast(packet.size())); bool imageOk = true; for (uint32_t x = 0; x < 4u && imageOk; ++x) { const uint32_t off = referenceAddrPSMCT32(0u, 1u, x, 0u); for (uint32_t c = 0; c < 4u; ++c) { if (vram[off + c] != payload[x * 4u + c]) { imageOk = false; break; } } } t.IsTrue(imageOk, "odd REGLIST payload should not corrupt alignment of the following IMAGE tag"); }); tc.Run("GIF REGLIST NREG=0 is treated as sixteen descriptors", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); const uint64_t bitblt = (static_cast(0u) << 0) | (static_cast(1u) << 16) | (static_cast(0u) << 24) | (static_cast(0u) << 32) | (static_cast(1u) << 48) | (static_cast(0u) << 56); gs.writeRegister(GS_REG_BITBLTBUF, bitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, (4ull << 0) | (1ull << 32)); gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packet; appendU64(packet, makeGifTag(1u, GIF_FMT_REGLIST, 0u, false)); // NREG=0 -> 16 regs appendU64(packet, 0ull); // 16x PRIM descriptors for (uint32_t i = 0; i < 16u; ++i) { appendU64(packet, static_cast(i)); } appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true)); appendU64(packet, 0ull); const uint8_t payload[16] = { 0x51u, 0x52u, 0x53u, 0x54u, 0x55u, 0x56u, 0x57u, 0x58u, 0x59u, 0x5Au, 0x5Bu, 0x5Cu, 0x5Du, 0x5Eu, 0x5Fu, 0x60u, }; packet.insert(packet.end(), payload, payload + sizeof(payload)); gs.processGIFPacket(packet.data(), static_cast(packet.size())); bool imageOk = true; for (uint32_t x = 0; x < 4u && imageOk; ++x) { const uint32_t off = referenceAddrPSMCT32(0u, 1u, x, 0u); for (uint32_t c = 0; c < 4u; ++c) { if (vram[off + c] != payload[x * 4u + c]) { imageOk = false; break; } } } t.IsTrue(imageOk, "NREG=0 REGLIST should consume 16 data words and keep following tag aligned"); }); tc.Run("GS SIGNAL and FINISH set CSR bits that clear by CSR write-one acknowledge", [](TestCase &t) { PS2Memory mem; t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); GS gs; gs.init(mem.getGSVRAM(), static_cast(PS2_GS_VRAM_SIZE), &mem.gs()); const uint64_t signalValue = (0xFFFFFFFFull << 32) | 0x11223344ull; gs.writeRegister(GS_REG_SIGNAL, signalValue); gs.writeRegister(GS_REG_FINISH, 0u); t.IsTrue((mem.gs().csr & 0x1ull) != 0ull, "SIGNAL should raise CSR.SIGNAL"); t.IsTrue((mem.gs().csr & 0x2ull) != 0ull, "FINISH should raise CSR.FINISH"); t.Equals(static_cast(mem.gs().siglblid & 0xFFFFFFFFull), 0x11223344u, "SIGNAL should update SIGLBLID low dword"); mem.write64(0x12001000u, 0x1ull); t.IsTrue((mem.gs().csr & 0x1ull) == 0ull, "writing CSR bit0 should acknowledge SIGNAL"); t.IsTrue((mem.gs().csr & 0x2ull) != 0ull, "acknowledging SIGNAL should not clear FINISH"); mem.write32(0x12001000u, 0x2u); t.IsTrue((mem.gs().csr & 0x2ull) == 0ull, "writing CSR bit1 should acknowledge FINISH"); }); tc.Run("GIF IMAGE packet writes host-to-local data into GS VRAM", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); // Setup for host->local transfer to DBP=0, DBW=1, PSMCT32, rect 2x2. const uint64_t bitblt = (static_cast(0u) << 0) | // SBP (static_cast(1u) << 16) | // SBW (static_cast(0u) << 24) | // SPSM (static_cast(0u) << 32) | // DBP (static_cast(1u) << 48) | // DBW (static_cast(0u) << 56); // DPSM (CT32) gs.writeRegister(GS_REG_BITBLTBUF, bitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, (2ull << 0) | (2ull << 32)); gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packet; appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true)); appendU64(packet, 0ull); const uint8_t payload[16] = { 0x10u, 0x11u, 0x12u, 0x13u, 0x20u, 0x21u, 0x22u, 0x23u, 0x30u, 0x31u, 0x32u, 0x33u, 0x40u, 0x41u, 0x42u, 0x43u, }; packet.insert(packet.end(), payload, payload + sizeof(payload)); gs.processGIFPacket(packet.data(), static_cast(packet.size())); bool same = true; for (uint32_t y = 0; y < 2u && same; ++y) { for (uint32_t x = 0; x < 2u; ++x) { const uint32_t pixelIndex = y * 2u + x; const uint32_t off = referenceAddrPSMCT32(0u, 1u, x, y); for (uint32_t c = 0; c < 4u; ++c) { if (vram[off + c] != payload[pixelIndex * 4u + c]) { same = false; break; } } if (!same) break; } } t.IsTrue(same, "GIF IMAGE transfer should write payload bytes into GS VRAM"); }); tc.Run("GIF load-image packet uses native upload fast path", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); const uint64_t bitblt = (static_cast(0u) << 0) | (static_cast(1u) << 16) | (static_cast(0u) << 24) | (static_cast(0u) << 32) | (static_cast(1u) << 48) | (static_cast(0u) << 56); const uint64_t trxpos = 0ull; const uint64_t trxreg = (2ull << 0) | (2ull << 32); const uint64_t trxdir = 0ull; const uint8_t payload[16] = { 0x10u, 0x11u, 0x12u, 0x13u, 0x20u, 0x21u, 0x22u, 0x23u, 0x30u, 0x31u, 0x32u, 0x33u, 0x40u, 0x41u, 0x42u, 0x43u, }; std::vector packet; appendU64(packet, makeGifTag(4u, GIF_FMT_PACKED, 1u, false)); appendU64(packet, 0x0Eull); appendGifAd(packet, bitblt, GS_REG_BITBLTBUF); appendGifAd(packet, trxpos, GS_REG_TRXPOS); appendGifAd(packet, trxreg, GS_REG_TRXREG); appendGifAd(packet, trxdir, GS_REG_TRXDIR); appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true)); appendU64(packet, 0ull); packet.insert(packet.end(), payload, payload + sizeof(payload)); gs.processGIFPacket(packet.data(), static_cast(packet.size())); t.Equals(gs.nativeImageUploadCount(), 1ull, "load-image packet should use the native image upload fast path"); bool same = true; for (uint32_t y = 0; y < 2u && same; ++y) { for (uint32_t x = 0; x < 2u; ++x) { const uint32_t pixelIndex = y * 2u + x; const uint32_t off = referenceAddrPSMCT32(0u, 1u, x, y); for (uint32_t c = 0; c < 4u; ++c) { if (vram[off + c] != payload[pixelIndex * 4u + c]) { same = false; break; } } if (!same) break; } } t.IsTrue(same, "native load-image upload should preserve pixel payload"); }); tc.Run("GS local-to-host transfer supports partial incremental reads", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); for (uint32_t x = 0; x < 4u; ++x) { const uint32_t off = referenceAddrPSMCT32(0u, 1u, x, 0u); for (uint32_t c = 0; c < 4u; ++c) { vram[off + c] = static_cast(0xA0u + x * 4u + c); } } const uint64_t bitblt = (static_cast(0u) << 0) | // SBP (static_cast(1u) << 16) | // SBW (static_cast(0u) << 24) | // SPSM (CT32) (static_cast(0u) << 32) | (static_cast(1u) << 48) | (static_cast(0u) << 56); gs.writeRegister(GS_REG_BITBLTBUF, bitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, (4ull << 0) | (1ull << 32)); // 4 pixels, 1 row -> 16 bytes gs.writeRegister(GS_REG_TRXDIR, 1ull); uint8_t bufA[8] = {}; uint8_t bufB[16] = {}; const uint32_t nA = gs.consumeLocalToHostBytes(bufA, 6u); const uint32_t nB = gs.consumeLocalToHostBytes(bufB, 16u); const uint32_t nC = gs.consumeLocalToHostBytes(bufB, 4u); t.Equals(nA, 6u, "first partial read should consume requested bytes"); t.Equals(nB, 10u, "second read should consume the remaining bytes"); t.Equals(nC, 0u, "buffer should be empty after all bytes are consumed"); bool bytesOk = true; for (uint32_t i = 0; i < 6u; ++i) { if (bufA[i] != static_cast(0xA0u + i)) bytesOk = false; } for (uint32_t i = 0; i < 10u; ++i) { if (bufB[i] != static_cast(0xA6u + i)) bytesOk = false; } t.IsTrue(bytesOk, "partial reads should return local->host data in-order"); }); tc.Run("GS CT24 host-local-host transfer preserves 24-bit RGB payload", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); const uint64_t bitblt = (static_cast(0u) << 0) | // SBP (static_cast(1u) << 16) | // SBW (static_cast(1u) << 24) | // SPSM CT24 (static_cast(0u) << 32) | // DBP (static_cast(1u) << 48) | // DBW (static_cast(1u) << 56); // DPSM CT24 gs.writeRegister(GS_REG_BITBLTBUF, bitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, (2ull << 0) | (1ull << 32)); // 2 pixels gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packet; appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true)); appendU64(packet, 0ull); const uint8_t rgbData[16] = { 0x11u, 0x22u, 0x33u, 0x44u, 0x55u, 0x66u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u }; packet.insert(packet.end(), rgbData, rgbData + sizeof(rgbData)); gs.processGIFPacket(packet.data(), static_cast(packet.size())); // Read back from local to host in CT24. gs.writeRegister(GS_REG_TRXDIR, 1ull); uint8_t out[16] = {}; const uint32_t outBytes = gs.consumeLocalToHostBytes(out, sizeof(out)); t.Equals(outBytes, 6u, "CT24 local->host read should output 3 bytes per pixel"); t.Equals(out[0], static_cast(0x11u), "pixel0 R should roundtrip"); t.Equals(out[1], static_cast(0x22u), "pixel0 G should roundtrip"); t.Equals(out[2], static_cast(0x33u), "pixel0 B should roundtrip"); t.Equals(out[3], static_cast(0x44u), "pixel1 R should roundtrip"); t.Equals(out[4], static_cast(0x55u), "pixel1 G should roundtrip"); t.Equals(out[5], static_cast(0x66u), "pixel1 B should roundtrip"); }); tc.Run("GS PSMT4 host-local-host keeps nibble packing stable", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); const uint64_t bitblt = (static_cast(0u) << 0) | // SBP (static_cast(1u) << 16) | // SBW (static_cast(20u) << 24) | // SPSM PSMT4 (static_cast(0u) << 32) | // DBP (static_cast(1u) << 48) | // DBW (static_cast(20u) << 56); // DPSM PSMT4 gs.writeRegister(GS_REG_BITBLTBUF, bitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, (4ull << 0) | (1ull << 32)); // 4 texels => 2 bytes gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packet; appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true)); appendU64(packet, 0ull); const uint8_t nibbleData[16] = {0x21u, 0x43u}; packet.insert(packet.end(), nibbleData, nibbleData + sizeof(nibbleData)); gs.processGIFPacket(packet.data(), static_cast(packet.size())); gs.writeRegister(GS_REG_TRXDIR, 1ull); uint8_t out[8] = {}; const uint32_t outBytes = gs.consumeLocalToHostBytes(out, sizeof(out)); t.Equals(outBytes, 2u, "PSMT4 local->host should return packed nibble bytes"); t.Equals(out[0], static_cast(0x21u), "packed nibble byte 0 should roundtrip"); t.Equals(out[1], static_cast(0x43u), "packed nibble byte 1 should roundtrip"); }); tc.Run("GS PSMT4 host-local upload keeps position across split IMAGE packets", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); const uint64_t bitblt = (static_cast(0u) << 0) | (static_cast(1u) << 16) | (static_cast(GS_PSM_T4) << 24) | (static_cast(0u) << 32) | (static_cast(1u) << 48) | (static_cast(GS_PSM_T4) << 56); gs.writeRegister(GS_REG_BITBLTBUF, bitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, (8ull << 0) | (8ull << 32)); // 64 texels => 32 bytes gs.writeRegister(GS_REG_TRXDIR, 0ull); uint8_t packedSource[32] = {}; for (uint32_t i = 0; i < 32u; ++i) { packedSource[i] = static_cast(0x10u + i); } std::vector packetA; appendU64(packetA, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true)); appendU64(packetA, 0ull); packetA.insert(packetA.end(), packedSource, packedSource + 16u); std::vector packetB; appendU64(packetB, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true)); appendU64(packetB, 0ull); packetB.insert(packetB.end(), packedSource + 16u, packedSource + 32u); gs.processGIFPacket(packetA.data(), static_cast(packetA.size())); gs.processGIFPacket(packetB.data(), static_cast(packetB.size())); gs.writeRegister(GS_REG_TRXDIR, 1ull); uint8_t out[32] = {}; const uint32_t outBytes = gs.consumeLocalToHostBytes(out, sizeof(out)); t.Equals(outBytes, 32u, "split T4 IMAGE upload should fill the full packed byte range"); for (uint32_t i = 0; i < 32u; ++i) { t.Equals(out[i], packedSource[i], "split T4 IMAGE upload should preserve packed nibble order"); } }); tc.Run("GS CT32 upload aliases cleanly into later PSMT8 sampling", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexWidth = 128u; constexpr uint32_t kTexHeight = 64u; constexpr uint32_t kUploadWidth = 64u; constexpr uint32_t kUploadHeight = 32u; constexpr uint32_t kTexTbp = 0u; constexpr uint32_t kTexTbw = 2u; std::vector source(kTexWidth * kTexHeight, 0u); for (uint32_t i = 0; i < source.size(); ++i) { source[i] = static_cast((i * 37u + 11u) & 0xFFu); } std::vector rawToUpload(8192u, 0xFFFFu); for (uint32_t y = 0; y < kUploadHeight; ++y) { for (uint32_t x = 0; x < kUploadWidth; ++x) { const uint32_t rawBase = referenceAddrPSMCT32(kTexTbp, 1u, x, y); const uint32_t uploadBase = ((y * kUploadWidth) + x) * 4u; for (uint32_t c = 0; c < 4u; ++c) { rawToUpload[rawBase + c] = static_cast(uploadBase + c); } } } bool inverseComplete = true; for (uint16_t byteOff : rawToUpload) { if (byteOff == 0xFFFFu) { inverseComplete = false; break; } } t.IsTrue(inverseComplete, "reference CT32 raw-to-upload map should cover every byte in a 64x32 CT32 page"); std::vector upload(kUploadWidth * kUploadHeight * 4u, 0u); for (uint32_t y = 0; y < kTexHeight; ++y) { for (uint32_t x = 0; x < kTexWidth; ++x) { const uint32_t texelIndex = y * kTexWidth + x; const uint32_t rawOff = referenceAddrPSMT8(kTexTbp, kTexTbw, x, y); const uint32_t uploadOff = rawToUpload[rawOff]; upload[uploadOff] = source[texelIndex]; } } const uint64_t bitblt = (static_cast(0u) << 0) | (static_cast(1u) << 16) | (static_cast(GS_PSM_CT32) << 24) | (static_cast(kTexTbp) << 32) | (static_cast(1u) << 48) | (static_cast(GS_PSM_CT32) << 56); gs.writeRegister(GS_REG_BITBLTBUF, bitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, (static_cast(kUploadWidth) << 0) | (static_cast(kUploadHeight) << 32)); gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packet; appendU64(packet, makeGifTag(static_cast(upload.size() / 16u), GIF_FMT_IMAGE, 0u, true)); appendU64(packet, 0ull); packet.insert(packet.end(), upload.begin(), upload.end()); gs.processGIFPacket(packet.data(), static_cast(packet.size())); bool aliasOk = true; uint32_t badX = 0u; uint32_t badY = 0u; uint32_t got = 0u; uint32_t expected = 0u; for (uint32_t y = 0; y < kTexHeight && aliasOk; ++y) { for (uint32_t x = 0; x < kTexWidth; ++x) { const uint32_t texelOff = GSPSMT8::addrPSMT8(kTexTbp, kTexTbw, x, y); got = vram[texelOff]; expected = source[y * kTexWidth + x]; if (got != expected) { aliasOk = false; badX = x; badY = y; break; } } } if (!aliasOk) { t.Fail("CT32 image upload should preserve Veronica's later PSMT8 sampling layout " "(first mismatch at x=" + std::to_string(badX) + ", y=" + std::to_string(badY) + ", got " + std::to_string(got) + ", expected " + std::to_string(expected) + ")"); } }); tc.Run("GS PSMT4 local-local copy respects swizzled page layout", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kSrcBp = 64u; constexpr uint32_t kDstBp = 96u; constexpr uint64_t kUploadBitblt = (static_cast(0u) << 0) | (static_cast(2u) << 16) | (static_cast(GS_PSM_T4) << 24) | (static_cast(kSrcBp) << 32) | (static_cast(2u) << 48) | (static_cast(GS_PSM_T4) << 56); constexpr uint64_t kCopyBitblt = (static_cast(kSrcBp) << 0) | (static_cast(2u) << 16) | (static_cast(GS_PSM_T4) << 24) | (static_cast(kDstBp) << 32) | (static_cast(2u) << 48) | (static_cast(GS_PSM_T4) << 56); constexpr uint64_t kCopyPos = (static_cast(0u) << 0) | (static_cast(0u) << 16) | (static_cast(32u) << 32) | (static_cast(16u) << 48); constexpr uint64_t kReadBitblt = (static_cast(kDstBp) << 0) | (static_cast(2u) << 16) | (static_cast(GS_PSM_T4) << 24) | (static_cast(0u) << 32) | (static_cast(2u) << 48) | (static_cast(GS_PSM_T4) << 56); constexpr uint64_t kReadPos = (static_cast(32u) << 0) | (static_cast(16u) << 16); constexpr uint64_t kRect = (8ull << 0) | (4ull << 32); const uint8_t packedSource[16] = { 0x10u, 0x32u, 0x54u, 0x76u, 0x98u, 0xBAu, 0xDCu, 0xFEu, 0x01u, 0x23u, 0x45u, 0x67u, 0x89u, 0xABu, 0xCDu, 0xEFu }; gs.writeRegister(GS_REG_BITBLTBUF, kUploadBitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, kRect); gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packet; appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true)); appendU64(packet, 0ull); packet.insert(packet.end(), packedSource, packedSource + sizeof(packedSource)); gs.processGIFPacket(packet.data(), static_cast(packet.size())); gs.writeRegister(GS_REG_BITBLTBUF, kCopyBitblt); gs.writeRegister(GS_REG_TRXPOS, kCopyPos); gs.writeRegister(GS_REG_TRXREG, kRect); gs.writeRegister(GS_REG_TRXDIR, 2ull); gs.writeRegister(GS_REG_BITBLTBUF, kReadBitblt); gs.writeRegister(GS_REG_TRXPOS, kReadPos); gs.writeRegister(GS_REG_TRXREG, kRect); gs.writeRegister(GS_REG_TRXDIR, 1ull); uint8_t out[16] = {}; const uint32_t outBytes = gs.consumeLocalToHostBytes(out, sizeof(out)); t.Equals(outBytes, 16u, "PSMT4 local-local copy should preserve the full packed byte count"); for (size_t i = 0; i < sizeof(packedSource); ++i) { t.Equals(out[i], packedSource[i], "PSMT4 local-local copy should preserve packed nibble order"); } }); tc.Run("GS T4 CSM1 lookup matches Veronica ClutCopy layout", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint32_t kClutCbp = 128u; constexpr uint32_t kFrameReg = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_T4) << 20) | (0ull << 26) | (0ull << 30) | (1ull << 34) | (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | // TME (1ull << 8); // FST constexpr uint32_t kExpectedColor = 0x800000FFu; // RGBA = (255,0,0,128) constexpr uint32_t kWrongColor = 0x8000FF00u; // RGBA = (0,255,0,128) const uint32_t texNibbleAddr = GSPSMT4::addrPSMT4(kTexTbp, 1u, 0u, 0u); const uint32_t texByteOff = texNibbleAddr >> 1; vram[texByteOff] = static_cast((vram[texByteOff] & 0xF0u) | 0x08u); // Veronica uploads CSM1 CLUT rows with a 64-pixel GS stride, so logical entry 8 // resolves to row 1, column 0 after the CSM1 swizzle. const uint32_t wrongClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 8u, 0u); const uint32_t expectedClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 0u, 1u); std::memcpy(vram.data() + wrongClutOff, &kWrongColor, sizeof(kWrongColor)); std::memcpy(vram.data() + expectedClutOff, &kExpectedColor, sizeof(kExpectedColor)); gs.writeRegister(GS_REG_FRAME_1, kFrameReg); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, 0ull); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); uint32_t pixel = 0u; std::memcpy(&pixel, vram.data(), sizeof(pixel)); t.Equals(pixel, kExpectedColor, "T4 CSM1 lookup should follow Veronica's swizzled CLUT row layout for logical index 8"); }); tc.Run("GS T8 CT32-uploaded CSM1 CLUT follows swizzled palette layout", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint32_t kClutCbp = 128u; constexpr uint64_t kFrameReg = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_T8) << 20) | (0ull << 26) | (0ull << 30) | (1ull << 34) | (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | // TME (1ull << 8); // FST constexpr uint64_t kClutBitblt = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24) | (static_cast(kClutCbp) << 32) | (1ull << 48) | (static_cast(GS_PSM_CT32) << 56); constexpr uint64_t kClutRect = (16ull << 0) | (2ull << 32); constexpr uint32_t kExpectedColor = 0x80FFFFFFu; const uint32_t texOff = GSPSMT8::addrPSMT8(kTexTbp, 1u, 0u, 0u); vram[texOff] = 8u; std::vector clut(32u, 0u); clut[16] = kExpectedColor; gs.writeRegister(GS_REG_BITBLTBUF, kClutBitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, kClutRect); gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packet; appendU64(packet, makeGifTag(static_cast((clut.size() * sizeof(uint32_t)) / 16u), GIF_FMT_IMAGE, 0u, true)); appendU64(packet, 0ull); const size_t payloadOffset = packet.size(); packet.resize(payloadOffset + clut.size() * sizeof(uint32_t)); std::memcpy(packet.data() + payloadOffset, clut.data(), clut.size() * sizeof(uint32_t)); gs.processGIFPacket(packet.data(), static_cast(packet.size())); gs.writeRegister(GS_REG_FRAME_1, kFrameReg); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, 0ull); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); uint32_t pixel = 0u; std::memcpy(&pixel, vram.data(), sizeof(pixel)); t.Equals(pixel, kExpectedColor, "T8 CSM1 CLUT sampling should read CT32-uploaded palette entries through GS swizzled addressing"); }); tc.Run("GS T8 CSM1 applies CSA and masks CSA bit 4 for CT32 CLUTs", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint32_t kClutCbp = 128u; constexpr uint64_t kFrameReg = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_T8) << 20) | (0ull << 26) | (0ull << 30) | (1ull << 34) | (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | (17ull << 56); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | (1ull << 8); constexpr uint32_t kExpectedColor = 0xFF204080u; constexpr uint32_t kWrongNoCsaColor = 0xFF00FF00u; constexpr uint32_t kWrongBit4Color = 0xFFFF0000u; const uint32_t texOff = GSPSMT8::addrPSMT8(kTexTbp, 1u, 0u, 0u); vram[texOff] = 0u; // CSA=17 is CSA=1 for a CT32 CLUT. Logical entry 16 is at // physical CSM1 entry 8 after address bits 3 and 4 are swapped. gs.WriteVram(GS_PSM_CT32, kClutCbp, 1u, 0u, 0u, kWrongNoCsaColor); gs.WriteVram(GS_PSM_CT32, kClutCbp, 1u, 8u, 0u, kExpectedColor); gs.WriteVram(GS_PSM_CT32, kClutCbp, 1u, 8u, 16u, kWrongBit4Color); gs.writeRegister(GS_REG_FRAME_1, kFrameReg); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, 0ull); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); uint32_t pixel = 0u; std::memcpy(&pixel, vram.data(), sizeof(pixel)); t.Equals(pixel, kExpectedColor, "T8 CSM1 should offset by CSA while CT32 ignores the fifth CSA bit"); }); tc.Run("GS T4 CSM1 preserves CSA bit 4 for CT16 CLUTs", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint32_t kClutCbp = 128u; constexpr uint64_t kFrameReg = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_T4) << 20) | (0ull << 26) | (0ull << 30) | (1ull << 34) | (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT16) << 51) | (16ull << 56); constexpr uint64_t kTexa = (0x80ull << 32); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | (1ull << 8); constexpr uint16_t kExpectedRed = 0x801Fu; constexpr uint16_t kWrongGreen = 0x83E0u; constexpr uint32_t kExpectedColor = 0x800000F8u; writePSMT4Texel(vram, kTexTbp, 1u, 0u, 0u, 1u); // CSA=16 selects the upper half of a CT16 CLUT. CSM1 swaps bits // 3 and 4 but must preserve address bit 8. gs.WriteVram(GS_PSM_CT16, kClutCbp, 1u, 1u, 0u, kWrongGreen); gs.WriteVram(GS_PSM_CT16, kClutCbp, 1u, 1u, 16u, kExpectedRed); gs.writeRegister(GS_REG_FRAME_1, kFrameReg); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, 0ull); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_TEXA, kTexa); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); uint32_t pixel = 0u; std::memcpy(&pixel, vram.data(), sizeof(pixel)); t.Equals(pixel, kExpectedColor, "CT16 CSM1 should retain CSA[4] instead of aliasing the upper palette onto the lower one"); }); tc.Run("GS TEX0 dimensions saturate at 1024 pixels", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); GSRasterizer rasterizer; constexpr uint32_t kTexTbp = 64u; constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (16ull << 14) | (static_cast(GS_PSM_CT32) << 20) | (15ull << 26) | (15ull << 30) | (1ull << 34) | (1ull << 35); constexpr uint64_t kPrim = static_cast(GS_PRIM_TRIANGLE) | (1ull << 4); constexpr uint32_t kExpectedColor = 0xFF3366CCu; constexpr uint32_t kUnsaturatedColor = 0xFF00FF00u; gs.WriteVram(GS_PSM_CT32, kTexTbp, 16u, 1u, 0u, kExpectedColor); gs.WriteVram(GS_PSM_CT32, kTexTbp, 16u, 32u, 0u, kUnsaturatedColor); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_PRIM, kPrim); const uint32_t sampled = rasterizer.sampleTexture(&gs, 1.0f / 1024.0f, 0.0f, 1.0f, 0u, 0u); t.Equals(sampled, kExpectedColor, "TW/TH values above 10 should address a 1024-pixel texture instead of growing beyond GS limits"); }); tc.Run("GS TEX2 updates CLUT state independently from TEX0", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint32_t kWrongClutCbp = 128u; constexpr uint32_t kExpectedClutCbp = 192u; constexpr uint64_t kFrameReg = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_T8) << 20) | (0ull << 26) | (0ull << 30) | (1ull << 34) | (1ull << 35) | (static_cast(kWrongClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | (1ull << 55); constexpr uint64_t kTex2 = (static_cast(GS_PSM_T8) << 20) | (static_cast(kExpectedClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | (1ull << 55); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | (1ull << 8); constexpr uint32_t kWrongColor = 0xFF00FF00u; constexpr uint32_t kExpectedColor = 0xFF0000FFu; const uint32_t texOff = GSPSMT8::addrPSMT8(kTexTbp, 1u, 0u, 0u); vram[texOff] = 8u; const uint32_t wrongClutOff = GSPSMCT32::addrPSMCT32(kWrongClutCbp, 1u, 8u, 0u); const uint32_t expectedClutOff = GSPSMCT32::addrPSMCT32(kExpectedClutCbp, 1u, 8u, 0u); std::memcpy(vram.data() + wrongClutOff, &kWrongColor, sizeof(kWrongColor)); std::memcpy(vram.data() + expectedClutOff, &kExpectedColor, sizeof(kExpectedColor)); gs.writeRegister(GS_REG_FRAME_1, kFrameReg); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, 0ull); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_TEX2_1, kTex2); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, (16ull << 0) | (16ull << 16)); uint32_t pixel = 0u; std::memcpy(&pixel, vram.data(), sizeof(pixel)); t.Equals(pixel, kExpectedColor, "TEX2 should override the active CLUT base and format state without requiring a new TEX0 write"); }); tc.Run("GS TEXCLUT offsets T8 CLUT fetch coordinates", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint32_t kClutCbp = 128u; constexpr uint64_t kFrameReg = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_T8) << 20) | (0ull << 26) | (0ull << 30) | (1ull << 34) | (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | (1ull << 55); constexpr uint64_t kTexClut = (1ull << 0) | (3ull << 6) | (2ull << 12); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | (1ull << 8); constexpr uint32_t kWrongColor = 0xFF00FF00u; constexpr uint32_t kExpectedColor = 0xFF3366CCu; const uint32_t texOff = GSPSMT8::addrPSMT8(kTexTbp, 1u, 0u, 0u); vram[texOff] = 0u; const uint32_t wrongClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 0u, 0u); const uint32_t expectedClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 3u, 2u); std::memcpy(vram.data() + wrongClutOff, &kWrongColor, sizeof(kWrongColor)); std::memcpy(vram.data() + expectedClutOff, &kExpectedColor, sizeof(kExpectedColor)); gs.writeRegister(GS_REG_FRAME_1, kFrameReg); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, 0ull); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_TEXCLUT, kTexClut); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, (16ull << 0) | (16ull << 16)); uint32_t pixel = 0u; std::memcpy(&pixel, vram.data(), sizeof(pixel)); t.Equals(pixel, kExpectedColor, "TEXCLUT should offset the CLUT lookup coordinates instead of always starting from the CLUT base"); }); tc.Run("GS TEXA expands CT24 alpha and honors AEM for black texels", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint64_t kFrameReg = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kScissor = (0ull << 0) | (1ull << 16) | (0ull << 32) | (0ull << 48); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_CT24) << 20) | (1ull << 26) | (0ull << 30) | (1ull << 34) | (1ull << 35); constexpr uint64_t kTexa = (0x55ull << 0) | (1ull << 15) | (0xAAull << 32); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | (1ull << 8); constexpr uint32_t kExpectedRed = 0x550000FFu; constexpr uint32_t kExpectedBlack = 0x00000000u; const uint32_t redOff = GSPSMCT32::addrPSMCT32(kTexTbp, 1u, 0u, 0u); vram[redOff + 0u] = 0xFFu; vram[redOff + 1u] = 0x00u; vram[redOff + 2u] = 0x00u; vram[redOff + 3u] = 0x00u; const uint32_t blackOff = GSPSMCT32::addrPSMCT32(kTexTbp, 1u, 1u, 0u); vram[blackOff + 0u] = 0x00u; vram[blackOff + 1u] = 0x00u; vram[blackOff + 2u] = 0x00u; vram[blackOff + 3u] = 0x00u; gs.writeRegister(GS_REG_FRAME_1, kFrameReg); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, kScissor); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_TEXA, kTexa); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, (16ull << 0) | (16ull << 16)); gs.writeRegister(GS_REG_UV, (16ull << 0)); gs.writeRegister(GS_REG_XYZ2, (16ull << 0)); gs.writeRegister(GS_REG_UV, (16ull << 0)); gs.writeRegister(GS_REG_XYZ2, (32ull << 0) | (16ull << 16)); const uint32_t redPixel = readReferencePSMCT32Pixel(vram, 0u, 1u, 0u, 0u); const uint32_t blackPixel = readReferencePSMCT32Pixel(vram, 0u, 1u, 1u, 0u); t.Equals(redPixel, kExpectedRed, "TEXA should supply TA0 as the alpha for non-alpha CT24 texels"); t.Equals(blackPixel, kExpectedBlack, "TEXA AEM should force zero alpha when a CT24 texel is RGB=0"); }); tc.Run("GS TCC=0 MODULATE uses texture RGB and keeps vertex alpha", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint64_t kFrameReg = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kScissor = (0ull << 0) | (0ull << 16) | (0ull << 32) | (0ull << 48); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_CT32) << 20) | (0ull << 26) | (0ull << 30) | (0ull << 34) | (0ull << 35); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | (1ull << 8); constexpr uint32_t kTexturePixel = 0x12u | (0x34u << 8) | (0x56u << 16) | (0x78u << 24); constexpr uint32_t kExpectedPixel = 0x12u | (0x34u << 8) | (0x56u << 16) | (0x44u << 24); const uint32_t texOff = GSPSMCT32::addrPSMCT32(kTexTbp, 1u, 0u, 0u); std::memcpy(vram.data() + texOff, &kTexturePixel, sizeof(kTexturePixel)); gs.writeRegister(GS_REG_FRAME_1, kFrameReg); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, kScissor); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x44808080ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, (16ull << 0) | (16ull << 16)); const uint32_t pixel = readReferencePSMCT32Pixel(vram, 0u, 1u, 0u, 0u); t.Equals(pixel, kExpectedPixel, "TCC=0 MODULATE should still use texture RGB while sourcing alpha from the shaded vertex"); }); tc.Run("GS HIGHLIGHT adds vertex alpha into RGB and texture alpha into A", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint64_t kFrameReg = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kScissor = (0ull << 0) | (0ull << 16) | (0ull << 32) | (0ull << 48); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_CT32) << 20) | (0ull << 26) | (0ull << 30) | (1ull << 34) | (2ull << 35); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | (1ull << 8); constexpr uint32_t kTexturePixel = 0x20u | (0x40u << 8) | (0x60u << 16) | (0x10u << 24); constexpr uint32_t kExpectedPixel = 0x40u | (0x60u << 8) | (0x80u << 16) | (0x30u << 24); const uint32_t texOff = GSPSMCT32::addrPSMCT32(kTexTbp, 1u, 0u, 0u); std::memcpy(vram.data() + texOff, &kTexturePixel, sizeof(kTexturePixel)); gs.writeRegister(GS_REG_FRAME_1, kFrameReg); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, kScissor); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x20808080ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, (16ull << 0) | (16ull << 16)); const uint32_t pixel = readReferencePSMCT32Pixel(vram, 0u, 1u, 0u, 0u); t.Equals(pixel, kExpectedPixel, "HIGHLIGHT should add the shaded vertex alpha into RGB and accumulate texture plus vertex alpha"); }); tc.Run("GS HIGHLIGHT2 keeps texture alpha while adding vertex alpha into RGB", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint64_t kFrameReg = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kScissor = (0ull << 0) | (0ull << 16) | (0ull << 32) | (0ull << 48); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_CT32) << 20) | (0ull << 26) | (0ull << 30) | (1ull << 34) | (3ull << 35); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | (1ull << 8); constexpr uint32_t kTexturePixel = 0x20u | (0x40u << 8) | (0x60u << 16) | (0x10u << 24); constexpr uint32_t kExpectedPixel = 0x40u | (0x60u << 8) | (0x80u << 16) | (0x10u << 24); const uint32_t texOff = GSPSMCT32::addrPSMCT32(kTexTbp, 1u, 0u, 0u); std::memcpy(vram.data() + texOff, &kTexturePixel, sizeof(kTexturePixel)); gs.writeRegister(GS_REG_FRAME_1, kFrameReg); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, kScissor); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x20808080ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, (16ull << 0) | (16ull << 16)); const uint32_t pixel = readReferencePSMCT32Pixel(vram, 0u, 1u, 0u, 0u); t.Equals(pixel, kExpectedPixel, "HIGHLIGHT2 should add the shaded vertex alpha into RGB while preserving the texture alpha"); }); tc.Run("GS TEX1 linear filter blends T4 STQ triangle samples", [](TestCase &t) { auto renderSamplePixel = [](uint64_t tex1Reg) -> uint32_t { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint32_t kClutCbp = 128u; constexpr uint64_t kFrame = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kScissor = (0ull << 0) | (4ull << 16) | (0ull << 32) | (4ull << 48); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_T4) << 20) | (1ull << 26) | (0ull << 30) | (1ull << 34) | (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51); constexpr uint64_t kPrim = static_cast(GS_PRIM_TRIANGLE) | (1ull << 4) | (0ull << 8); constexpr uint64_t kRgbaq = 0x3F80000080808080ull; constexpr uint32_t kBlack = 0x80000000u; constexpr uint32_t kWhite = 0x80FFFFFFu; writePSMT4Texel(vram, kTexTbp, 1u, 0u, 0u, 0u); writePSMT4Texel(vram, kTexTbp, 1u, 1u, 0u, 1u); std::memcpy(vram.data() + kClutCbp * 256u + 0u * 4u, &kBlack, sizeof(kBlack)); std::memcpy(vram.data() + kClutCbp * 256u + 1u * 4u, &kWhite, sizeof(kWhite)); auto packFloat = [](float value) -> uint32_t { uint32_t bits = 0u; std::memcpy(&bits, &value, sizeof(bits)); return bits; }; auto packSt = [&](float s, float tVal) -> uint64_t { return static_cast(packFloat(s)) | (static_cast(packFloat(tVal)) << 32); }; gs.writeRegister(GS_REG_FRAME_1, kFrame); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, kScissor); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_TEX1_1, tex1Reg); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, kRgbaq); gs.writeRegister(GS_REG_ST, packSt(0.0f, 0.0f)); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_ST, packSt(1.0f, 0.0f)); gs.writeRegister(GS_REG_XYZ2, (64ull << 0) | (0ull << 16)); gs.writeRegister(GS_REG_ST, packSt(0.0f, 0.0f)); gs.writeRegister(GS_REG_XYZ2, (0ull << 0) | (64ull << 16)); return readReferencePSMCT32Pixel(vram, 0u, 1u, 1u, 1u); }; constexpr uint64_t kTex1Linear = (1ull << 5) | (1ull << 6); const uint32_t nearestPixel = renderSamplePixel(0ull); const uint32_t linearPixel = renderSamplePixel(kTex1Linear); t.Equals(nearestPixel, 0x80000000u, "point sampling should keep the sampled STQ triangle pixel on texel 0"); const uint8_t linearR = static_cast(linearPixel & 0xFFu); const uint8_t linearA = static_cast((linearPixel >> 24) & 0xFFu); t.IsTrue(linearR > 0x10u && linearR < 0x70u, "linear filtering should blend the STQ triangle sample between black and white T4 texels"); t.Equals(linearA, static_cast(0x80u), "linear filtering should preserve the shared opaque alpha from the CLUT entries"); }); tc.Run("GS CLAMP modes transform texture coordinates before sampling", [](TestCase &t) { auto renderConstantUv = [](uint64_t clampReg, uint16_t fixedU, uint16_t fixedV) -> uint32_t { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint32_t kTexel0 = 0x800000FFu; constexpr uint32_t kTexel1 = 0x8000FF00u; constexpr uint32_t kTexel2 = 0x80FF0000u; constexpr uint32_t kTexel3 = 0x80FFFFFFu; constexpr uint32_t kTexelV3 = 0x80FFFF00u; constexpr uint64_t kFrame = (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_CT32) << 20) | (2ull << 26) | (2ull << 30) | (1ull << 34) | (1ull << 35); constexpr uint64_t kPrim = static_cast(GS_PRIM_TRIANGLE) | (1ull << 4) | (1ull << 8); constexpr uint64_t kRgbaq = 0x3F80000080808080ull; writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 0u, 0u, kTexel0); writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 1u, 0u, kTexel1); writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 2u, 0u, kTexel2); writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 3u, 0u, kTexel3); writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 0u, 3u, kTexelV3); gs.writeRegister(GS_REG_FRAME_1, kFrame); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, (3ull << 16) | (3ull << 48)); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_CLAMP_1, clampReg); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, kRgbaq); const uint64_t uv = static_cast(fixedU) | (static_cast(fixedV) << 16); gs.writeRegister(GS_REG_UV, uv); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, uv); gs.writeRegister(GS_REG_XYZ2, 32ull); gs.writeRegister(GS_REG_UV, uv); gs.writeRegister(GS_REG_XYZ2, (32ull << 16)); return readReferencePSMCT32Pixel(vram, 0u, 1u, 0u, 0u); }; constexpr uint64_t kClamp = 1ull; constexpr uint64_t kRegionClamp = 2ull | (1ull << 4) | (2ull << 14); constexpr uint64_t kRegionRepeat = 3ull | (1ull << 4) | (2ull << 14); t.Equals(renderConstantUv(0ull, 4u * 16u, 0u), 0x800000FFu, "REPEAT should wrap texel 4 to texel 0 for a four-wide texture"); t.Equals(renderConstantUv(0ull, 0u, 4u * 16u), 0x800000FFu, "REPEAT should wrap texel row 4 to row 0 for a four-high texture"); t.Equals(renderConstantUv(kClamp, 4u * 16u, 0u), 0x80FFFFFFu, "CLAMP should hold texel 4 at the last texel"); t.Equals(renderConstantUv(kRegionClamp, 3u * 16u, 0u), 0x80FF0000u, "REGION_CLAMP should hold texel 3 at MAXU=2"); t.Equals(renderConstantUv(kRegionRepeat, 4u * 16u, 0u), 0x80FF0000u, "REGION_REPEAT should calculate (U & UMSK) | UFIX"); }); tc.Run("GS STQ triangle interpolation divides homogeneous coordinates after DDA", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint64_t kFrame = (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kTex0 = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_CT32) << 20) | (2ull << 26) | (1ull << 34) | (1ull << 35); constexpr uint64_t kPrim = static_cast(GS_PRIM_TRIANGLE) | (1ull << 4); constexpr uint32_t kAffineTexel = 0x800000FFu; constexpr uint32_t kHomogeneousTexel = 0x8000FF00u; auto packFloat = [](float value) -> uint32_t { uint32_t bits = 0u; std::memcpy(&bits, &value, sizeof(bits)); return bits; }; auto packSt = [&](float s, float tVal) -> uint64_t { return static_cast(packFloat(s)) | (static_cast(packFloat(tVal)) << 32); }; auto packRgbaq = [&](float q) -> uint64_t { return 0x80808080ull | (static_cast(packFloat(q)) << 32); }; writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 1u, 0u, kAffineTexel); writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 2u, 0u, kHomogeneousTexel); gs.writeRegister(GS_REG_FRAME_1, kFrame); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, (4ull << 16) | (4ull << 48)); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_CLAMP_1, 1ull); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_ST, packSt(0.0f, 0.0f)); gs.writeRegister(GS_REG_RGBAQ, packRgbaq(1.0f)); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_ST, packSt(2.0f, 0.0f)); gs.writeRegister(GS_REG_RGBAQ, packRgbaq(2.0f)); gs.writeRegister(GS_REG_XYZ2, 64ull); gs.writeRegister(GS_REG_ST, packSt(0.0f, 0.0f)); gs.writeRegister(GS_REG_RGBAQ, packRgbaq(1.0f)); gs.writeRegister(GS_REG_XYZ2, (64ull << 16)); const uint32_t pixel = readReferencePSMCT32Pixel(vram, 0u, 1u, 1u, 1u); t.Equals(pixel, kHomogeneousTexel, "the DDA should interpolate S=0.75 and Q=1.375, selecting texel 2 after S/Q"); }); tc.Run("GS alpha-test AFAIL independently masks framebuffer and depth", [](TestCase &t) { constexpr uint32_t kInitialFramebuffer = 0xAB030201u; constexpr uint32_t kInitialDepth = 0x11111111u; constexpr uint64_t kTestBase = 1ull | // ATE (5ull << 1) | // ATST = GEQUAL (0x80ull << 4) | // AREF (1ull << 16) | // ZTE (1ull << 17); // ZTST = ALWAYS const GsPixelTestResult keep = drawGsPixelForTests(GS_PSM_CT32, kTestBase | (0ull << 12), false, kInitialFramebuffer, kInitialDepth, 0x00u); t.Equals(keep.framebuffer, kInitialFramebuffer, "AFAIL=KEEP should preserve the framebuffer"); t.Equals(keep.depth, kInitialDepth, "AFAIL=KEEP should preserve depth"); const GsPixelTestResult framebufferOnly = drawGsPixelForTests(GS_PSM_CT32, kTestBase | (1ull << 12), false, kInitialFramebuffer, kInitialDepth, 0x00u); t.Equals(framebufferOnly.framebuffer, 0x00563412u, "AFAIL=FB_ONLY should update RGBA"); t.Equals(framebufferOnly.depth, kInitialDepth, "AFAIL=FB_ONLY should preserve depth"); const GsPixelTestResult depthOnly = drawGsPixelForTests(GS_PSM_CT32, kTestBase | (2ull << 12), false, kInitialFramebuffer, kInitialDepth, 0x00u); t.Equals(depthOnly.framebuffer, kInitialFramebuffer, "AFAIL=ZB_ONLY should preserve the framebuffer"); t.Equals(depthOnly.depth, 0x22222222u, "AFAIL=ZB_ONLY should update depth"); const GsPixelTestResult rgbOnly = drawGsPixelForTests(GS_PSM_CT32, kTestBase | (3ull << 12), false, kInitialFramebuffer, kInitialDepth, 0x00u); t.Equals(rgbOnly.framebuffer, 0xAB563412u, "AFAIL=RGB_ONLY should preserve destination alpha on CT32"); t.Equals(rgbOnly.depth, kInitialDepth, "AFAIL=RGB_ONLY should preserve depth"); }); tc.Run("GS RGB_ONLY falls back to FB_ONLY outside CT32", [](TestCase &t) { constexpr uint32_t kInitialDepth = 0x11111111u; constexpr uint64_t kTest = 1ull | (5ull << 1) | (0x80ull << 4) | (3ull << 12) | (1ull << 16) | (1ull << 17); const GsPixelTestResult ct24 = drawGsPixelForTests(GS_PSM_CT24, kTest, false, 0x00030201u, kInitialDepth, 0x00u); t.Equals(ct24.framebuffer, 0x00563412u, "RGB_ONLY should write the full CT24 framebuffer pixel"); t.Equals(ct24.depth, kInitialDepth, "RGB_ONLY-as-FB_ONLY should preserve CT24 depth"); const GsPixelTestResult ct16 = drawGsPixelForTests(GS_PSM_CT16, kTest, false, 0x8001u, kInitialDepth, 0x00u); t.Equals(ct16.framebuffer, 0x28C2u, "RGB_ONLY should write RGB and alpha for CT16"); t.Equals(ct16.depth, kInitialDepth, "RGB_ONLY-as-FB_ONLY should preserve CT16 depth"); }); tc.Run("GS ZMSK suppresses depth without suppressing framebuffer writes", [](TestCase &t) { constexpr uint64_t kTest = 1ull | (5ull << 1) | (0x80ull << 4) | (1ull << 16) | (1ull << 17); const GsPixelTestResult result = drawGsPixelForTests(GS_PSM_CT32, kTest, true, 0xAB030201u, 0x11111111u, 0x80u); t.Equals(result.framebuffer, 0x80563412u, "a passing alpha test should write the framebuffer"); t.Equals(result.depth, 0x11111111u, "ZMSK should preserve depth"); }); tc.Run("GS DATE and DATM inspect the framebuffer-format alpha bit", [](TestCase &t) { constexpr uint32_t kInitialDepth = 0x11111111u; constexpr uint64_t kTestBase = (1ull << 14) | // DATE (1ull << 16) | // ZTE (1ull << 17); // ZTST = ALWAYS const GsPixelTestResult ct32ZeroPass = drawGsPixelForTests(GS_PSM_CT32, kTestBase, false, 0x00030201u, kInitialDepth, 0x80u); t.Equals(ct32ZeroPass.framebuffer, 0x80563412u, "DATM=0 should accept a clear CT32 alpha bit"); t.Equals(ct32ZeroPass.depth, 0x22222222u, "a passing CT32 DATE should allow depth"); const GsPixelTestResult ct32OneFail = drawGsPixelForTests(GS_PSM_CT32, kTestBase, false, 0x80030201u, kInitialDepth, 0x80u); t.Equals(ct32OneFail.framebuffer, 0x80030201u, "DATM=0 should reject a set CT32 alpha bit"); t.Equals(ct32OneFail.depth, kInitialDepth, "a failing CT32 DATE should reject depth"); const GsPixelTestResult ct32OnePass = drawGsPixelForTests(GS_PSM_CT32, kTestBase | (1ull << 15), false, 0x80030201u, kInitialDepth, 0x80u); t.Equals(ct32OnePass.framebuffer, 0x80563412u, "DATM=1 should accept a set CT32 alpha bit"); const GsPixelTestResult ct16ZeroPass = drawGsPixelForTests(GS_PSM_CT16, kTestBase, false, 0x0001u, kInitialDepth, 0x80u); t.Equals(ct16ZeroPass.framebuffer, 0xA8C2u, "DATM=0 should accept a clear CT16 alpha bit"); const GsPixelTestResult ct16OneFail = drawGsPixelForTests(GS_PSM_CT16, kTestBase, false, 0x8001u, kInitialDepth, 0x80u); t.Equals(ct16OneFail.framebuffer, 0x8001u, "DATM=0 should reject a set CT16 alpha bit"); t.Equals(ct16OneFail.depth, kInitialDepth, "a failing CT16 DATE should reject depth"); const GsPixelTestResult ct16OnePass = drawGsPixelForTests(GS_PSM_CT16, kTestBase | (1ull << 15), false, 0x8001u, kInitialDepth, 0x80u); t.Equals(ct16OnePass.framebuffer, 0xA8C2u, "DATM=1 should accept a set CT16 alpha bit"); const GsPixelTestResult ct24DatmZero = drawGsPixelForTests(GS_PSM_CT24, kTestBase, false, 0x00030201u, kInitialDepth, 0x80u); const GsPixelTestResult ct24DatmOne = drawGsPixelForTests(GS_PSM_CT24, kTestBase | (1ull << 15), false, 0x00030201u, kInitialDepth, 0x80u); t.Equals(ct24DatmZero.framebuffer, 0x00563412u, "CT24 DATE should pass for DATM=0"); t.Equals(ct24DatmOne.framebuffer, 0x00563412u, "CT24 DATE should pass for DATM=1"); t.Equals(ct24DatmOne.depth, 0x22222222u, "CT24 DATE should not block depth"); }); tc.Run("GS triangle fan subpixel quad fills rows without interior holes", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint64_t kFrame = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kScissor = (0ull << 0) | (31ull << 16) | (0ull << 32) | (31ull << 48); constexpr uint64_t kPrim = static_cast(GS_PRIM_TRIFAN); constexpr uint64_t kRgbaq = 0xFFull | (0xFFull << 8) | (0xFFull << 16) | (0x80ull << 24) | (0x3F800000ull << 32); // q = 1.0f auto makeXyzf = [](uint16_t x, uint16_t y) -> uint64_t { return static_cast(x) | (static_cast(y) << 16); }; gs.writeRegister(GS_REG_FRAME_1, kFrame); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, kScissor); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, kRgbaq); gs.writeRegister(GS_REG_XYZF2, makeXyzf(102u, 102u)); gs.writeRegister(GS_REG_XYZF2, makeXyzf(420u, 102u)); gs.writeRegister(GS_REG_XYZF2, makeXyzf(420u, 420u)); gs.writeRegister(GS_REG_XYZF2, makeXyzf(102u, 420u)); bool sawFilledRow = false; for (uint32_t y = 6u; y <= 26u; ++y) { int first = -1; int last = -1; for (uint32_t x = 6u; x <= 26u; ++x) { const size_t offset = (static_cast(y) * 64u + static_cast(x)) * 4u; uint32_t pixel = 0u; std::memcpy(&pixel, vram.data() + offset, sizeof(pixel)); if ((pixel & 0x00FFFFFFu) != 0u) { if (first < 0) { first = static_cast(x); } last = static_cast(x); } } if (first < 0 || last < 0) { continue; } sawFilledRow = true; for (int x = first; x <= last; ++x) { const size_t offset = (static_cast(y) * 64u + static_cast(x)) * 4u; uint32_t pixel = 0u; std::memcpy(&pixel, vram.data() + offset, sizeof(pixel)); if ((pixel & 0x00FFFFFFu) == 0u) { t.Fail("triangle fan quad should not leave interior holes within a covered row"); break; } } } t.IsTrue(sawFilledRow, "triangle fan quad should light at least one framebuffer row"); }); tc.Run("sceGsExecLoadImage and sceGsExecStoreImage roundtrip and free guest packets", [](TestCase &t) { PS2Runtime runtime; t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); uint8_t *const rdram = runtime.memory().getRDRAM(); constexpr uint32_t kImageAddr = 0x4000u; constexpr uint32_t kSrcAddr = 0x5000u; constexpr uint32_t kDstAddr = 0x6000u; const GsImageMem image{0u, 0u, 2u, 2u, 0u, 1u, 0u}; const uint8_t pixels[16] = { 0x10u, 0x20u, 0x30u, 0x40u, 0x50u, 0x60u, 0x70u, 0x80u, 0x90u, 0xA0u, 0xB0u, 0xC0u, 0xD0u, 0xE0u, 0xF0u, 0xFFu, }; writeGsImageTest(rdram, kImageAddr, image); std::memcpy(rdram + kSrcAddr, pixels, sizeof(pixels)); R5900Context loadCtx{}; setRegU32(loadCtx, 4, kImageAddr); setRegU32(loadCtx, 5, kSrcAddr); ps2_stubs::sceGsExecLoadImage(rdram, &loadCtx, &runtime); t.Equals(static_cast(getRegU32Test(loadCtx, 2)), 0, "sceGsExecLoadImage should succeed for a simple CT32 upload"); uint64_t loadTag = 0u; std::memcpy(&loadTag, rdram + runtime.guestHeapBase(), sizeof(loadTag)); t.Equals(loadTag, 0x1000000000008004ull, "sceGsExecLoadImage should populate the packed A+D GIF tag in guest RAM"); uint64_t loadReg1 = 0u; uint64_t loadReg2 = 0u; uint64_t loadReg3 = 0u; uint64_t loadReg4 = 0u; std::memcpy(&loadReg1, rdram + runtime.guestHeapBase() + 24u, sizeof(loadReg1)); std::memcpy(&loadReg2, rdram + runtime.guestHeapBase() + 40u, sizeof(loadReg2)); std::memcpy(&loadReg3, rdram + runtime.guestHeapBase() + 56u, sizeof(loadReg3)); std::memcpy(&loadReg4, rdram + runtime.guestHeapBase() + 72u, sizeof(loadReg4)); t.Equals(loadReg1, 0x50ull, "sceGsExecLoadImage should encode BITBLTBUF as A+D register 0x50"); t.Equals(loadReg2, 0x51ull, "sceGsExecLoadImage should encode TRXPOS as A+D register 0x51"); t.Equals(loadReg3, 0x52ull, "sceGsExecLoadImage should encode TRXREG as A+D register 0x52"); t.Equals(loadReg4, 0x53ull, "sceGsExecLoadImage should encode TRXDIR as A+D register 0x53"); expectGuestHeapReusable(t, runtime, "sceGsExecLoadImage should free its temporary GIF packet"); R5900Context storeCtx{}; setRegU32(storeCtx, 4, kImageAddr); setRegU32(storeCtx, 5, kDstAddr); ps2_stubs::sceGsExecStoreImage(rdram, &storeCtx, &runtime); t.Equals(static_cast(getRegU32Test(storeCtx, 2)), 0, "sceGsExecStoreImage should succeed for a matching CT32 readback"); uint64_t storeTag = 0u; std::memcpy(&storeTag, rdram + runtime.guestHeapBase(), sizeof(storeTag)); t.Equals(storeTag, 0x1000000000008004ull, "sceGsExecStoreImage should populate the packed A+D GIF tag in guest RAM"); uint64_t storeReg1 = 0u; uint64_t storeReg2 = 0u; uint64_t storeReg3 = 0u; uint64_t storeReg4 = 0u; std::memcpy(&storeReg1, rdram + runtime.guestHeapBase() + 24u, sizeof(storeReg1)); std::memcpy(&storeReg2, rdram + runtime.guestHeapBase() + 40u, sizeof(storeReg2)); std::memcpy(&storeReg3, rdram + runtime.guestHeapBase() + 56u, sizeof(storeReg3)); std::memcpy(&storeReg4, rdram + runtime.guestHeapBase() + 72u, sizeof(storeReg4)); t.Equals(storeReg1, 0x50ull, "sceGsExecStoreImage should encode BITBLTBUF as A+D register 0x50"); t.Equals(storeReg2, 0x51ull, "sceGsExecStoreImage should encode TRXPOS as A+D register 0x51"); t.Equals(storeReg3, 0x52ull, "sceGsExecStoreImage should encode TRXREG as A+D register 0x52"); t.Equals(storeReg4, 0x53ull, "sceGsExecStoreImage should encode TRXDIR as A+D register 0x53"); expectGuestHeapReusable(t, runtime, "sceGsExecStoreImage should free its temporary GIF packet"); bool roundtripOk = true; size_t mismatchIndex = 0u; for (size_t i = 0; i < sizeof(pixels); ++i) { if (rdram[kDstAddr + i] != pixels[i]) { roundtripOk = false; mismatchIndex = i; break; } } if (!roundtripOk) { t.Fail("sceGsExecLoadImage/sceGsExecStoreImage should roundtrip CT32 pixel data " "(first mismatch at byte " + std::to_string(mismatchIndex) + ", got " + std::to_string(rdram[kDstAddr + mismatchIndex]) + ", expected " + std::to_string(pixels[mismatchIndex]) + ")"); } }); tc.Run("sceGifPkRefLoadImage seeds A+D GIFtag nloop once (no double-count)", [](TestCase &t) { std::vector rdram(PS2_RAM_SIZE, 0u); PS2Runtime runtime; R5900Context ctx{}; constexpr uint32_t stateAddr = 0x1000u; constexpr uint32_t baseAddr = 0x2000u; constexpr uint32_t spAddr = 0x8000u; setRegU32(ctx, 4, stateAddr); setRegU32(ctx, 5, baseAddr); ps2_stubs::sceGifPkInit(rdram.data(), &ctx, &runtime); setRegU32(ctx, 29, spAddr); const uint32_t width = 16u; const uint32_t height = 1u; std::memcpy(rdram.data() + spAddr, &width, sizeof(width)); std::memcpy(rdram.data() + spAddr + 8u, &height, sizeof(height)); const uint32_t dbp = 0x3fc0u; const uint32_t dpsm = 0u; const uint32_t dbw = 1u; const uint32_t dataAddr = 0u; const uint32_t dsax = 0u; const uint32_t dsay = 0u; setRegU32(ctx, 4, stateAddr); setRegU32(ctx, 5, dbp); setRegU32(ctx, 6, dpsm); setRegU32(ctx, 7, dbw); setRegU32(ctx, 8, dataAddr); setRegU32(ctx, 9, 0u); // qwcRemaining: setup only, no image body setRegU32(ctx, 10, dsax); setRegU32(ctx, 11, dsay); ps2_stubs::sceGifPkRefLoadImage(rdram.data(), &ctx, &runtime); uint64_t tagLo = 0u; uint64_t tagHi = 0u; std::memcpy(&tagLo, rdram.data() + baseAddr + 16u, sizeof(tagLo)); std::memcpy(&tagHi, rdram.data() + baseAddr + 24u, sizeof(tagHi)); t.Equals(tagLo, static_cast(0x1000000000000004ULL), "header GIFtag lo must be nloop=4 nreg=1 A+D eop=0 (double-count would give ...0008)"); t.Equals(tagHi, static_cast(0xEULL), "header GIFtag hi must be A+D register descriptor 0xE"); uint64_t reg1Desc = 0u; uint64_t reg2Desc = 0u; uint64_t reg3Desc = 0u; uint64_t reg4Desc = 0u; std::memcpy(®1Desc, rdram.data() + baseAddr + 32u + 0u * 16u + 8u, sizeof(reg1Desc)); std::memcpy(®2Desc, rdram.data() + baseAddr + 32u + 1u * 16u + 8u, sizeof(reg2Desc)); std::memcpy(®3Desc, rdram.data() + baseAddr + 32u + 2u * 16u + 8u, sizeof(reg3Desc)); std::memcpy(®4Desc, rdram.data() + baseAddr + 32u + 3u * 16u + 8u, sizeof(reg4Desc)); t.Equals(reg1Desc, static_cast(0x50ULL), "first register qword should be BITBLTBUF (0x50)"); t.Equals(reg2Desc, static_cast(0x51ULL), "second register qword should be TRXPOS (0x51)"); t.Equals(reg3Desc, static_cast(0x52ULL), "third register qword should be TRXREG (0x52)"); t.Equals(reg4Desc, static_cast(0x53ULL), "fourth register qword should be TRXDIR (0x53)"); uint64_t reg1Payload = 0u; uint64_t reg2Payload = 0u; uint64_t reg3Payload = 0u; uint64_t reg4Payload = 0u; std::memcpy(®1Payload, rdram.data() + baseAddr + 32u + 0u * 16u + 0u, sizeof(reg1Payload)); std::memcpy(®2Payload, rdram.data() + baseAddr + 32u + 1u * 16u + 0u, sizeof(reg2Payload)); std::memcpy(®3Payload, rdram.data() + baseAddr + 32u + 2u * 16u + 0u, sizeof(reg3Payload)); std::memcpy(®4Payload, rdram.data() + baseAddr + 32u + 3u * 16u + 0u, sizeof(reg4Payload)); // BITBLTBUF: dbp=0x3fc0 (bits 32-45), dbw=1 (bits 48-53), dpsm=0 (bits 56-61). t.Equals(reg1Payload, static_cast(0x00013FC000000000ULL), "BITBLTBUF payload must encode dbp=0x3fc0, dbw=1, dpsm=0"); // TRXPOS: dsax=0, dsay=0. t.Equals(reg2Payload, static_cast(0x0ULL), "TRXPOS payload must encode dsax=0, dsay=0"); // TRXREG: width=16 (bits 0-31), height=1 (bits 32-63). t.Equals(reg3Payload, static_cast(0x0000000100000010ULL), "TRXREG payload must encode width=16, height=1"); // TRXDIR: host-to-local transfer, dir=0. t.Equals(reg4Payload, static_cast(0x0ULL), "TRXDIR payload must encode dir=0 (host-to-local)"); }); tc.Run("sceGsResetGraph frees its temporary GIF packet", [](TestCase &t) { PS2Runtime runtime; t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); std::vector rdram(PS2_RAM_SIZE, 0u); R5900Context ctx{}; setRegU32(ctx, 4, 0u); setRegU32(ctx, 5, 1u); setRegU32(ctx, 6, 2u); setRegU32(ctx, 7, 1u); ps2_stubs::sceGsResetGraph(rdram.data(), &ctx, &runtime); t.Equals(static_cast(getRegU32Test(ctx, 2)), 0, "sceGsResetGraph should succeed in reset mode"); expectGuestHeapReusable(t, runtime, "sceGsResetGraph should free its temporary GIF packet"); }); tc.Run("sceGsSyncV resumes through the scheduler with deterministic field parity", [](TestCase &t) { PS2Runtime runtime; t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); std::vector rdram(PS2_RAM_SIZE, 0u); R5900Context resetCtx{}; setRegU32(resetCtx, 4, 0u); setRegU32(resetCtx, 5, 1u); setRegU32(resetCtx, 6, 2u); setRegU32(resetCtx, 7, 1u); ps2_stubs::sceGsResetGraph(rdram.data(), &resetCtx, &runtime); runtime.registerFunction(kGsSyncWait0Pc, testGsSyncWait0); runtime.registerFunction(kGsSyncResume0Pc, testGsSyncResume0); runtime.registerFunction(kGsSyncWait1Pc, testGsSyncWait1); runtime.registerFunction(kGsSyncResume1Pc, testGsSyncResume1); g_gsSyncFirstField.store(-1, std::memory_order_release); g_gsSyncSecondField.store(-1, std::memory_order_release); R5900Context mainContext{}; mainContext.pc = kGsSyncWait0Pc; runtime.eeScheduler().reset(rdram.data(), mainContext); runtime.eeScheduler().run(); t.Equals(g_gsSyncFirstField.load(std::memory_order_acquire), 0, "first interlaced VBlank should report even field"); t.Equals(g_gsSyncSecondField.load(std::memory_order_acquire), 1, "second interlaced VBlank should report odd field"); }); tc.Run("sceGsSyncVCallback runs as a scheduler invocation on its callback stack", [](TestCase &t) { g_gsSyncCallbackHits.store(0u, std::memory_order_relaxed); g_gsSyncCallbackLastTick.store(0u, std::memory_order_relaxed); g_gsSyncCallbackSp.store(0u, std::memory_order_relaxed); g_gsSyncCallbackGp.store(0u, std::memory_order_relaxed); g_gsSyncCallbackPrevious.store(0xFFFFFFFFu, std::memory_order_relaxed); PS2Runtime runtime; t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); std::vector rdram(PS2_RAM_SIZE, 0u); runtime.configureGuestHeap(0x01F00000u, 0x01F00000u); runtime.registerFunction(kGsCallbackMainPc, testGsCallbackMain); runtime.registerFunction(kGsCallbackResumePc, testGsCallbackResume); runtime.registerFunction(kGsCallbackPc, testGsSyncVCallback); R5900Context mainContext{}; mainContext.pc = kGsCallbackMainPc; runtime.eeScheduler().reset(rdram.data(), mainContext); runtime.eeScheduler().run(); t.Equals(g_gsSyncCallbackPrevious.load(std::memory_order_acquire), 0u, "first callback registration should return no previous callback"); t.Equals(g_gsSyncCallbackHits.load(std::memory_order_acquire), 1u, "the callback should execute once at the next VBlank boundary"); t.IsTrue(g_gsSyncCallbackLastTick.load(std::memory_order_acquire) > 0u, "VSync callback should receive a positive tick value"); t.Equals(g_gsSyncCallbackGp.load(std::memory_order_acquire), kGsCallbackGp, "callback invocation should preserve the registered GP"); t.IsTrue(g_gsSyncCallbackSp.load(std::memory_order_acquire) >= 0x01F00000u, "callback invocation should use the reserved async stack pool"); t.IsTrue(g_gsSyncCallbackSp.load(std::memory_order_acquire) != kGsCallbackCallerSp, "callback invocation must not reuse the caller stack"); }); tc.Run("GS T4HL/T4HH shared-plane upload preserves both index planes via RMW", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kDbp = 64u; constexpr uint32_t kDbw = 1u; constexpr uint32_t kRrw = 8u; constexpr uint32_t kRrh = 8u; constexpr uint64_t kRect = (static_cast(kRrw) << 0) | (static_cast(kRrh) << 32); // Two independent, differing index patterns for the T4HL and T4HH planes. auto indexA = [](uint32_t x, uint32_t y) -> uint8_t { return static_cast((x * 3u + y * 5u + 1u) & 0xFu); }; auto indexB = [](uint32_t x, uint32_t y) -> uint8_t { return static_cast((x * 7u + y * 2u + 9u) & 0xFu); }; auto buildPacked = [&](const auto &indexFn) -> std::vector { std::vector packed((kRrw * kRrh) / 2u, 0u); for (uint32_t y = 0; y < kRrh; ++y) { for (uint32_t x = 0; x < kRrw; x += 2u) { const uint8_t lo = indexFn(x, y) & 0xFu; const uint8_t hi = indexFn(x + 1u, y) & 0xFu; packed[(y * kRrw + x) / 2u] = static_cast(lo | (hi << 4)); } } return packed; }; const std::vector packedA = buildPacked(indexA); const std::vector packedB = buildPacked(indexB); constexpr uint64_t kUploadHLBitblt = (static_cast(kDbp) << 32) | (static_cast(kDbw) << 48) | (static_cast(GS_PSM_T4HL) << 56); constexpr uint64_t kUploadHHBitblt = (static_cast(kDbp) << 32) | (static_cast(kDbw) << 48) | (static_cast(GS_PSM_T4HH) << 56); gs.writeRegister(GS_REG_BITBLTBUF, kUploadHLBitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, kRect); gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packetA; appendU64(packetA, makeGifTag(static_cast(packedA.size() / 16u), GIF_FMT_IMAGE, 0u, true)); appendU64(packetA, 0ull); packetA.insert(packetA.end(), packedA.begin(), packedA.end()); gs.processGIFPacket(packetA.data(), static_cast(packetA.size())); gs.writeRegister(GS_REG_BITBLTBUF, kUploadHHBitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, kRect); gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packetB; appendU64(packetB, makeGifTag(static_cast(packedB.size() / 16u), GIF_FMT_IMAGE, 0u, true)); appendU64(packetB, 0ull); packetB.insert(packetB.end(), packedB.begin(), packedB.end()); gs.processGIFPacket(packetB.data(), static_cast(packetB.size())); bool planesMatch = true; bool memReadersMatch = true; for (uint32_t y = 0; y < kRrh; ++y) { for (uint32_t x = 0; x < kRrw; ++x) { const uint32_t off = GSPSMCT32::addrPSMCT32(kDbp, kDbw, x, y); uint32_t word = 0u; std::memcpy(&word, vram.data() + off, sizeof(word)); const uint8_t expectedA = indexA(x, y); const uint8_t expectedB = indexB(x, y); const uint8_t gotA = static_cast((word >> 24) & 0xFu); const uint8_t gotB = static_cast((word >> 28) & 0xFu); if (gotA != expectedA || gotB != expectedB) planesMatch = false; const uint32_t memA = GSMem::ReadP4HL(vram.data(), kDbp, kDbw, x, y); const uint32_t memB = GSMem::ReadP4HH(vram.data(), kDbp, kDbw, x, y); if (memA != expectedA || memB != expectedB) memReadersMatch = false; } } t.IsTrue(planesMatch, "T4HL and T4HH uploads to the same shared CT32 word must not clobber each other's nibble"); t.IsTrue(memReadersMatch, "GSMem::ReadP4HL/ReadP4HH should agree with the raw shared-word nibble extraction"); // --- T8H coverage: full-byte upload, round-trip via GSMem::ReadP8H, and the // --- clobber interaction when a later T4HL nibble upload lands on the same word. constexpr uint32_t kDbpT8H = 128u; constexpr uint32_t kDbwT8H = 1u; // Full 0..255 range so both nibbles of the uploaded byte vary independently. auto byteT8H = [](uint32_t x, uint32_t y) -> uint8_t { return static_cast((x * 11u + y * 13u + 7u) & 0xFFu); }; std::vector packedT8H(kRrw * kRrh, 0u); for (uint32_t y = 0; y < kRrh; ++y) { for (uint32_t x = 0; x < kRrw; ++x) { packedT8H[y * kRrw + x] = byteT8H(x, y); } } constexpr uint64_t kUploadT8HBitblt = (static_cast(kDbpT8H) << 32) | (static_cast(kDbwT8H) << 48) | (static_cast(GS_PSM_T8H) << 56); gs.writeRegister(GS_REG_BITBLTBUF, kUploadT8HBitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, kRect); gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packetT8H; appendU64(packetT8H, makeGifTag(static_cast(packedT8H.size() / 16u), GIF_FMT_IMAGE, 0u, true)); appendU64(packetT8H, 0ull); packetT8H.insert(packetT8H.end(), packedT8H.begin(), packedT8H.end()); gs.processGIFPacket(packetT8H.data(), static_cast(packetT8H.size())); bool t8hByteMatches = true; bool t8hMemReaderMatches = true; for (uint32_t y = 0; y < kRrh; ++y) { for (uint32_t x = 0; x < kRrw; ++x) { const uint32_t off = GSPSMCT32::addrPSMCT32(kDbpT8H, kDbwT8H, x, y); uint32_t word = 0u; std::memcpy(&word, vram.data() + off, sizeof(word)); const uint8_t expected = byteT8H(x, y); const uint8_t got = static_cast((word >> 24) & 0xFFu); if (got != expected) t8hByteMatches = false; const uint32_t memByte = GSMem::ReadP8H(vram.data(), kDbpT8H, kDbwT8H, x, y); if (memByte != expected) t8hMemReaderMatches = false; } } t.IsTrue(t8hByteMatches, "T8H upload must land the full byte in bits 24-31 of the shared CT32 word"); t.IsTrue(t8hMemReaderMatches, "GSMem::ReadP8H should agree with the raw shared-word byte extraction after a T8H upload"); // Clobber interaction: upload a T8H byte plane, then upload a T4HL nibble plane to // the same shared word. WriteP4HL's nibble RMW should overwrite bits 24-27 with the // new nibble while preserving bits 28-31 (the T8H byte's high nibble). constexpr uint32_t kDbpMix = 192u; constexpr uint32_t kDbwMix = 1u; auto byteMix = [](uint32_t x, uint32_t y) -> uint8_t { return static_cast((x * 7u + y * 5u + 3u) & 0xFFu); }; auto nibbleN = [](uint32_t x, uint32_t y) -> uint8_t { return static_cast((x * 3u + y + 1u) & 0xFu); }; std::vector packedMixT8H(kRrw * kRrh, 0u); for (uint32_t y = 0; y < kRrh; ++y) { for (uint32_t x = 0; x < kRrw; ++x) { packedMixT8H[y * kRrw + x] = byteMix(x, y); } } constexpr uint64_t kUploadMixT8HBitblt = (static_cast(kDbpMix) << 32) | (static_cast(kDbwMix) << 48) | (static_cast(GS_PSM_T8H) << 56); gs.writeRegister(GS_REG_BITBLTBUF, kUploadMixT8HBitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, kRect); gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packetMixT8H; appendU64(packetMixT8H, makeGifTag(static_cast(packedMixT8H.size() / 16u), GIF_FMT_IMAGE, 0u, true)); appendU64(packetMixT8H, 0ull); packetMixT8H.insert(packetMixT8H.end(), packedMixT8H.begin(), packedMixT8H.end()); gs.processGIFPacket(packetMixT8H.data(), static_cast(packetMixT8H.size())); std::vector packedMixNibble((kRrw * kRrh) / 2u, 0u); for (uint32_t y = 0; y < kRrh; ++y) { for (uint32_t x = 0; x < kRrw; x += 2u) { const uint8_t lo = nibbleN(x, y) & 0xFu; const uint8_t hi = nibbleN(x + 1u, y) & 0xFu; packedMixNibble[(y * kRrw + x) / 2u] = static_cast(lo | (hi << 4)); } } constexpr uint64_t kUploadMixHLBitblt = (static_cast(kDbpMix) << 32) | (static_cast(kDbwMix) << 48) | (static_cast(GS_PSM_T4HL) << 56); gs.writeRegister(GS_REG_BITBLTBUF, kUploadMixHLBitblt); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, kRect); gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packetMixNibble; appendU64(packetMixNibble, makeGifTag(static_cast(packedMixNibble.size() / 16u), GIF_FMT_IMAGE, 0u, true)); appendU64(packetMixNibble, 0ull); packetMixNibble.insert(packetMixNibble.end(), packedMixNibble.begin(), packedMixNibble.end()); gs.processGIFPacket(packetMixNibble.data(), static_cast(packetMixNibble.size())); bool mixClobberMatches = true; for (uint32_t y = 0; y < kRrh; ++y) { for (uint32_t x = 0; x < kRrw; ++x) { const uint32_t off = GSPSMCT32::addrPSMCT32(kDbpMix, kDbwMix, x, y); uint32_t word = 0u; std::memcpy(&word, vram.data() + off, sizeof(word)); const uint8_t gotLow = static_cast((word >> 24) & 0xFu); const uint8_t gotHigh = static_cast((word >> 28) & 0xFu); const uint8_t expectedLow = nibbleN(x, y); const uint8_t expectedHigh = static_cast((byteMix(x, y) >> 4) & 0xFu); if (gotLow != expectedLow || gotHigh != expectedHigh) mixClobberMatches = false; } } t.IsTrue(mixClobberMatches, "T4HL nibble upload over a T8H byte must overwrite bits 24-27 with the nibble and preserve " "bits 28-31 from the T8H byte's high nibble"); }); tc.Run("GS T4HL/T4HH sampling reads only its own plane through independent CLUTs", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kTexTbp = 64u; constexpr uint32_t kClutCbpA = 128u; constexpr uint32_t kClutCbpB = 192u; constexpr uint8_t kIndexA = 4u; // T4HL plane index at the sampled texel (0..7 -> identity swizzle) constexpr uint8_t kIndexB = 0u; // T4HH plane index at the sampled texel; must differ from kIndexA // Shared CT32 word at texel (0,0): T4HL nibble occupies bits 24-27, T4HH bits 28-31. const uint32_t sharedWordOff = GSPSMCT32::addrPSMCT32(kTexTbp, 1u, 0u, 0u); const uint32_t sharedWord = (static_cast(kIndexB) << 28) | (static_cast(kIndexA) << 24); std::memcpy(vram.data() + sharedWordOff, &sharedWord, sizeof(sharedWord)); constexpr uint32_t kExpectedColorA = 0x800000FFu; // RGBA = (255,0,0,128) constexpr uint32_t kExpectedColorB = 0x8000FF00u; // RGBA = (0,255,0,128) constexpr uint32_t kDistractorColor = 0x800000AAu; // Place each plane's expected color at its own CLUT's entry for the sampled index. const uint32_t clutAOff = GSPSMCT32::addrPSMCT32(kClutCbpA, 1u, kIndexA, 0u); const uint32_t clutBOff = GSPSMCT32::addrPSMCT32(kClutCbpB, 1u, kIndexB, 0u); std::memcpy(vram.data() + clutAOff, &kExpectedColorA, sizeof(kExpectedColorA)); std::memcpy(vram.data() + clutBOff, &kExpectedColorB, sizeof(kExpectedColorB)); // Seed distractor entries at the *other* plane's index in each CLUT so that a // cross-plane nibble read (a bug reading the wrong plane, or the wrong CLUT) would // resolve to a non-matching color instead of accidentally matching by coincidence. const uint32_t clutADistractorOff = GSPSMCT32::addrPSMCT32(kClutCbpA, 1u, kIndexB, 0u); const uint32_t clutBDistractorOff = GSPSMCT32::addrPSMCT32(kClutCbpB, 1u, kIndexA, 0u); std::memcpy(vram.data() + clutADistractorOff, &kDistractorColor, sizeof(kDistractorColor)); std::memcpy(vram.data() + clutBDistractorOff, &kDistractorColor, sizeof(kDistractorColor)); constexpr uint64_t kFrameReg = (0ull << 0) | (1ull << 16) | (static_cast(GS_PSM_CT32) << 24); constexpr uint64_t kZbuf = (1ull << 32); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | // TME (1ull << 8); // FST const uint64_t kTex0HL = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_T4HL) << 20) | (0ull << 26) | (0ull << 30) | (1ull << 34) | (1ull << 35) | (static_cast(kClutCbpA) << 37) | (static_cast(GS_PSM_CT32) << 51); gs.writeRegister(GS_REG_FRAME_1, kFrameReg); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); gs.writeRegister(GS_REG_SCISSOR_1, 0ull); gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0HL); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); uint32_t pixelHL = 0u; std::memcpy(&pixelHL, vram.data(), sizeof(pixelHL)); t.Equals(pixelHL, kExpectedColorA, "T4HL sampling should resolve through its own CLUT plane, unaffected by the co-resident T4HH nibble"); const uint64_t kTex0HH = (static_cast(kTexTbp) << 0) | (1ull << 14) | (static_cast(GS_PSM_T4HH) << 20) | (0ull << 26) | (0ull << 30) | (1ull << 34) | (1ull << 35) | (static_cast(kClutCbpB) << 37) | (static_cast(GS_PSM_CT32) << 51); gs.writeRegister(GS_REG_TEX0_1, kTex0HH); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); gs.writeRegister(GS_REG_UV, 0ull); gs.writeRegister(GS_REG_XYZ2, 0ull); uint32_t pixelHH = 0u; std::memcpy(&pixelHH, vram.data(), sizeof(pixelHH)); t.Equals(pixelHH, kExpectedColorB, "T4HH sampling should resolve through its own CLUT plane, unaffected by the co-resident T4HL nibble"); }); tc.Run("GS T4HL upload deactivates the transfer at total_pixels and discards excess bytes", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); constexpr uint32_t kDbw = 1u; constexpr uint32_t kRrw = 8u; constexpr uint32_t kRrh = 8u; constexpr uint64_t kRect = (static_cast(kRrw) << 0) | (static_cast(kRrh) << 32); auto buildPacked = [&](const auto &indexFn) -> std::vector { std::vector packed((kRrw * kRrh) / 2u, 0u); for (uint32_t y = 0; y < kRrh; ++y) { for (uint32_t x = 0; x < kRrw; x += 2u) { const uint8_t lo = indexFn(x, y) & 0xFu; const uint8_t hi = indexFn(x + 1u, y) & 0xFu; packed[(y * kRrw + x) / 2u] = static_cast(lo | (hi << 4)); } } return packed; }; // --- First transfer: an ~8x oversized IMAGE payload (256 bytes / 16 qwords) for a // rect that only needs 32 bytes (64 texels). The first 32 bytes carry a known // pattern; the remaining 224 bytes are a 0xFF sentinel that must be discarded. constexpr uint32_t kDbp1 = 0u; constexpr uint64_t kUploadBitblt1 = (static_cast(kDbp1) << 32) | (static_cast(kDbw) << 48) | (static_cast(GS_PSM_T4HL) << 56); auto indexPattern1 = [](uint32_t x, uint32_t y) -> uint8_t { return static_cast((x + y * 3u + 2u) & 0xFu); }; const std::vector packed1 = buildPacked(indexPattern1); t.Equals(packed1.size(), static_cast(32), "sanity: packed rect should be 32 bytes (64 texels)"); gs.writeRegister(GS_REG_BITBLTBUF, kUploadBitblt1); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, kRect); gs.writeRegister(GS_REG_TRXDIR, 0ull); constexpr uint32_t kOversizedBytes = 256u; // 16 qwords, 8x the required 32 bytes std::vector packet; appendU64(packet, makeGifTag(static_cast(kOversizedBytes / 16u), GIF_FMT_IMAGE, 0u, true)); appendU64(packet, 0ull); const size_t payloadOffset = packet.size(); packet.resize(payloadOffset + kOversizedBytes, 0xFFu); std::memcpy(packet.data() + payloadOffset, packed1.data(), packed1.size()); gs.processGIFPacket(packet.data(), static_cast(packet.size())); const GSDebugSnapshot snap1 = gs.getDebugSnapshot(); t.Equals(snap1.transferCopiedPixels, 64u, "T4HL transfer should stop after copying exactly rrw*rrh texels"); t.Equals(snap1.trxdir, 3u, "T4HL transfer should deactivate (trxdir=3) once total_pixels is reached"); bool pattern1Ok = true; for (uint32_t y = 0; y < kRrh; ++y) { for (uint32_t x = 0; x < kRrw; ++x) { const uint32_t off = GSPSMCT32::addrPSMCT32(kDbp1, kDbw, x, y); uint32_t word = 0u; std::memcpy(&word, vram.data() + off, sizeof(word)); if (((word >> 24) & 0xFu) != indexPattern1(x, y)) pattern1Ok = false; } } t.IsTrue(pattern1Ok, "the first 64 texels of the oversized T4HL transfer should match the known pattern; sentinel bytes must not leak in"); // --- Second, correctly-sized transfer to a different DBP: proves the discarded // excess bytes from the first transfer were not mis-accounted into later state. constexpr uint32_t kDbp2 = 128u; constexpr uint64_t kUploadBitblt2 = (static_cast(kDbp2) << 32) | (static_cast(kDbw) << 48) | (static_cast(GS_PSM_T4HL) << 56); auto indexPattern2 = [](uint32_t x, uint32_t y) -> uint8_t { return static_cast((x * 5u + y + 3u) & 0xFu); }; const std::vector packed2 = buildPacked(indexPattern2); gs.writeRegister(GS_REG_BITBLTBUF, kUploadBitblt2); gs.writeRegister(GS_REG_TRXPOS, 0ull); gs.writeRegister(GS_REG_TRXREG, kRect); gs.writeRegister(GS_REG_TRXDIR, 0ull); std::vector packet2; appendU64(packet2, makeGifTag(static_cast(packed2.size() / 16u), GIF_FMT_IMAGE, 0u, true)); appendU64(packet2, 0ull); packet2.insert(packet2.end(), packed2.begin(), packed2.end()); gs.processGIFPacket(packet2.data(), static_cast(packet2.size())); const GSDebugSnapshot snap2 = gs.getDebugSnapshot(); t.Equals(snap2.transferCopiedPixels, 64u, "second, correctly-sized T4HL transfer should copy exactly rrw*rrh texels"); t.Equals(snap2.trxdir, 3u, "second T4HL transfer should also deactivate cleanly"); bool pattern2Ok = true; for (uint32_t y = 0; y < kRrh; ++y) { for (uint32_t x = 0; x < kRrw; ++x) { const uint32_t off = GSPSMCT32::addrPSMCT32(kDbp2, kDbw, x, y); uint32_t word = 0u; std::memcpy(&word, vram.data() + off, sizeof(word)); if (((word >> 24) & 0xFu) != indexPattern2(x, y)) pattern2Ok = false; } } t.IsTrue(pattern2Ok, "second T4HL transfer to a different DBP should be byte-correct, proving the discarded excess bytes from the first transfer did not leak into subsequent transfer state"); }); }); }