Skip to content

Dual present filters: a set for 2D scenes and one for 3D (dw2003recomp #122) - #111

Merged
gascarcella merged 3 commits into
mainfrom
gascarcella/dual-filters-runtime
Oct 26, 2026
Merged

gascarcella merged 3 commits into
mainfrom
gascarcella/dual-filters-runtime

Conversation

@gascarcella

Copy link
Copy Markdown
Owner

A game can now opt in to two present-filter sets, one for its 2D scenes and one for its 3D scenes, and the runtime switches between them as the game moves between scenes (dw2003recomp #122). A game that doesn't opt in keeps one set and sees no change. This PR is the contract and the runtime. The launcher and the in-game menu are a separate PR stacked on this one.

What

  • Contract (first commit, 68efaf6):
    • game.json gets video.scenes (2d.label, 2d.filter, 3d.label), in the schema and in tools/game_gen.py.
    • The generator writes PSXSTACK_GAME_SCENES, PSXSTACK_GAME_SCENE_2D_LABEL, PSXSTACK_GAME_SCENE_3D_LABEL and PSXSTACK_GAME_SCENE_2D_FILTER.
    • New adapter probe game_state_scene() returns PORT_SCENE_3D (0) or PORT_SCENE_2D (1). Its weak default is 3D.
    • examples/dw2003.game.json has the scenes block.
    • The change is additive, so PSXSTACK_API stays 1.
  • Settings:
    • video.filter and video.crt are now the 3D set.
    • The new video.scene_2d { filter, crt } is the 2D set. It is read member by member over its default: game.json's 2d.filter with the default crt values, or a copy of the 3D set when game.json names no 2D filter. A bad member exits 64 and names the key.
    • A game without scenes logs scene_2d as unknown and ignores it.
    • --print-settings writes scene_2d for a game that opts in, and its output loads back to the same output.
  • Command line: --filter sets both sets. The new --filter-2d NAME[:KEY=V,...] sets the 2D set alone, on top of --filter. A game without scenes refuses --filter-2d (exit 64).
  • Video:
    • video.c holds both sets and reads game_state_scene() once per vsync, before the present, only when the description declares scenes. A game without scenes never calls the probe.
    • A scene change is logged as renderer: gpu: scene 2d (filter smooth), vsync N.
    • The port_video_apply() / port_live_apply() path takes both sets.
  • Present:
    • render_gpu_present.c keeps two slots, both built at start and at live apply.
    • Pipelines are cached per filter kind, so two slots with the same kind share one. Smooth's 640x576 R16 edges texture exists when either slot uses smooth.
    • A scene change only picks which slot the present binds. Nothing is created or freed.
  • Menu hook: int32_t port_video_scene(void) in port_harness.h returns the scene in use, for the in-game menu.
  • Docs: GAME_CONTRACT.md, RUNTIME.md, PORT.md "Rendering", one DECISIONS entry (the scene comes from an explicit probe rather than detection, and both pipelines stay built), and the README feature line.

Why

DW2003 wants smooth on the field's pixel art and something else in battles. The runtime can't detect the scene from what is drawn: the game draws rotated 2D sprites with the GTE, and counting polygons flickers. So the game names its scene through a pull probe, the same way it reports game_state_stage. A switch must not stall a frame, so both pipelines stay built. The scene is read fresh each vsync, so replays, checkpoint hashes and save states don't hold it.

Gates that ran

  • python3 tests/game_gen_test.py, with and without jsonschema (with and without scenes, and the bad shapes)
  • tests/hello_test.py, --menu, --menu --gpu
  • tests/shim_test.py
  • tests/tasks_test.py --sanitize
  • psyq/check.sh --compile
  • python3 -m py_compile on the Python I touched
  • dw2003recomp (origin/main d4ed071, configured with -DPSXSTACK_DIR = this branch, with the disc):
    • With its own game.json (no scenes): tests/port/settings.py pass and tests/port/render_filters.py pass.
    • With a local, uncommitted scenes block and game_state_scene() (3D while the tier-1 overlay is FIGHTSTG), tests/port/run.py passes (first_battle_save, new_game).
    • In the same setup, a scripted switching check on first_battle_save, headless, using --gpu-screenshot @960x720 readbacks:
      • The run with crt for 3D and smooth for 2D gives a field picture (vsync 15000) byte-identical to a smooth-only run's, and a battle picture (vsync 20300) byte-identical to a crt-only run's.
      • --filter crt --filter-2d smooth gives the same pictures.
      • The frame logs are identical across the runs.
      • The switch is logged at vsync 19760.
    • In the same setup, tests/port/settings.py fails exactly 2 exact-dict checks, because --print-settings now writes scene_2d. The round trip passes. Updating those expectations is the game PR's job (worker C).

These did not run here: the Windows cross-build and Wine (CI covers them), and a windowed present on a real display. The window path uses the same draw call as the readback.

Owner play-test

  1. Set the field to smooth and battles to crt (or none). Walk the field, enter a battle, and leave it. The filter should snap at the transition with no hitch.
  2. Open the in-game menu during a battle and on the field, change either set and apply. Both sets should follow live.
  3. With an older settings file that has only video.filter: battles should keep that filter and the field should show smooth.
  4. Run --filter crt alone (both sets crt), then add --filter-2d sharp.

🤖 Generated with Claude Code

gascarcella and others added 3 commits October 26, 2026 14:20
…_scene()

A game may opt in to one present-filter set for its 2D scenes and one for its 3D scenes (dw2003recomp #122). This is
the contract part alone, so the launcher and the game can start from it: game.json's video.scenes (the labels and the
2D set's default filter) in the schema and the generator, which writes PSXSTACK_GAME_SCENES and the labels and
filter as macros; the adapter's new probe game_state_scene() (PORT_SCENE_3D / PORT_SCENE_2D) with its weak default,
3D; the settings' video.scene_2d in settings.c's key list; the first game's copy of its scenes block. Additive, so
PSXSTACK_API stays 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e menu's getter

For a game with video.scenes (dw2003recomp #122): the settings read video.scene_2d member by member over its
default (game.json's 2d.filter with the default crt values, else a copy of the 3D set) and --print-settings writes
it; --filter sets both sets and the new --filter-2d the 2D set alone. video.c holds both sets and reads
game_state_scene() once per vsync, before the present, only when the description declares scenes; a change is
logged and picks the other slot. render_gpu_present.c keeps two slots built (pipelines per kind, so equal kinds share
them; smooth's edges texture when either slot is smooth): a scene change binds another pipeline and creates or frees
nothing. Live apply builds both again. port_video_scene() is the scene in use, for the in-game menu.

A game without scenes has one set: f[1] copies f[0], the probe is never called, scene_2d is an unknown key, the
log line is the old "renderer: gpu: filter NAME".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/RUNTIME.md (video.scene_2d, --filter-2d, the log lines), docs/PORT.md "Rendering" (two slots, both built),
docs/DECISIONS.md (the game names its scene rather than the runtime detecting it, and both pipelines stay built so a
switch builds nothing), README.md's feature line. The scene change's log line gets ", vsync N", so a test can tell
where a scene began.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gascarcella
gascarcella added this pull request to stack #112 October 26, 2026 18:27
@gascarcella
gascarcella merged commit 5b2debf into main Oct 26, 2026
5 checks passed
@gascarcella
gascarcella deleted the gascarcella/dual-filters-runtime branch October 26, 2026 18:28
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