diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp index 7e46972fc..0ffc3e6f5 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp @@ -1050,7 +1050,12 @@ namespace ps2_stubs } const uint32_t fbw = std::max(1u, (w + 63u) / 64u); - const uint64_t pmode = makePmode(1u, 1u, 0u, 0u, 0u, 0x80u); + // Double-buffering drives read circuit 2 only: on swap, applyGsDispEnv + // programs DISPFB2/DISPLAY2 and never touches circuit 1. Enabling EN1 + // here would advertise a circuit this path never programs, leaving the + // compositor to blend circuit 1's stale reset-default surface over the + // correct circuit-2 page. Seed EN2 only; circuit 1 stays guest-owned. + 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); @@ -1121,7 +1126,12 @@ namespace ps2_stubs } const uint32_t fbw = std::max(1u, (w + 63u) / 64u); - const uint64_t pmode = makePmode(1u, 1u, 0u, 0u, 0u, 0x80u); + // Double-buffering drives read circuit 2 only: on swap, applyGsDispEnv + // programs DISPFB2/DISPLAY2 and never touches circuit 1. Enabling EN1 + // here would advertise a circuit this path never programs, leaving the + // compositor to blend circuit 1's stale reset-default surface over the + // correct circuit-2 page. Seed EN2 only; circuit 1 stays guest-owned. + 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 a98e548d4..89126592b 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h @@ -1875,8 +1875,11 @@ 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 NOT programmed here: sceGsPutDispEnv's + // env struct describes read circuit 2 only; the game drives circuit 1 + // itself via GS privileged MMIO. Writing circuit 1 from the single- + // circuit env forces both read circuits onto the same surface and + // collapses any two-buffer blend (e.g. movie underlay + UI overlay). 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 4b0edc038..ca7dddf0d 100644 --- a/ps2xTest/src/ps2_gs_tests.cpp +++ b/ps2xTest/src/ps2_gs_tests.cpp @@ -459,12 +459,159 @@ 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); + + // The double-buffer path programs read circuit 2 only. Its seeded + // PMODE must NOT enable read circuit 1 (EN1); otherwise the + // compositor blends the never-programmed circuit-1 surface (frozen + // at reset defaults) over the correct circuit-2 page. + 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); + + // The double-buffer path programs read circuit 2 only. Its seeded + // PMODE must NOT enable read circuit 1 (EN1); otherwise the + // compositor blends the never-programmed circuit-1 surface (frozen + // at reset defaults) over the correct circuit-2 page. + 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;