applyGsDispEnv programs display read circuit 2 only#152
Open
smmathews wants to merge 3 commits into
Open
Conversation
…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
force-pushed
the
feature/05-gs-dispenv-circuit2
branch
from
July 7, 2026 18:33
cc2ff60 to
6978322
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
applyGsDispEnv— the shared helper behind thesceGsPutDispEnvHLE and thesceGsSwapDBuff/sceGsSwapDBuffDcdouble-buffer paths — translated a singlesceGsDispEnvstruct into GS privileged register writes and programmed bothdisplay read circuits:
dispfb1/display1anddispfb2/display2, fromthe one-circuit env. Real libgraph
sceGsPutDispEnvprograms onlyPMODE,SMODE2,DISPFB2,DISPLAY2andBGCOLOR; 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) programDISPFB1/DISPLAY1themselves.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.h—applyGsDispEnv: deletedthe two circuit-1 assignments (
regs.dispfb1 = env.dispfb; regs.display1 = env.display;)and replaced them with an explanatory comment.
PMODE,SMODE2,DISPFB2,DISPLAY2,BGCOLORare unchanged. The guest's own MMIO writes toDISPFB1/DISPLAY1now survive.Collateral: double-buffer PMODE seeds
Making
applyGsDispEnvcircuit-2-only exposed the double-buffer seed helpers.sceGsSetDefDBuffDcandsceGsSetDefDBuff(
ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp) seededPMODE = makePmode(1,1,...) = 0x8007(both read circuits enabled), but the only thing that programs a displaycircuit on the swap path is
applyGsDispEnv(db.disp[which]), which post-fixwrites
DISPFB2/DISPLAY2only. So the double-buffer path drives circuit 2exclusively; 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
EN1everlooked valid. Post-fix, circuit 1 would freeze at the runtime's reset defaults
(
dispfb1fbw=10,display1639×447) while circuit 2 flipped, so the compositor(
ps2_gs_gpu.cpp) would blend that stale circuit-1 surface (fbp 0) over thecorrect circuit-2 page for any title using the stock double-buffer library
without driving circuit 1 itself.
Remedy: seed the double-buffer
PMODEwith read circuit 2 only —makePmode(1u, 1u, ...)→makePmode(0u, 1u, ...)at both sites, i.e.0x8007→0x8006. A seed that enables a circuit the path never programs iswrong;
EN2-only matches real-hardware semantics, because plain libgraphdouble-buffering is single-circuit (one visible circuit alternating
framebuffers).
GsSetCrt/sceGsResetGraph, which enableEN1, also programthe circuit-1 registers consistently and are a different, correct code path —
left untouched.
Known residual (follow-up)
ps2_memory.cppseeds the circuit-1/2 display reset defaults non-null (dispfb1fbw=10,display1639×447) instead of the null region a real GS reset leaves. That keeps the compositor'shasDisplaySetupgate true before circuit 1 is ever programmed, so a title that enablesEN1via raw MMIO without writingDISPFB1would 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
EN1also programs circuit 1; the double-buffer seeds are nowEN2-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=EN1only, 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 (PMODEis 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==1andcircuit-1 sentinel preservation:
sceGsPutDispEnvcircuit-2-only: pre-seedsdispfb1/display1sentinels,invokes the HLE with a known env, asserts circuit-2 + shared registers carry
env values while the circuit-1 sentinels are untouched.
sceGsSwapDBuffDcswap path: aftersceGsSetDefDBuffDc+sceGsSwapDBuffDc,asserts the seeded
PMODEdoes not enable read circuit 1 (EN1==0) and doesenable read circuit 2 (
EN2==1), and that pre-seeded circuit-1DISPFB1/DISPLAY1sentinels survive the swap.sceGsSwapDBuffswap path: the non-Dc twin, drivingsceGsSetDefDBuff+sceGsSwapDBuffwith identical assertions so the non-Dc seed is directlyexercised rather than relying on correct-by-symmetry.
The migrated
sceGsSetDefDBuffDctest had its post-swap assertions correctedfrom
dispfb1/display1todispfb2/display2.Verification
ps2x_tests: 299 tests, 0 failed.makePmodeseeds back to(1,1)/0x8007atboth sites fails exactly the two swap-path
EN1==0assertions — one from theDc test, one from the non-Dc test (297/299); restoring returns to 299/299.
-DPS2X_BUILD_STUDIO=OFFand-march=native -include cstdintinCMAKE_CXX_FLAGS(host-local only, not committed).