Skip to content

applyGsDispEnv programs display read circuit 2 only#152

Open
smmathews wants to merge 3 commits into
ran-j:mainfrom
smmathews:feature/05-gs-dispenv-circuit2
Open

applyGsDispEnv programs display read circuit 2 only#152
smmathews wants to merge 3 commits into
ran-j:mainfrom
smmathews:feature/05-gs-dispenv-circuit2

Conversation

@smmathews

@smmathews smmathews commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Problem

applyGsDispEnv — the shared helper behind the sceGsPutDispEnv HLE and the
sceGsSwapDBuff/sceGsSwapDBuffDc double-buffer paths — translated a single
sceGsDispEnv struct into GS privileged register writes and programmed both
display read circuits: dispfb1/display1 and dispfb2/display2, from
the one-circuit env. 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.

Titles that composite two different buffers (for example a movie/scene underlay
beneath a UI overlay, PMODE=0x8007) program DISPFB1/DISPLAY1 themselves.
The HLE's circuit-1 clobber forced both read circuits onto the same surface,
collapsing the blend.

Fix

ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.happlyGsDispEnv: deleted
the two circuit-1 assignments (regs.dispfb1 = env.dispfb; regs.display1 = env.display;)
and replaced them with an explanatory comment. PMODE, SMODE2, DISPFB2,
DISPLAY2, BGCOLOR are unchanged. The guest's own MMIO writes to
DISPFB1/DISPLAY1 now survive.

Collateral: double-buffer PMODE seeds

Making applyGsDispEnv circuit-2-only exposed the double-buffer seed helpers.
sceGsSetDefDBuffDc and sceGsSetDefDBuff
(ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp) seeded PMODE = makePmode(1,1,...) = 0x8007 (both read circuits enabled), but the only thing that programs a display
circuit on the swap path is applyGsDispEnv(db.disp[which]), which post-fix
writes DISPFB2/DISPLAY2 only. So the double-buffer path drives circuit 2
exclusively; the buffer flip only ever moved circuit 2.

Before the fix the shared helper wrote both circuits, so each swap dragged
circuit 1 along in lockstep (equal to circuit 2) — the only reason EN1 ever
looked valid. Post-fix, circuit 1 would freeze at the runtime's reset defaults
(dispfb1 fbw=10, display1 639×447) while circuit 2 flipped, so the compositor
(ps2_gs_gpu.cpp) would blend that stale circuit-1 surface (fbp 0) over the
correct circuit-2 page for any title using the stock double-buffer library
without driving circuit 1 itself.

Remedy: seed the double-buffer PMODE with read circuit 2 only —
makePmode(1u, 1u, ...)makePmode(0u, 1u, ...) at both sites, i.e.
0x80070x8006. A seed that enables a circuit the path never programs is
wrong; EN2-only matches real-hardware semantics, because plain libgraph
double-buffering is single-circuit (one visible circuit alternating
framebuffers). GsSetCrt / sceGsResetGraph, which enable EN1, also program
the circuit-1 registers consistently and are a different, correct code path —
left untouched.

Known residual (follow-up)

ps2_memory.cpp seeds the circuit-1/2 display reset defaults non-null (dispfb1 fbw=10, display1 639×447) instead of the null region a real GS reset leaves. That keeps the compositor's hasDisplaySetup gate true before circuit 1 is ever programmed, so a title that enables EN1 via raw MMIO without writing DISPFB1 would blend circuit 1's stale VRAM (page 0) over circuit 2 — garbage where hardware shows nothing.

Why not here: it's unreachable through the HLE paths this PR fixes (every HLE writer that sets EN1 also programs circuit 1; the double-buffer seeds are now EN2-only), so folding in an unrelated boot-defaults change would only widen the blast radius.

Suggested follow-up: zero the four display-circuit boot defaults in ps2_memory.cpp — the hardware-correct value — with a regression test (zero-init regs, PMODE=EN1 only, assert circuit 1 contributes no presentation frame). ~2 lines; it removes the root cause instead of masking it in the compositor, and the blast radius is contained (PMODE is already zero at boot, so nothing presents until a game programs it, and the GS tests supply their own register state).

Tests (ps2xTest/src/ps2_gs_tests.cpp)

Three tests cover the behavior, each asserting EN1==0 / EN2==1 and
circuit-1 sentinel preservation:

  • sceGsPutDispEnv circuit-2-only: pre-seeds dispfb1/display1 sentinels,
    invokes the HLE with a known env, asserts circuit-2 + shared registers carry
    env values while the circuit-1 sentinels are untouched.
  • sceGsSwapDBuffDc swap path: after sceGsSetDefDBuffDc + sceGsSwapDBuffDc,
    asserts the seeded PMODE does not enable read circuit 1 (EN1==0) and does
    enable read circuit 2 (EN2==1), and that pre-seeded circuit-1
    DISPFB1/DISPLAY1 sentinels survive the swap.
  • sceGsSwapDBuff swap path: the non-Dc twin, driving sceGsSetDefDBuff +
    sceGsSwapDBuff with identical assertions so the non-Dc seed is directly
    exercised rather than relying on correct-by-symmetry.

The migrated sceGsSetDefDBuffDc test had its post-swap assertions corrected
from dispfb1/display1 to dispfb2/display2.

Verification

  • ps2x_tests: 299 tests, 0 failed.
  • Revert behavior: reverting the makePmode seeds back to (1,1)/0x8007 at
    both sites fails exactly the two swap-path EN1==0 assertions — one from the
    Dc test, one from the non-Dc test (297/299); restoring returns to 299/299.
  • Build/toolchain note: GCC 16 host; verification build configured with
    -DPS2X_BUILD_STUDIO=OFF and -march=native -include cstdint in
    CMAKE_CXX_FLAGS (host-local only, not committed).

…nd 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.
@smmathews
smmathews force-pushed the feature/05-gs-dispenv-circuit2 branch from cc2ff60 to 6978322 Compare July 7, 2026 18:33
@smmathews
smmathews marked this pull request as ready for review July 7, 2026 18:44
…ypes

Without an explicit include, elf_parser.h relied on elfio.hpp
transitively pulling in <cstdint>, which no longer happens with
the newer libstdc++/GCC toolchain in CI, breaking the build.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant