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
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,11 @@ jobs:
run: python3 tests/fixtures/steam/make_fixtures.py --check
- name: Self-test (offscreen)
run: SDL_VIDEO_DRIVER=offscreen DW3_SELFTEST_TAR="$(command -v bsdtar)" build/launcher/dw2003-launcher --self-test build/selftest
- name: "hello with the window and the in-game menu: the input self-test offscreen, the menu's picture (tests/hello_test.py --menu)"
run: python3 tests/hello_test.py --menu
# Mesa's lavapipe: a Vulkan device that presents to SDL's offscreen driver, for the menu on the hardware renderer.
- name: lavapipe
run: test -f /usr/share/vulkan/icd.d/lvp_icd.x86_64.json || (sudo apt-get update -q && sudo apt-get install -y -q --no-install-recommends mesa-vulkan-drivers)
- name: "hello with the window and the in-game menu: the input self-test offscreen, the menu's pictures on both renderers (tests/hello_test.py --menu --gpu)"
run: python3 tests/hello_test.py --menu --gpu
- name: "The launcher of a disc-free game with Steam artwork (examples/hello): the pictures are make_art.py's, it builds, its self-test"
run: |
python3 examples/hello/steam/make_art.py --check
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ python3 tests/hello_test.py [--record] # the smoke test: examples/hel
python3 tests/tasks_test.py [--sanitize|--m32] # the fibers: examples/tasks (three tasks, one preempted), its picture, its log twice, a state saved on a task's fiber and resumed
python3 tests/shim_test.py [--sanitize|--m32] # the shim alone (psyq/*.c + tests/shim/shim_test.c): LIBAPI's events and root counter 3, the card's BIOS file calls and LIBCARD, CdSearchFile over a built ISO, CdRead, LIBSPU, LIBSND's SEQs
python3 tests/hello_test.py --exe build/hello-win/hello.exe --wine --gpu # its Windows build (-DPSXSTACK_SDL=ON, cross-built) under Wine; --gpu: the hardware renderer's picture too (D3D12)
python3 tests/hello_test.py --menu # hello with the window and the in-game menu (-DPSXSTACK_MENU=ON): the input self-test offscreen, ending with the menu's round trip, and the menu's pictures (Settings; Mods at 1920x1080, 1280x800, 640x480), none wider than its window
python3 tests/hello_test.py --menu [--gpu] # hello with the window and the in-game menu (-DPSXSTACK_MENU=ON): the input self-test offscreen, ending with the menu's round trip (unmuted live: the device opens), and the menu's pictures (Settings; Mods at 1920x1080, 1280x800, 640x480), none wider than its window; --gpu: Settings on the hardware renderer too (lavapipe), the software one but for edge pixels
cmake -S launcher -B build/launcher -G Ninja [-DPSXSTACK_GAME_JSON=path/to/game.json] && cmake --build build/launcher
SDL_VIDEO_DRIVER=offscreen build/launcher/<id>-launcher --self-test build/selftest # <id> from game.json (dw2003 by default)
cmake -S launcher -B build/launcher-win -G Ninja -DCMAKE_TOOLCHAIN_FILE="$PWD/cmake/windows-x86_64.cmake" -DCMAKE_BUILD_TYPE=Release # the toolchain path must be absolute
Expand Down
53 changes: 28 additions & 25 deletions docs/LAUNCHER.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ and absolute paths and exits 0, or exits 64 naming the bad key.
| `video.filter` | `"none"`, `"sharp"`, `"scanlines"`, `"crt"` or `"smooth"` | `"none"` | The hardware renderer's present filter (docs/PORT.md "Rendering"): `none` shows the picture's pixels as whole blocks, `sharp` is sharp bilinear, `scanlines` sharp with dark lines, `crt` a TV, `smooth` xBR on the 1x picture (the internal scale does not apply to it). The software renderer shows the picture unfiltered and logs it. The launcher's "Filter" combo, shown for the GPU renderer; it writes the member only once it is set |
| `video.crt` | `{ "scanlines", "mask", "curvature" }`, integers 0-100 | 50, 30, 0 | The filters' parameters: `scanlines` (the lines' strength) for `scanlines` and `crt`, `mask` (the aperture grille) and `curvature` for `crt`. The launcher's sliders under the Filter combo; written once set, as `video.filter` |
| `video.menu_size` | `"smaller"`, `"auto"` or `"larger"` | `"auto"` | The in-game menu's size ("In-game menu"): its fit to the window, a step smaller or larger. The Settings screen's "Menu size"; written once set. The menu reads it, the runtime does not (`--print-settings` leaves it out); a game built before it logs the key and ignores it |
| `audio.mute` | bool | false | No audio device |
| `audio.mute` | bool | false | No audio device (switched off in the in-game menu: the device opens then) |
| `memcard1`, `memcard2` | string or `null` | `"card1.mcd"`, `"card2.mcd"` | A `.mcd` image, created formatted when missing; `null`: no card in that slot. An empty string is an error |
| `watchdog` | integer 0-3600 | 0 | Seconds without a vsync before the game exits 4; 0 is off. The bare binary (no `--config`) keeps its 10 s |
| `input` | object | the game's | "Input bindings" |
Expand Down Expand Up @@ -474,23 +474,22 @@ at a time), where leaving the game for the launcher's window is a chore.
submit). The window's events go to ImGui as well as to the runtime's keys (`runtime/input.c`): the menu navigates
with the keyboard, the mouse and every gamepad; the pause, save and load hotkeys do not act while it is up. The
hotkey again, or **Resume**, closes it and the game goes on; **Quit** ends the run with status 0.
- **The pages** (the navigation on the left; the shoulder buttons or Ctrl+PageUp/PageDown step through them):
**Game** (the title, Resume, Quit) and the launcher's own **Settings**, **Controls** and **Mods** screens
- **The pages** (the navigation on the left; the shoulder buttons or Ctrl+PageUp/PageDown step through them): **Game**
(the title, Resume, Quit) and the launcher's own **Settings**, **Controls** and **Mods** screens
(`launcher/ui/screens.cpp`, the one implementation both draw; the update check, "Updates", is the window's own),
over the settings file the game was started with
(`--config`: the runtime's `settings.c` read it; the menu reads it again through the launcher's `SettingsFile` and
writes it as the launcher would, keeping every member it does not edit) and the mods beside the executable plus the
user's data mods (`--mods-dir`). A change is saved when no widget is in use (and when the menu closes), and the
runtime then re-reads the file and applies it (`runtime/live.c` `port_live_apply`): the window's scale and
fullscreen, the hardware renderer's filter and sub-pixel mode, `audio.mute`, the pad map and every hotkey (the
over the settings file the game was started with (`--config`: the runtime's `settings.c` read it; the menu reads it
again through the launcher's `SettingsFile` and writes it as the launcher would, keeping every member it does not
edit) and the mods beside the executable plus the user's data mods (`--mods-dir`). A change is saved when no widget
is in use (and when the menu closes), and the runtime then re-reads the file and applies it (`runtime/live.c`
`port_live_apply`): the window's scale and fullscreen, the hardware renderer's filter and sub-pixel mode,
`audio.mute` (a game started muted opens its audio device when the mute goes off), the pad map and every hotkey (the
table starts over, the enabled mods' bindings registered again) and the mods (`port_mods_apply`: one switched on
gets its hotkeys and its `start`, one switched off its `stop`, one that stays on with other values its `start`
again, since a mod's `start` is a setter: "Mod runtime" rule 6). What waits for the
next start is marked **(restart)** on the page: the disc, the renderer and its internal scale, the rate, the memory
cards, the data mods, and a mod whose manifest says `"applies": "restart"` ("Mod manifest"). A file that does not
read back is logged and changes nothing. A game started without `--config` has the Game page alone; one the
launcher started (`<PREFIX>_LAUNCHER` in its environment) says Quit *to launcher*. The disc stays the launcher's
window's.
again, since a mod's `start` is a setter: "Mod runtime" rule 6). What waits for the next start is marked
**(restart)** on the page: the disc, the renderer and its internal scale, the rate, the memory cards, the data mods,
and a mod whose manifest says `"applies": "restart"` ("Mod manifest"). A file that does not read back is logged and
changes nothing. A game started without `--config` has the Game page alone; one the launcher started
(`<PREFIX>_LAUNCHER` in its environment) says Quit *to launcher*. The disc stays the launcher's window's.
- **The style** (`launcher/ui/style.cpp`, shared with the launcher's window) is scaled to fit the launcher's canvas,
its window at scale 1 (960x620, `launcher/ui/style.h` `CANVAS_W`/`CANVAS_H`), into the game's window, so a page the
launcher fits fits the menu too: about 1.74 at 1920x1080, 1.29 at 1280x800, at least 1. `video.menu_size` steps
Expand All @@ -503,19 +502,23 @@ at a time), where leaving the game for the launcher's window is a chore.
the widget it reaches.
- **The test:** `--input-test` ends with the menu's round trip (its hotkey at a vsync, the menu shown in the pause's
polls, the hotkey again, the next vsync follows the paused one), and `<PREFIX>_PORT_MENU_SHOT=PATH` writes the
window's output with the menu up as a PNG (the software renderer; the hardware renderer's present is not read
back with the menu in it), on the page `<PREFIX>_PORT_MENU_SCREEN=game|settings|controls|mods` names (`mods:<id>`:
with that mod chosen; `controls:keyboard|gamepad|hotkeys`: on that tab), with the window first sized to `<PREFIX>_PORT_MENU_WINDOW=WxH` when it is set (SDL's
software renderer follows it: `SDL_RENDER_DRIVER=software`). The shot's log line names the windows whose content
reaches past their right edge ("nothing wider than its window" when none does).
window's output with the menu up as a PNG, on either renderer (the hardware renderer's swapchain cannot be read
back, so the present that takes it draws the picture and the menu into a texture of the swapchain's format, blits
that into the swapchain and reads it back: `runtime/render_gpu.c`), on the page
`<PREFIX>_PORT_MENU_SCREEN=game|settings|controls|mods` names (`mods:<id>`: with that mod chosen;
`controls:keyboard|gamepad|hotkeys`: on that tab), with the window first sized to `<PREFIX>_PORT_MENU_WINDOW=WxH`
when it is set (SDL's software renderer follows it: `SDL_RENDER_DRIVER=software`). The shot's log line names the
windows whose content reaches past their right edge ("nothing wider than its window" when none does).
The round trip also re-applies the settings live at its second poll, as the menu does after a save, and with
`<PREFIX>_PORT_MENU_LIVE_FILE=PATH` that file is put in place of the settings first (the hotkey pressed next is the
file's binding, so a rebinding must take). `tests/hello_test.py --menu` builds `examples/hello` that way and runs
both on SDL's offscreen driver with a settings file of its own (the live file rebinds the menu hotkey and switches
fast-forward on), the picture its Settings page, then the Mods page's at 1920x1080, 1280x800 and 640x480 and at
1280x800 "larger" (`tests/fixtures/mods/multipliers` chosen, a page larger than any a game ships so far), none of
them wider than its window; CI's `launcher` job runs it and keeps the pictures. The screens' logic is the launcher
self-test's.
both on SDL's offscreen driver with a settings file of its own, muted (the live file rebinds the menu hotkey,
switches fast-forward on and the mute off, so the audio device must open), the picture its Settings page (with
`--gpu` also on the hardware renderer, on Mesa's lavapipe, which presents to the offscreen driver: the software
picture but for the anti-aliased edges, at most 0.1 % of the pixels off by more than 8 levels), then the Mods page's
at 1920x1080, 1280x800 and 640x480 and at 1280x800 "larger" (`tests/fixtures/mods/multipliers` chosen, a page larger
than any a game ships so far), none of them wider than its window; CI's `launcher` job runs it and keeps the
pictures. The screens' logic is the launcher self-test's.
- **The release:** a game that ships the menu links libstdc++ and libgcc statically with
`-DPSXSTACK_MENU_STATIC_RUNTIME=ON`, as the launcher does (`launcher/README.md` "Releases").

