From 6978322db255dc3a76c4580b92f7835c00366e2e Mon Sep 17 00:00:00 2001 From: Shane Michael Mathews Date: Tue, 7 Jul 2026 14:32:52 -0400 Subject: [PATCH 1/2] fix(runtime): program display read circuit 2 only in applyGsDispEnv and double-buffer seeds applyGsDispEnv -- the shared helper behind sceGsPutDispEnv and the sceGsSwapDBuff/Dc double-buffer paths -- translated a single-circuit sceGsDispEnv struct into GS register writes and programmed BOTH read circuits (dispfb1/display1 and dispfb2/display2). Real libgraph sceGsPutDispEnv programs only PMODE, SMODE2, DISPFB2, DISPLAY2 and BGCOLOR; the env describes read circuit 2, and circuit 1 is left for the game to drive itself via GS privileged MMIO. The circuit-1 clobber forced both read circuits onto the same surface, collapsing any two-buffer blend (e.g. a movie underlay beneath a UI overlay, PMODE=0x8007). Stop writing circuit 1 so the guest's own DISPFB1/DISPLAY1 MMIO writes survive. Making applyGsDispEnv circuit-2-only exposed the double-buffer seeds: sceGsSetDefDBuffDc and sceGsSetDefDBuff seeded PMODE=makePmode(1,1,...)=0x8007 (both circuits), but the swap path only ever programs circuit 2. Post-fix, circuit 1 would freeze at reset defaults while circuit 2 flipped, so the compositor blended that stale circuit-1 surface over the correct circuit-2 page for any stock-double-buffer title. Seed read circuit 2 only -- makePmode(0,1,...) / 0x8006 -- at both sites; EN2-only matches real-hardware single-circuit double-buffering. GsSetCrt / sceGsResetGraph, which enable EN1 and program the circuit-1 registers consistently, are a separate correct path and are left untouched. Tests (ps2xTest/src/ps2_gs_tests.cpp): migrate the existing sceGsSwapDBuffDc display-page assertions from circuit 1 to circuit 2, and add three cases -- sceGsPutDispEnv, sceGsSwapDBuffDc, and the non-Dc sceGsSwapDBuff twin -- each asserting EN1==0 / EN2==1 and that pre-seeded circuit-1 DISPFB1/DISPLAY1 sentinels survive. Known residual: ps2_memory.cpp reset defaults for dispfb1/display1 are non-zero (fbw=10 / 639x447) rather than the null region a real GS reset leaves, so a game that enables EN1 without ever programming circuit 1 would show garbage here versus nothing on hardware. Unreachable through the HLE paths (which this change fully fixes); zeroing the boot defaults has wider early-boot-presentation blast radius and is left as a known residual. ps2x_tests: 299 passed, 0 failed. --- ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp | 8 +- .../src/lib/Kernel/Stubs/Helpers/Support.h | 5 +- ps2xTest/src/ps2_gs_tests.cpp | 147 +++++++++++++++++- 3 files changed, 154 insertions(+), 6 deletions(-) diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp index 7e46972fc..8eec66750 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp @@ -1050,7 +1050,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); @@ -1121,7 +1123,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 a98e548d4..05f44ee50 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h @@ -1875,8 +1875,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 4b0edc038..568271a55 100644 --- a/ps2xTest/src/ps2_gs_tests.cpp +++ b/ps2xTest/src/ps2_gs_tests.cpp @@ -459,12 +459,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; From 592aaf62b86938f6318d2fbc3db98cf6a1c0d87d Mon Sep 17 00:00:00 2001 From: Shane Michael Mathews Date: Wed, 15 Jul 2026 15:05:28 -0400 Subject: [PATCH 2/2] fix(recomp): include before elfio.hpp for fixed-width int types Without an explicit include, elf_parser.h relied on elfio.hpp transitively pulling in , which no longer happens with the newer libstdc++/GCC toolchain in CI, breaking the build. --- ps2xRecomp/include/ps2recomp/elf_parser.h | 1 + 1 file changed, 1 insertion(+) 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