Skip to content
Merged
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ consumer is [dw2003recomp](https://github.com/gascarcella/dw2003recomp), the Dig
- **The hardware renderer** on SDL_GPU (Vulkan; Direct3D 12 on Windows): internal resolution up to 8x, sub-pixel
precision from the GTE's fractions, optional perspective-correct texturing (docs/PORT.md "Rendering",
"Sub-pixel precision").
- **Present filters:** sharp bilinear, scanlines, CRT, xBR (docs/RUNTIME.md "The window").
- **Present filters:** sharp bilinear, scanlines, CRT, xBR (docs/RUNTIME.md "The window"); a game with 2D and 3D scenes
may give each its own (game.json `video.scenes`).
- **Screen fit:** fit, integer or stretch scaling; a wide 16:9 canvas a game's mod can turn on for the scenes that
widen.
- **Texture dumps and texture packs**, keyed by content (docs/RUNTIME.md "Texture dump", "Texture packs").
Expand Down
12 changes: 12 additions & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,15 @@ the gamepad map and never reaches a hotkey: the host's player owns pause, the me
the pad (the window, a script) sets both ports every vsync, since the shim's "connected" is part of a save state. The
runtime's players are arrays of `PORT_PADS` and the setting is `player<n>`, so a multitap can add players without a
new format.

## Two filter sets: the game names its scene, both pipelines stay built (2026-10-26)

