Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1050,7 +1050,12 @@ namespace ps2_stubs
}

const uint32_t fbw = std::max<uint32_t>(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<uint64_t>(g_gparam.interlace & 0x1u) << 0) |
(static_cast<uint64_t>(g_gparam.ffmode & 0x1u) << 1);
Expand Down Expand Up @@ -1121,7 +1126,12 @@ namespace ps2_stubs
}

const uint32_t fbw = std::max<uint32_t>(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<uint64_t>(g_gparam.interlace & 0x1u) << 0) |
(static_cast<uint64_t>(g_gparam.ffmode & 0x1u) << 1);
Expand Down
7 changes: 5 additions & 2 deletions ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h
Original file line number Diff line number Diff line change
Expand Up @@ -1875,8 +1875,11 @@ namespace
auto &regs = 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;
Expand Down
151 changes: 149 additions & 2 deletions ps2xTest/src/ps2_gs_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t> 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<uint8_t> 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<uint8_t> 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;
Expand Down