diff --git a/ps2xRecomp/include/ps2recomp/elf_parser.h b/ps2xRecomp/include/ps2recomp/elf_parser.h index c8cafe5ee..10f9e4ce8 100644 --- a/ps2xRecomp/include/ps2recomp/elf_parser.h +++ b/ps2xRecomp/include/ps2recomp/elf_parser.h @@ -1,6 +1,7 @@ #ifndef PS2RECOMP_ELF_PARSER_H #define PS2RECOMP_ELF_PARSER_H +#include #include #include #include diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp index bd68b714b..0abf8aec6 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp @@ -1053,7 +1053,9 @@ namespace ps2_stubs } const uint32_t fbw = std::max(1u, (w + 63u) / 64u); - const uint64_t pmode = makePmode(1u, 1u, 0u, 0u, 0u, 0x80u); + // Seed read circuit 2 only (EN2): the swap path programs DISPFB2/DISPLAY2 and + // never circuit 1, so enabling EN1 would advertise a circuit it never drives. + const uint64_t pmode = makePmode(0u, 1u, 0u, 0u, 0u, 0x80u); const uint64_t smode2 = (static_cast(g_gparam.interlace & 0x1u) << 0) | (static_cast(g_gparam.ffmode & 0x1u) << 1); @@ -1124,7 +1126,9 @@ namespace ps2_stubs } const uint32_t fbw = std::max(1u, (w + 63u) / 64u); - const uint64_t pmode = makePmode(1u, 1u, 0u, 0u, 0u, 0x80u); + // Seed read circuit 2 only (EN2): the swap path programs DISPFB2/DISPLAY2 and + // never circuit 1, so enabling EN1 would advertise a circuit it never drives. + const uint64_t pmode = makePmode(0u, 1u, 0u, 0u, 0u, 0x80u); const uint64_t smode2 = (static_cast(g_gparam.interlace & 0x1u) << 0) | (static_cast(g_gparam.ffmode & 0x1u) << 1); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h index 52fa84a45..c1bbc3b35 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h @@ -1883,8 +1883,9 @@ namespace auto ®s = runtime->memory().gs(); regs.pmode = env.pmode; regs.smode2 = env.smode2; - regs.dispfb1 = env.dispfb; - regs.display1 = env.display; + // Circuit 1 (DISPFB1/DISPLAY1) is deliberately not programmed: the env + // describes read circuit 2 only, and the game drives circuit 1 itself via + // GS MMIO. Writing it here would collapse any two-buffer blend. regs.dispfb2 = env.dispfb; regs.display2 = env.display; regs.bgcolor = env.bgcolor; diff --git a/ps2xTest/src/ps2_gs_tests.cpp b/ps2xTest/src/ps2_gs_tests.cpp index 4c3e8ec1c..88169dcbc 100644 --- a/ps2xTest/src/ps2_gs_tests.cpp +++ b/ps2xTest/src/ps2_gs_tests.cpp @@ -466,12 +466,155 @@ void register_ps2_gs_tests() setRegU32(ctx, 5, 1u); ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, &runtime); - t.Equals(runtime.memory().gs().dispfb1 & 0x1FFull, 151ull, + t.Equals(runtime.memory().gs().dispfb2 & 0x1FFull, 151ull, "sceGsSwapDBuffDc should program GS to the selected display page"); - t.Equals((runtime.memory().gs().display1 >> 32) & 0x0FFFull, 639ull, + t.Equals((runtime.memory().gs().display2 >> 32) & 0x0FFFull, 639ull, "sceGsSwapDBuffDc should preserve the display width from the seeded env"); }); + tc.Run("sceGsSwapDBuffDc drives read circuit 2 only and preserves circuit 1", [](TestCase &t) + { + PS2Runtime runtime; + t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); + + // Seed distinct circuit-1 sentinels AFTER initialize(); reset + // defaults are fbw=10 / 639x447, so these differ from them. + runtime.memory().gs().dispfb1 = 0xDEADBEEFDEADBEEFull; + runtime.memory().gs().display1 = 0xCAFEF00DCAFEF00Dull; + runtime.memory().gs().pmode = 0u; + + std::vector rdram(PS2_RAM_SIZE, 0u); + constexpr uint32_t kEnvAddr = 0x7000u; + constexpr uint32_t kDBuffSize = 0x330u; + + R5900Context ctx{}; + setRegU32(ctx, 4, kEnvAddr); + setRegU32(ctx, 5, 0u); // psm + setRegU32(ctx, 6, 640u); // width + setRegU32(ctx, 7, 448u); // height + std::memset(rdram.data() + kEnvAddr, 0xCD, kDBuffSize); + ps2_stubs::sceGsSetDefDBuffDc(rdram.data(), &ctx, &runtime); + + std::memset(&ctx, 0, sizeof(ctx)); + setRegU32(ctx, 4, kEnvAddr); + setRegU32(ctx, 5, 1u); // swap to page 1 + ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, &runtime); + + // Double-buffer path is circuit-2-only: the seeded PMODE must not enable + // EN1, or the compositor blends a never-programmed circuit-1 surface. + t.Equals(runtime.memory().gs().pmode & 0x1ull, 0x0ull, + "sceGsSwapDBuffDc must not enable read circuit 1 (EN1) in PMODE"); + t.Equals(runtime.memory().gs().pmode & 0x2ull, 0x2ull, + "sceGsSwapDBuffDc should enable read circuit 2 (EN2) in PMODE"); + + // The shared circuit-2-only helper must leave circuit 1 untouched. + t.Equals(runtime.memory().gs().dispfb1, 0xDEADBEEFDEADBEEFull, + "sceGsSwapDBuffDc must not clobber circuit-1 DISPFB1"); + t.Equals(runtime.memory().gs().display1, 0xCAFEF00DCAFEF00Dull, + "sceGsSwapDBuffDc must not clobber circuit-1 DISPLAY1"); + }); + + tc.Run("sceGsSwapDBuff drives read circuit 2 only and preserves circuit 1", [](TestCase &t) + { + PS2Runtime runtime; + t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); + + // Seed distinct circuit-1 sentinels AFTER initialize(); reset + // defaults are fbw=10 / 639x447, so these differ from them. + runtime.memory().gs().dispfb1 = 0xDEADBEEFDEADBEEFull; + runtime.memory().gs().display1 = 0xCAFEF00DCAFEF00Dull; + runtime.memory().gs().pmode = 0u; + + std::vector rdram(PS2_RAM_SIZE, 0u); + constexpr uint32_t kEnvAddr = 0x8000u; + constexpr uint32_t kDBuffSize = 0x330u; + constexpr uint32_t kStackAddr = 0xB00u; + + R5900Context ctx{}; + setRegU32(ctx, 4, kEnvAddr); + setRegU32(ctx, 5, 0u); // psm + setRegU32(ctx, 6, 640u); // width + setRegU32(ctx, 7, 448u); // height + // sceGsSetDefDBuff (non-Dc) reads its trailing ztest/zpsm/clear + // args from the guest stack via sp+16/sp+20/sp+24, so sp (r29) + // must point at valid, zero-initialized rdram. + setRegU32(ctx, 29, kStackAddr); + std::memset(rdram.data() + kEnvAddr, 0xCD, kDBuffSize); + ps2_stubs::sceGsSetDefDBuff(rdram.data(), &ctx, &runtime); + + std::memset(&ctx, 0, sizeof(ctx)); + setRegU32(ctx, 4, kEnvAddr); + setRegU32(ctx, 5, 1u); // swap to page 1 + ps2_stubs::sceGsSwapDBuff(rdram.data(), &ctx, &runtime); + + // Double-buffer path is circuit-2-only: the seeded PMODE must not enable + // EN1, or the compositor blends a never-programmed circuit-1 surface. + t.Equals(runtime.memory().gs().pmode & 0x1ull, 0x0ull, + "sceGsSwapDBuff must not enable read circuit 1 (EN1) in PMODE"); + t.Equals(runtime.memory().gs().pmode & 0x2ull, 0x2ull, + "sceGsSwapDBuff should enable read circuit 2 (EN2) in PMODE"); + + // The shared circuit-2-only helper must leave circuit 1 untouched. + t.Equals(runtime.memory().gs().dispfb1, 0xDEADBEEFDEADBEEFull, + "sceGsSwapDBuff must not clobber circuit-1 DISPFB1"); + t.Equals(runtime.memory().gs().display1, 0xCAFEF00DCAFEF00Dull, + "sceGsSwapDBuff must not clobber circuit-1 DISPLAY1"); + }); + + tc.Run("sceGsPutDispEnv programs read circuit 2 only and leaves circuit 1 untouched", [](TestCase &t) + { + PS2Runtime runtime; + t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); + + runtime.memory().gs().dispfb1 = 0xDEADBEEFDEADBEEFull; + runtime.memory().gs().display1 = 0xCAFEF00DCAFEF00Dull; + runtime.memory().gs().dispfb2 = 0u; + runtime.memory().gs().display2 = 0u; + runtime.memory().gs().pmode = 0u; + runtime.memory().gs().smode2 = 0u; + runtime.memory().gs().bgcolor = 0u; + + std::vector rdram(PS2_RAM_SIZE, 0u); + constexpr uint32_t kEnvAddr = 0x6000u; + constexpr uint32_t kPmodeOffset = 0u; + constexpr uint32_t kSmode2Offset = 8u; + constexpr uint32_t kDispFbOffset = 16u; + constexpr uint32_t kDisplayOffset = 24u; + constexpr uint32_t kBgColorOffset = 32u; + + const uint64_t pmode = 0x8007ull; + const uint64_t smode2 = 0x1ull; + const uint64_t dispfb = 0x9070ull; + const uint64_t display = 0x1234000000005678ull; + const uint64_t bgcolor = 0x00445566ull; + + std::memcpy(rdram.data() + kEnvAddr + kPmodeOffset, &pmode, sizeof(pmode)); + std::memcpy(rdram.data() + kEnvAddr + kSmode2Offset, &smode2, sizeof(smode2)); + std::memcpy(rdram.data() + kEnvAddr + kDispFbOffset, &dispfb, sizeof(dispfb)); + std::memcpy(rdram.data() + kEnvAddr + kDisplayOffset, &display, sizeof(display)); + std::memcpy(rdram.data() + kEnvAddr + kBgColorOffset, &bgcolor, sizeof(bgcolor)); + + R5900Context ctx{}; + setRegU32(ctx, 4, kEnvAddr); + ps2_stubs::sceGsPutDispEnv(rdram.data(), &ctx, &runtime); + + t.Equals(runtime.memory().gs().pmode, pmode, + "sceGsPutDispEnv should program PMODE from the env"); + t.Equals(runtime.memory().gs().smode2, smode2, + "sceGsPutDispEnv should program SMODE2 from the env"); + t.Equals(runtime.memory().gs().dispfb2, dispfb, + "sceGsPutDispEnv should program DISPFB2 from the env"); + t.Equals(runtime.memory().gs().display2, display, + "sceGsPutDispEnv should program DISPLAY2 from the env"); + t.Equals(runtime.memory().gs().bgcolor, bgcolor, + "sceGsPutDispEnv should program BGCOLOR from the env"); + + t.Equals(runtime.memory().gs().dispfb1, 0xDEADBEEFDEADBEEFull, + "sceGsPutDispEnv must not clobber circuit-1 DISPFB1"); + t.Equals(runtime.memory().gs().display1, 0xCAFEF00DCAFEF00Dull, + "sceGsPutDispEnv must not clobber circuit-1 DISPLAY1"); + }); + tc.Run("sceGsSetDefDBuffDc seeds a clear packet and swap clears the draw buffer", [](TestCase &t) { PS2Runtime runtime;