A game may want one present filter for its 2D scenes and another for its 3D ones (dw2003recomp #122: smooth for the
field's pixel art, another for the battles). The sets are fixed, `2d` and `3d` (a filter and its crt values;
everything else stays global), opted in by game.json's `video.scenes`. The scene comes from the adapter,
`game_state_scene()`, pulled once per vsync like the other probes (the adapter has no per-vsync hook, and its mods
can be off), not detected by the runtime: a game draws rotated 2D sprites with the GTE, and counting polygons or
textures flickers between frames. A switch must not stall a frame, so both sets' pipelines are built when the
filters are set and a scene change only picks the slot the present binds; the cost is a second filter's shader and
pipelines, and smooth's 640x576 16-bit texture (720 KB) when either set is smooth. The scene is read fresh each
vsync and only chooses the present, so replays, checkpoints and save states do not hold it.
20 changes: 19 additions & 1 deletion docs/GAME_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ the port and the launcher stay single binaries. Contents:
`--input-test` then runs the window's input self-test on the pump alone, without the game.
- **Timing:** `video.rate` (the nominal vsyncs per second: 50 PAL, 60 NTSC), the rates the launcher offers and the
text that explains the alternative.
- **Scenes** (optional): `video.scenes`, `{ "2d": { "label", "filter" }, "3d": { "label" } }`: the game has two present
filter sets, one for its 2D scenes and one for its 3D scenes, and the runtime switches between them at the vsync
where the adapter's `game_state_scene()` changes ("4. The adapter units"). A set is a filter and its crt values;
the sub-pixel mode, the scale, fullscreen, the scaling and the internal scale stay global. `label` names each set in
the launcher's Filter rows (`"the field and the menus"`, `"battles"`); `2d.filter` (optional) is the 2D set's
default filter, with the default crt values (absent: the 2D set starts as a copy of the 3D set). The settings'
`video.filter` and `video.crt` are the 3D set, `video.scene_2d` the 2D set (docs/LAUNCHER.md "Settings file").
The sets are fixed, `2d` and `3d`. Absent: one set, and the probe is never read.
- **Memory:** `memory.ram` (base, size), `memory.slots` (name, base, size, in address order, contiguous), `memory.heap`
(PS1 start and end, the host heap size; optional: a game whose heap is its own data, an array in its `.bss`, has
none, and the arena is then the slots alone), `memory.bios_standin` (addresses the game reads from the BIOS ROM and what
Expand All @@ -84,6 +92,7 @@ launcher and the game's adapter use by these exact names:
| `PsxstackGameBiosStandin` (`address`, `text`), `PSXSTACK_GAME_BIOS_STANDIN_COUNT`, `PSXSTACK_GAME_BIOS_STANDINS` | `memory.bios_standin`; with none the count is 0 and the initializer holds one empty entry |
| `PSXSTACK_GAME_ABOUT`, `PSXSTACK_GAME_DISC_HINT`, `PSXSTACK_GAME_WEBSITE` | `launcher.*`; `about` defaults to the title, the others to `""` |
| `PSXSTACK_GAME_UPDATES_GITHUB` | `launcher.updates.github` (`"owner/repo"`), `""` when absent (no update check) |
| `PSXSTACK_GAME_SCENES`, `PSXSTACK_GAME_SCENE_2D_LABEL`, `PSXSTACK_GAME_SCENE_3D_LABEL`, `PSXSTACK_GAME_SCENE_2D_FILTER` | `video.scenes`: 1 or 0; the labels; the 2D set's default filter by name, `""` when `2d.filter` is absent (a copy of the 3D set); all three strings `""` without scenes |

`--cmake` writes the CMake variables: `PSXSTACK_GAME_ID`, `PSXSTACK_GAME_TITLE`, `PSXSTACK_GAME_ENV_PREFIX`,
`PSXSTACK_GAME_RATE`, and `PSXSTACK_GAME_STEAM_ART_CAPSULE`, `_WIDE_CAPSULE`, `_HERO`, `_LOGO`, `_ICON` (`launcher.steam_art`'s
Expand Down Expand Up @@ -144,6 +153,14 @@ C the game compiles into the port alongside the runtime. It implements:
writes, a script's `write_mem` and `poke_ps1`). The stack asks the adapter first and maps the arena itself for what the adapter does not. A game
whose random index is LIBC2's `rand` state (the PS1's generator, the shim's: `psyq/libc2.c`) returns
`port_rand_seed()` (`hooks.h`) from `game_state_random_index()`.
- **The scene** (a game with `video.scenes`): `int32_t game_state_scene(void)`, `PORT_SCENE_3D` (0) or
`PORT_SCENE_2D` (1): which present-filter set the window shows now. The runtime reads it once per vsync, before the
present, and never for a game without scenes; the switch snaps at that vsync, with nothing built (both sets'
pipelines are kept). It is a probe the runtime pulls, like the others, because the adapter has no per-vsync hook and
its mods can be off. The game decides; the runtime does not detect a scene from what is drawn (a game draws rotated
2D sprites with the GTE, and counting polygons flickers). dw2003: 3D while its battle overlay is in slot 1, 2D
otherwise. Default: `PORT_SCENE_3D`. It reads the game's state and changes none, so replays, checkpoint hashes and
save states do not depend on it. Additive: no `PSXSTACK_API` bump.
- **The mods:** `game_mod_count()` and `game_mods()` return the game's `PortMod` records (`<psxstack/mods.h>`): id,
version, options, `start`/`frame`/`stop` callbacks (`start` a setter the in-game menu may run again, `stop` its undo:
docs/LAUNCHER.md "Mod runtime" rule 6), an optional `status` (text for the window title) and `applies` (`"restart"`
Expand Down Expand Up @@ -206,7 +223,8 @@ the id as a bare token), `_TITLE`, `_ENV_PREFIX`, `_RATE`, `_RATE_COUNT`, `_RATE
`_RAM_SIZE`; `PORT_SLOT_COUNT`, `PORT_SLOT<n>_BASE/SIZE/NAME/OFS`, `port_slot<n>`, `PORT_HEAP_START_ADDR`,
`PORT_HEAP_END_ADDR`, `PORT_HEAP_OFS`, `PORT_HEAP_SIZE`, `PORT_ARENA_SIZE`; `PsxstackGameDisc`,
`PSXSTACK_GAME_DISC_COUNT`, `PSXSTACK_GAME_DISCS`; `PsxstackGameBiosStandin`, `PSXSTACK_GAME_BIOS_STANDIN_COUNT`,
`PSXSTACK_GAME_BIOS_STANDINS`; `PSXSTACK_GAME_ABOUT`, `_DISC_HINT`, `_WEBSITE`, `_UPDATES_GITHUB`. The runtime's
`PSXSTACK_GAME_BIOS_STANDINS`; `PSXSTACK_GAME_ABOUT`, `_DISC_HINT`, `_WEBSITE`, `_UPDATES_GITHUB`;
`PSXSTACK_GAME_SCENES`, `_SCENE_2D_LABEL`, `_SCENE_3D_LABEL`, `_SCENE_2D_FILTER`. The runtime's
environment variables are `<PREFIX>_PORT_<NAME>`.

### 6. Tests
Expand Down
8 changes: 8 additions & 0 deletions docs/PORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,14 @@ against the PS1 or an emulator) are listed in `psyq/README.md` "Behaviour assume
4:3) is smoothed from its canvas in the target read at 1x (every N-th pixel, exact for the 2D layers). The window
and `--gpu-screenshot`'s `@WxH` pictures are filtered; the picture itself (no `@WxH`, the debug channel's) is not.
The software renderer shows the picture unfiltered and logs it.
**Two sets** (a game with `video.scenes`: GAME_CONTRACT.md "1. game.json"): a filter and its parameters for the
2D scenes and another for the 3D scenes, in two slots that are both built at the start and at a live apply
(pipelines per filter kind, so two slots of one kind share them; smooth's edges texture when either slot is
smooth). video.c reads `game_state_scene()` once per vsync before the present and passes the slot to every
present call: the switch snaps at that vsync and creates or frees nothing (DECISIONS "Two filter sets: the game
names its scene, both pipelines stay built"). A game without scenes has one set; the probe is never called. The in-game menu, over
whichever scene, is drawn after the filtered picture, which covers the whole frame; `port_video_scene()` tells it
the set in use.
- **Widescreen** (`runtime/render_gpu_wide.c`, its header comment; DECISIONS "Widescreen: a wide canvas beside the
VRAM"): a game mod asks for it (`port_video_widescreen_enable`, then `port_video_widescreen(on)` every vsync, on for
the scenes whose 3D the game already sends past the display's edges). The target gets a strip of 512 VRAM columns
Expand Down
8 changes: 7 additions & 1 deletion docs/RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,13 @@ default: the picture's pixels as whole blocks), `sharp` (sharp bilinear), `scanl
`crt[:scanlines=,mask=,curvature=]` (a parameter not named keeps `video.crt`'s value; defaults 50, 30, 0) or `smooth`
(xBR on the 1x software image, whatever the internal scale); it applies to
the window and to `--gpu-screenshot`'s `@WxH` pictures, and a software-renderer run logs that its picture stays
unfiltered (docs/PORT.md "Rendering").
unfiltered (docs/PORT.md "Rendering"). A game with 2D and 3D scenes (game.json `video.scenes`, GAME_CONTRACT.md
"1. game.json") has two such sets: `video.filter` and `video.crt` are the 3D scenes', `video.scene_2d` (`{ "filter",
"crt" }`, read member by member over its default: game.json's `2d.filter` with the default crt values, else a copy of
the 3D set) the 2D scenes'. `--filter` sets both, `--filter-2d NAME[:KEY=V,...]` the 2D set alone (over `--filter`;
refused, status 64, for a game without scenes, where `video.scene_2d` is an unknown key). The window shows the set of
the scene the adapter's `game_state_scene()` names, read every vsync; the log says `renderer: gpu: filter crt (3d),
smooth (2d)` once both are built and `renderer: gpu: scene 2d (filter smooth), vsync N` at each change.
**Widescreen** (docs/PORT.md "Rendering"): when a game mod calls `port_video_widescreen_enable` (from its start) and
`port_video_widescreen(1)` (every vsync of the scenes that widen), the hardware renderer presents those scenes 16:9
inside the window, the rest 4:3; the window opens 16:9 when the mod is on and `video.renderer` is `gpu`. With the
Expand Down
4 changes: 3 additions & 1 deletion examples/dw2003.game.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"video": {
"rate": 50,
"rates": [50, 60],
"rate_note": "The game's own 60 Hz mode, as the NTSC patch gives it: time, walking and music keep their speed, other things run 20% faster. A game built before its 60 Hz mode runs at 50 Hz."
"rate_note": "The game's own 60 Hz mode, as the NTSC patch gives it: time, walking and music keep their speed, other things run 20% faster. A game built before its 60 Hz mode runs at 50 Hz.",
"scenes": { "2d": { "label": "the field and the menus", "filter": "smooth" },
"3d": { "label": "battles" } }
},
"memory": {
"ram": { "base": "0x80000000", "size": "0x200000" },
Expand Down
8 changes: 8 additions & 0 deletions include/psxstack/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ int32_t game_state_random_index(void); /* the pad RNG's index (the record's rand
/* The field player's position in pixels: 1 and the values in x and y, or 0 when there is no player (default). */
int game_state_player_pos(double *x, double *y);

/* ---- The scene (a game with `video.scenes`, GAME_CONTRACT.md "1. game.json"): whether the game shows a 3D scene or a
* 2D one now, which picks the present-filter set the window uses. The runtime reads it once per vsync, before the
* present, and only when the description declares scenes (PSXSTACK_GAME_SCENES); the game decides, the runtime does
* not detect. Default: PORT_SCENE_3D (the one set of a game without scenes). */
#define PORT_SCENE_3D 0
#define PORT_SCENE_2D 1
int32_t game_state_scene(void);

/* ---- The checkpoint image: the game-state bytes a checkpoint hashes, as the emulator dumps them from PS1 RAM:
* game_state_image_size() bytes (0 by default: nothing to hash), written by game_state_image (pointers as the PS1
* addresses of the host functions they hold). game_state_volatile: the [lo, hi) byte ranges of the image zeroed for
Expand Down
17 changes: 13 additions & 4 deletions include/psxstack/port_harness.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#define PORT_HARNESS_H

#include <setjmp.h>
#include <stdint.h>
#include <stdio.h>

#include "psxstack/types.h"
Expand Down Expand Up @@ -129,7 +130,15 @@ const char *port_filter_choices(int quoted);
/* `--filter`'s argument into *f: the name, and the parameters it names (KEY=V, V 0..100; the others keep their value);
* 0 with the reason in `err` when it is malformed. */
int port_filter_parse(const char *spec, PortFilter *f, char *err, size_t err_size);
void port_video_set_filter(const PortFilter *f);
/* The two sets, indexed by PORT_SCENE_* (game.h): f[PORT_SCENE_3D] is video.filter and video.crt (`--filter`), the
* only set of a game without video.scenes; f[PORT_SCENE_2D] video.scene_2d (`--filter-2d`), ignored without scenes.
* The present uses the set of the scene game_state_scene() last said (read once per vsync; video.c); port_video_scene
* is that scene (PORT_SCENE_3D for a game without scenes): the in-game menu marks its set. */
void port_video_set_filters(const PortFilter f[2]);
int32_t port_video_scene(void);
/* The 2D set when nothing names it (no video.scene_2d, no --config; settings.c): game.json's video.scenes.2d.filter
* with the default parameters, or a copy of the 3D set `f3d` when the description names none. */
void port_filter_scene_2d_default(const PortFilter *f3d, PortFilter *f2d);
/* The picture's size in the window (`--scaling`, video.scaling; video.c video_dest; video_filter.c the names): fit
* (the default: as large as the window allows at the picture's aspect), integer (as tall as a whole multiple of its
* lines allows: every line the same height, black bars where the window is not such a multiple) or stretch (the whole
Expand All @@ -138,9 +147,9 @@ enum { PORT_SCALING_FIT, PORT_SCALING_INTEGER, PORT_SCALING_STRETCH, PORT_SCALIN
extern const char *const port_scaling_names[PORT_SCALING_COUNT];
int port_scaling_from_name(const char *name); /* PORT_SCALING_*; -1 for another name */
void port_video_set_scaling(int mode);
/* The window's settings changed live (the menu; runtime/live.c): the scale, fullscreen, the scaling, the filter and
* the sub-pixel mode; nothing without a window. */
void port_video_apply(int scale, int fullscreen, int scaling, const PortFilter *filter, int subpixel);
/* The window's settings changed live (the menu; runtime/live.c): the scale, fullscreen, the scaling, the filters (both
* sets, as port_video_set_filters) and the sub-pixel mode; nothing without a window. */
void port_video_apply(int scale, int fullscreen, int scaling, const PortFilter filters[2], int subpixel);
int port_video_gpu_screenshot_add(const char *spec);
/* A run without a window that has --gpu-screenshot: the GPU device and the rasteriser opened now, before the game
* draws (SDL build; logged and skipped without a device). */
Expand Down
4 changes: 4 additions & 0 deletions include/psxstack/settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ typedef struct PortSettings {
* internal scale 1 (2: textured perspective-correct too) */
int filter; /* video.filter: PORT_FILTER_* (default 0, "none"): the hardware renderer's present filter */
int crt_scanlines, crt_mask, crt_curvature; /* video.crt: the filters' parameters, 0..100 (default 50, 30, 0) */
/* video.scene_2d { filter, crt } (a game with video.scenes; the fields above are then the 3D set): the 2D set.
* Absent: port_filter_scene_2d_default's (game.json's 2d.filter with the default crt, or a copy of the 3D set). */
int filter_2d;
int crt_2d_scanlines, crt_2d_mask, crt_2d_curvature;
int mute; /* audio.mute (default 0) */
int watchdog; /* watchdog: seconds, 0 off (default 0: a player's game is never killed by the debugging aid) */
char *memcard[2]; /* memcard1/2, absolute; NULL: no card (null in the file); default card1.mcd/card2.mcd */
Expand Down
4 changes: 4 additions & 0 deletions runtime/game_defaults.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ WEAK int game_state_player_pos(double *x, double *y) {
return 0;
}

WEAK int32_t game_state_scene(void) {
return PORT_SCENE_3D;
}

WEAK uint32_t game_state_image_size(void) {
return 0;
}
Expand Down
19 changes: 12 additions & 7 deletions runtime/live.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* The settings applied live (docs/LAUNCHER.md "In-game menu"): the in-game menu wrote the settings file (through
* the launcher's own code, so it is well formed) and asks the runtime to follow it without a restart. What follows:
* the window's scale, fullscreen and scaling, the hardware renderer's filter and sub-pixel mode (video.c), audio.mute
* the window's scale, fullscreen and scaling, the hardware renderer's filters (both scenes' sets: video.c builds both
* again) and sub-pixel mode (video.c), audio.mute
* (audio.c), the pad map and the hotkeys (input.c: the table starts over, so the port's own actions and every
* enabled mod's bindings are registered again) and the mods (mods.c port_mods_apply). What waits for the next start,
* and the menu says so: the disc, the renderer and its internal scale, the rate, the memory cards, the watchdog, the
Expand All @@ -12,15 +13,19 @@

int port_live_apply(void) {
const PortSettings *s = &port_settings;
PortFilter filter;
PortFilter filters[2]; /* PORT_SCENE_3D's (video.filter, video.crt), PORT_SCENE_2D's (video.scene_2d) */
if (!port_settings_reload()) {
return 0;
}
filter.kind = s->filter;
filter.scanlines = s->crt_scanlines;
filter.mask = s->crt_mask;
filter.curvature = s->crt_curvature;
port_video_apply(s->scale, s->fullscreen, s->scaling, &filter, s->subpixel);
filters[PORT_SCENE_3D].kind = s->filter;
filters[PORT_SCENE_3D].scanlines = s->crt_scanlines;
filters[PORT_SCENE_3D].mask = s->crt_mask;
filters[PORT_SCENE_3D].curvature = s->crt_curvature;
filters[PORT_SCENE_2D].kind = s->filter_2d;
filters[PORT_SCENE_2D].scanlines = s->crt_2d_scanlines;
filters[PORT_SCENE_2D].mask = s->crt_2d_mask;
filters[PORT_SCENE_2D].curvature = s->crt_2d_curvature;
port_video_apply(s->scale, s->fullscreen, s->scaling, filters, s->subpixel);
port_audio_set_user_mute(s->mute);
port_input_settings(s->input);
port_mods_apply(s->mods);
Expand Down
Loading
Loading