Expand Down
5 changes: 3 additions & 2 deletions docs/RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ cmake -S port -B build/port-sdl -G Ninja -DPSXSTACK_SDL=ON -DPSXSTACK_MENU=ON
`menu` hotkey (Escape, or Back+Start on a gamepad), in a pause; docs/LAUNCHER.md "In-game menu". The game then
links as C++ (`launcher/ui/`, Dear ImGui from `tools/imgui`); without the option nothing of it is in the binary. A
change made in the menu is written to the settings file and applied live where the runtime can (`runtime/live.c`).
`<PREFIX>_PORT_MENU_SHOT=PATH` writes the window with the menu up as a PNG (the software renderer), on the page
`<PREFIX>_PORT_MENU_SHOT=PATH` writes the window with the menu up as a PNG (either renderer), on the page
`<PREFIX>_PORT_MENU_SCREEN` names (`mods:<id>` with a mod chosen, `controls:<tab>` on a tab of Controls), at the size `<PREFIX>_PORT_MENU_WINDOW=WxH` gives
the window (with `SDL_RENDER_DRIVER=software`).
SDL's backends follow the `-dev` headers present when `setup.sh sdl3` runs: missing optional ones are turned off one by
Expand Down Expand Up @@ -501,7 +501,8 @@ build/port-sdl/<game> --disc <disc>.cue --window --mute # no device
SDL_VIDEO_DRIVER=offscreen SDL_AUDIO_DRIVER=disk SDL_AUDIO_DISK_OUTPUT_FILE=out.raw build/port-sdl/<game> --disc <disc>.cue --window --max-frames 3000
```
`--wav FILE`: 44,100 Hz, stereo, signed 16-bit little-endian PCM; the header's sizes are written at exit
(`port_exit`), so the file is `44 + vsyncs × 882 × 4` bytes (every vsync of the run, the last one too). The window's device (unless `--mute`) is an SDL3 audio stream fed the
(`port_exit`), so the file is `44 + vsyncs × 882 × 4` bytes (every vsync of the run, the last one too). The window's device (unless `--mute` or `audio.mute`; a game started with
`audio.mute` opens it when the in-game menu switches the mute off) is an SDL3 audio stream fed the
same samples. Its clock and the window's pace (`CLOCK_MONOTONIC`, `pump.c`) drift apart; the queue (frames put and
not yet played, measured before each vsync's put) is held without touching the game's timing or the samples: a moving
average above the band (target ± one vsync, target = the device's period + two vsyncs: 63 ms ± 20 ms at 1,024 frames)
Expand Down
6 changes: 6 additions & 0 deletions include/psxstack/menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ int port_menu_frame(void);
* swapchain texture into the present's command buffer, before its submit. */
void port_menu_render_renderer(struct SDL_Renderer *renderer);
void port_menu_render_gpu(struct SDL_GPUCommandBuffer *cb, struct SDL_GPUTexture *swapchain);
/* The menu's picture from the hardware renderer (<PREFIX>_PORT_MENU_SHOT), whose swapchain cannot be read back:
* port_menu_shot_due says this present is the one to take it, and render_gpu.c then draws the picture and the menu
* into a texture of the swapchain's format, blits that into the swapchain and reads it back; port_menu_shot_save
* takes its pixels (4 bytes a pixel, w * 4 a row, B, G, R in memory when bgra, else R, G, B; the fourth ignored). */
int port_menu_shot_due(void);
void port_menu_shot_save(const void *pixels, int w, int h, int bgra);

#ifdef __cplusplus
}
Expand Down
4 changes: 3 additions & 1 deletion include/psxstack/port_harness.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ void port_audio_pause(int paused); /* the device paused (the pump's pause); noth
/* The device muted (fast-forward): its queue cleared, then the samples rendered but not queued; unmuted, the queue
* starts again from its target. The WAV and the SPU never change. */
void port_audio_set_mute(int mute); /* fast-forward's mute */
void port_audio_set_user_mute(int mute); /* audio.mute changed in the menu (live.c): either source mutes */
/* audio.mute: the settings' at the start (main.c), then as the menu changes it (live.c); either source mutes. A game
* started muted has no device: switched off, the mute opens it. */
void port_audio_set_user_mute(int mute);

/* ---- mods.c: the built-in mods (docs/LAUNCHER.md "Mod manifest", "Mod runtime") ----
* port_mods_settings: the settings' `mods` section (NULL: every mod off) read against the registry; a bad value fails
Expand Down
48 changes: 35 additions & 13 deletions launcher/ui/menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
// (cmake/psxstack.cmake).
//
// <PREFIX>_PORT_MENU_SHOT=PATH writes the window's output with the menu up, once, as a PNG: the second present after
// it is shown (the software renderer: the input self-test's picture; the hardware renderer's present cannot be read
// back with the menu in it, logged). <PREFIX>_PORT_MENU_SCREEN=game|settings|controls|mods picks the page it opens on
// (the picture of a page; default game), `mods:<id>` with that mod chosen, `controls:<tab>` on that tab (keyboard,
// gamepad, hotkeys).
// it is shown, on either present path (the hardware renderer's through a texture render_gpu.c reads back: menu.h
// port_menu_shot_due). <PREFIX>_PORT_MENU_SCREEN=game|settings|controls|mods picks the page it opens on (the picture
// of a page; default game), `mods:<id>` with that mod chosen, `controls:<tab>` on that tab (keyboard, gamepad,
// hotkeys).
#include "menu.h"

#include <algorithm>
Expand Down Expand Up @@ -287,19 +287,29 @@ std::string menu_overflow() {
return names;
}

void menu_shot_renderer(SDL_Renderer *renderer) {
if (mn.shot.empty() || mn.shot_done || mn.drawn < 2) {
return; // the second present: ImGui draws an auto-sized window from its second frame on
}
// The shot is the second present with the menu: ImGui draws an auto-sized window from its second frame on.
bool menu_shot_wanted() {
return !mn.shot.empty() && !mn.shot_done && mn.drawn >= 2;
}

// The window's output with the menu up (nullptr: it could not be read) as the shot's PNG, and the overflow line.
void menu_shot_write(SDL_Surface *s) {
mn.shot_done = true;
SDL_Surface *s = SDL_RenderReadPixels(renderer, nullptr);
if (s == nullptr || !SDL_SavePNG(s, mn.shot.c_str())) {
port_log("menu: shot %s: %s", mn.shot.c_str(), SDL_GetError());
} else {
port_log("menu: shot: %dx%d -> %s", s->w, s->h, mn.shot.c_str());
}
const std::string over = menu_overflow();
port_log("menu: shot: %s", over.empty() ? "nothing wider than its window" : ("wider than its window: " + over).c_str());
}

void menu_shot_renderer(SDL_Renderer *renderer) {
if (!menu_shot_wanted()) {
return;
}
SDL_Surface *s = SDL_RenderReadPixels(renderer, nullptr);
menu_shot_write(s);
SDL_DestroySurface(s);
}

Expand Down Expand Up @@ -333,10 +343,6 @@ void port_menu_open_gpu(SDL_Window *window, SDL_GPUDevice *device) {
info.MSAASamples = SDL_GPU_SAMPLECOUNT_1;
ImGui_ImplSDLGPU3_Init(&info);
port_log("menu: Dear ImGui %s on SDL_GPU", IMGUI_VERSION);
if (!mn.shot.empty()) {
port_log("menu: shot: the software renderer's present only; none from the hardware renderer");
mn.shot_done = true;
}
}

void port_menu_close(void) {
Expand Down Expand Up @@ -435,4 +441,20 @@ void port_menu_render_gpu(SDL_GPUCommandBuffer *cb, SDL_GPUTexture *swapchain) {
mn.drawn++;
}

int port_menu_shot_due(void) {
// the draw this present makes is the one the shot wants (port_menu_render_gpu counts it)
return menu_ready_to_draw() && mn.device != nullptr && !mn.shot.empty() && !mn.shot_done && mn.drawn + 1 >= 2;
}

void port_menu_shot_save(const void *pixels, int w, int h, int bgra) {
if (!menu_shot_wanted()) {
return;
}
SDL_Surface *s = pixels != nullptr ? SDL_CreateSurfaceFrom(w, h, bgra ? SDL_PIXELFORMAT_BGRX32 : SDL_PIXELFORMAT_RGBX32,
const_cast<void *>(pixels), w * 4)
: nullptr;
menu_shot_write(s);
SDL_DestroySurface(s);
}

} // extern "C"
Loading
Loading