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
6 changes: 4 additions & 2 deletions docs/LAUNCHER.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@ and absolute paths and exits 0, or exits 64 naming the bad key.
| `video.renderer` | `"software"` or `"gpu"` | `"software"` | The window's renderer: SDL_Renderer, or the hardware renderer (SDL_GPU; it falls back to software when no device can present). The launcher's "Renderer" choice. A game built before it logs the key and ignores it |
| `video.internal_scale` | integer 1-8 | 1 | The hardware renderer's resolution, N times the PS1's (1: the software picture). The launcher's "Resolution" slider, shown for the GPU renderer |
| `video.subpixel` | `"off"`, `"on"` or `"perspective"` | `"on"` | Above internal scale 1, the hardware renderer draws the 3D at the GTE's sub-pixel positions (`docs/PORT.md` "Sub-pixel precision") instead of the PS1's whole pixels; `"perspective"` also textures it perspective-correct (not the PS1's look: its affine textures swim on large polygons). The launcher's "3D vertices" choice, shown for the GPU renderer above x1. A game built before it logs the key and ignores it |
| `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.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. A game with scenes (GAME_CONTRACT.md "1. game.json" Scenes): the set of its 3D scenes, the row "Filter: <the 3D label>" |
| `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.scene_2d` | `{ "filter", "crt" }`, as `video.filter` and `video.crt` | game.json's `video.scenes.2d.filter` with the default `crt`; without one, `video.filter` and `video.crt` | A game with scenes: the present-filter set of its 2D scenes (`video.filter` and `video.crt` are then its 3D scenes'); the window switches between the two at the vsync where the game's scene changes. A member it lacks is the default's. The launcher's second Filter row, "Filter: <the 2D label>"; it writes the member whole once the set differs from the default, and keeps it once the file has it. A game without scenes logs the key and ignores it (the launcher leaves it as it is) |
| `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 (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 |
Expand Down Expand Up @@ -482,7 +483,8 @@ at a time), where leaving the game for the launcher's window is a chore.
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,
`port_live_apply`): the window's scale and fullscreen, the hardware renderer's filter (both sets, for a game with
scenes: the Settings page marks the one on screen, `port_video_scene()`) 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`
Expand Down
12 changes: 7 additions & 5 deletions launcher/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,16 +197,18 @@ drops `SDL_VIDEO_DRIVER` from the program's environment.)

No disc and no display needed (CI runs it). It replaces `DIR/launcher-self-test/` and checks: the path helpers (Windows
forms too), the JSON writer, the lookup order, the settings file's round trips (the documented example with its unknown
members kept, invalid values, a broken file, a newer schema), the binding grammar, `input.player2`, and the input prompt
members kept, invalid values, a broken file, a newer schema; `video.scene_2d` for a game with scenes, left as it is by
one without), the binding grammar, `input.player2`, and the input prompt
(keys, gamepad buttons, stick directions, chords, Escape), the `.cue` reader and the SHA-1 check (against the described
discs), the launch path with **the launcher itself as the game's stand-in** (`<PREFIX>_LAUNCHER_FAKE_GAME=mode`: a game
with and without `--config`, one that rejects the file, one that crashes, one that fails after 250 lines), then opens
the window, walks every screen with injected key events and a virtual gamepad, plays with the stand-in (a file of the
first described disc's size stands for the verified disc), rebinds a key through the prompt and checks the file, shows
the Gamepad tab with player 2 set and two virtual gamepads connected, drops a wrong file on the window, renders the mods
screen over the fixture manifests (`tests/fixtures/mods/`: the first game's `fast_forward` as shipped, the documented
`skip_dialogues` example, one with every option type, one with presets and a slider whose toggle makes it a typed
number; three unusable ones written at test time) and rebinds a mod's key through the prompt, and saves a picture of
the Gamepad tab with player 2 set and two virtual gamepads connected, the GPU renderer's rows (for a game with scenes
the two Filter rows, the 2D one marked as the in-game menu marks the set on screen), drops a wrong file on the window,
renders the mods screen over the fixture manifests (`tests/fixtures/mods/`: the first game's `fast_forward` as shipped,
the documented `skip_dialogues` example, one with every option type, one with presets and a slider whose toggle makes
it a typed number; three unusable ones written at test time) and rebinds a mod's key through the prompt, and saves a picture of
each screen in `DIR/launcher-self-test/screens/`. The update check never touches the network:
`<PREFIX>_LAUNCHER_FAKE_CURL` points at **the launcher itself as curl's stand-in** (it answers with a file, in pieces
for a download, or with curl's errors; `<PREFIX>_SELFTEST_CURL_REPLY` says which), and every check of the self-test goes
Expand Down
6 changes: 6 additions & 0 deletions launcher/src/brand.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ constexpr int GAME_RATE = PSXSTACK_GAME_RATE;
constexpr int GAME_RATE_COUNT = PSXSTACK_GAME_RATE_COUNT;
inline constexpr int GAME_RATES[PSXSTACK_GAME_RATE_COUNT] = PSXSTACK_GAME_RATES;
constexpr const char *GAME_RATE_NOTE = PSXSTACK_GAME_RATE_NOTE;
// The scenes (the description's video.scenes): a present-filter set for the 2D scenes and one for the 3D scenes,
// named by their labels in the Filter rows; the 2D set's default filter ("" = a copy of the 3D set's).
constexpr bool GAME_SCENES = PSXSTACK_GAME_SCENES != 0;
constexpr const char *GAME_SCENE_2D_LABEL = PSXSTACK_GAME_SCENE_2D_LABEL;
constexpr const char *GAME_SCENE_3D_LABEL = PSXSTACK_GAME_SCENE_3D_LABEL;
constexpr const char *GAME_SCENE_2D_FILTER = PSXSTACK_GAME_SCENE_2D_FILTER;
// The discs the game accepts (disc.h: disc_by_sha1, disc_known, disc_labels). A disc-free game (examples/hello)
// describes none: the generated list then holds one empty entry, which the count keeps out of reach.
constexpr int GAME_DISC_COUNT = PSXSTACK_GAME_DISC_COUNT;
Expand Down
151 changes: 118 additions & 33 deletions launcher/src/selftest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -302,23 +302,23 @@ static void test_settings_file(const std::string &root) {
write(path_join(filtered, SETTINGS_FILE), R"({"schema":1,"video":{"renderer":"gpu","filter":"sharp"}})");
SettingsFile fl;
fl.load(filtered);
check(fl.messages().empty() && fl.values.filter == "sharp", "video.filter is read");
fl.values.filter = "none";
check(fl.messages().empty() && fl.values.present.filter == "sharp", "video.filter is read");
fl.values.present.filter = "none";
check(fl.save(&err), "saving the filter: " + err);
const Json written = Json::parse(read(fl.path()), nullptr);
const Json *saved = written.find("video") != nullptr ? written.find("video")->find("filter") : nullptr;
check(saved != nullptr && saved->as_string() == "none", "a filter the file had is written back, \"none\" too");
write(path_join(filtered, SETTINGS_FILE), R"({"schema":1,"video":{"filter":"blur"}})");
fl.load(filtered);
check(fl.messages().size() == 1 && fl.values.filter == "none", "a bad filter: a warning, \"none\"");
check(fl.messages().size() == 1 && fl.values.present.filter == "none", "a bad filter: a warning, \"none\"");
// video.crt (the scanlines and crt filters' parameters): read, written once set, bad values warned about.
write(path_join(filtered, SETTINGS_FILE),
R"({"schema":1,"video":{"filter":"crt","crt":{"scanlines":80,"mask":10,"curvature":25}}})");
fl.load(filtered);
check(fl.messages().empty() && fl.values.filter == "crt" && fl.values.crt_scanlines == 80 &&
fl.values.crt_mask == 10 && fl.values.crt_curvature == 25,
check(fl.messages().empty() && fl.values.present.filter == "crt" && fl.values.present.scanlines == 80 &&
fl.values.present.mask == 10 && fl.values.present.curvature == 25,
"video.crt is read");
fl.values.crt_mask = 60;
fl.values.present.mask = 60;
check(fl.save(&err), "saving video.crt: " + err);
const Json crt_written = Json::parse(read(fl.path()), nullptr);
const Json *crt = crt_written.find("video") != nullptr ? crt_written.find("video")->find("crt") : nullptr;
Expand All @@ -327,8 +327,80 @@ static void test_settings_file(const std::string &root) {
"video.crt is written back");
write(path_join(filtered, SETTINGS_FILE), R"({"schema":1,"video":{"crt":{"scanlines":101,"mask":-1}}})");
fl.load(filtered);
check(fl.messages().size() == 2 && fl.values.crt_scanlines == 50 && fl.values.crt_mask == 30,
check(fl.messages().size() == 2 && fl.values.present.scanlines == 50 && fl.values.present.mask == 30,
"bad video.crt values: a warning each, the defaults");
// video.scene_2d (the 2D scenes' set, a game with scenes): absent, what the game's 2d.filter or the 3D set means,
// and not added; read member by member over that; written whole once it differs, and kept once the file has it;
// bad members warned about. A game without scenes leaves it as it is, unread.
{
const std::string sc = path_join(root, "scene_2d");
path_make_dir(sc, nullptr);
auto scene_2d = [&](const SettingsFile &f) {
const Json doc = Json::parse(read(f.path()), nullptr);
const Json *video = doc.find("video");
const Json *m = video != nullptr ? video->find("scene_2d") : nullptr;
return m != nullptr ? m->dump() : std::string();
};
write(path_join(sc, SETTINGS_FILE), R"({"schema":1,"video":{"filter":"crt","crt":{"scanlines":70}}})");
SettingsFile f;
f.load(sc);
FilterSet want;
if (GAME_SCENE_2D_FILTER[0] != '\0') {
want.filter = GAME_SCENE_2D_FILTER;
} else {
want.filter = "crt";
want.scanlines = 70;
}
check(f.messages().empty() && f.values.present.filter == "crt" && f.values.present_2d == want,
"a file without video.scene_2d: the 2D set is the game's 2d.filter, else the 3D set");
f.values.mute = true;
check(f.save(&err) && scene_2d(f).empty(), "a file that never chose a 2D set gets no video.scene_2d");
if (GAME_SCENES) {
f.values.present_2d.filter = "scanlines";
f.values.present_2d.scanlines = 90;
check(f.save(&err), "saving video.scene_2d: " + err);
SettingsFile g;
g.load(sc);
check(g.messages().empty() && g.values.present_2d == f.values.present_2d &&
g.values.present == f.values.present,
"video.scene_2d is written and read back, the 3D set as it was");
const Json whole = Json::parse(scene_2d(g), nullptr);
const Json *crt2 = whole.find("crt");
check(crt2 != nullptr && crt2->find("mask") != nullptr && crt2->find("curvature") != nullptr,
"video.scene_2d is written whole: " + scene_2d(g));
const std::string kept = scene_2d(g);
g.values.mute = false;
check(g.save(&err) && scene_2d(g) == kept, "an untouched video.scene_2d stays as it was");
g.values.present_2d = Settings::scene_2d_default(g.values.present);
check(g.save(&err) && !scene_2d(g).empty(),
"a video.scene_2d the file had is written back, the default too");
write(path_join(sc, SETTINGS_FILE),
R"({"schema":1,"video":{"filter":"sharp","scene_2d":{"filter":"crt"}}})");
g.load(sc);
FilterSet partial = Settings::scene_2d_default(g.values.present);
partial.filter = "crt";
check(g.messages().empty() && g.values.present_2d == partial,
"a partial video.scene_2d: its missing members are what an absent one means");
write(path_join(sc, SETTINGS_FILE),
R"({"schema":1,"video":{"scene_2d":{"filter":"blur","crt":{"mask":101}}}})");
g.load(sc);
check(g.messages().size() == 2 && g.values.present_2d == Settings::scene_2d_default(g.values.present),
"bad video.scene_2d members: a warning each, the defaults");
write(path_join(sc, SETTINGS_FILE), R"({"schema":1,"video":{"scene_2d":"crt"}})");
g.load(sc);
check(g.messages().size() == 1 && g.values.present_2d == Settings::scene_2d_default(g.values.present),
"a video.scene_2d that is not an object: a warning, the default");
} else {
write(path_join(sc, SETTINGS_FILE), R"({"schema":1,"video":{"scene_2d":{"filter":"blur"}}})");
SettingsFile g;
g.load(sc);
const std::string kept = scene_2d(g);
g.values.mute = true;
g.values.present.filter = "sharp";
check(g.messages().empty() && g.save(&err) && scene_2d(g) == kept,
"a game without scenes neither reads nor writes video.scene_2d");
}
}

// Not JSON: the defaults; the first save keeps the old file aside.
const std::string broken = path_join(root, "broken");
Expand Down Expand Up @@ -1187,6 +1259,32 @@ static void drop_file(SDL_Window *w, const std::string &path) {
}

// The play path with the game's stand-in and a verified disc (a file of the BIN's size and the right disc.sha1).
// The mouse over the page, its wheel turned down: the Settings screen scrolled to its end (the Updates part), or by
// `notches` of the wheel.
static void scroll_page_down(SDL_Window *w, App &app, float notches = 40) {
int width = 0, height = 0;
SDL_GetWindowSize(w, &width, &height);
SDL_Event e;
SDL_zero(e);
e.type = SDL_EVENT_MOUSE_MOTION;
e.motion.windowID = SDL_GetWindowID(w);
e.motion.x = (float)width * 0.6f;
e.motion.y = (float)height * 0.5f;
SDL_PushEvent(&e);
pump(app, 2);
for (int i = 0; notches > 0 && i < 4; i++) {
SDL_zero(e);
e.type = SDL_EVENT_MOUSE_WHEEL;
e.wheel.windowID = SDL_GetWindowID(w);
e.wheel.y = -std::min(notches, 10.0f);
notches -= 10;
e.wheel.mouse_x = (float)width * 0.6f;
e.wheel.mouse_y = (float)height * 0.5f;
SDL_PushEvent(&e);
pump(app, 2);
}
}

static void test_play(SDL_Window *window, const std::string &root) {
const std::string dir = path_join(root, "play"), shots = path_join(root, "screens");
if (GAME_DISC_COUNT == 0) {
Expand Down Expand Up @@ -1344,11 +1442,22 @@ static void test_play(SDL_Window *window, const std::string &root) {
png = path_join(shots, "9b-Settings-gpu.png");
app.frame(png.c_str());
// The CRT filter shows its sliders (a picture).
app.settings().values.filter = "crt";
app.settings().values.present.filter = "crt";
pump(app, 2);
png = path_join(shots, "9c-Settings-crt.png");
app.frame(png.c_str());
app.settings().values.filter = "none";
// A game with scenes: the Filter row for its 3D scenes and the one for its 2D scenes, the latter marked as the one
// on screen as the in-game menu marks it (a picture; the launcher marks none).
if (GAME_SCENES) {
app.settings().values.present_2d.filter = "scanlines";
app.screens().set_scene(Screens::SCENE_2D);
scroll_page_down(window, app, 4);
png = path_join(shots, "9d-Settings-scenes.png");
app.frame(png.c_str());
app.screens().set_scene(Screens::SCENE_NONE);
app.settings().values.present_2d = Settings::scene_2d_default(app.settings().values.present);
}
app.settings().values.present.filter = "none";
app.settings().values.renderer = "software";
app.settings().values.internal_scale = 1;
pump(app, 2);
Expand Down Expand Up @@ -1448,30 +1557,6 @@ static void test_mods_window(SDL_Window *window, const std::string &root) {
// ---- the update check (docs/LAUNCHER.md "Updates"): the version rules, GitHub's answer, curl through its stand-in,
// the prompt and the Settings screen. Never the network: the stand-in answers.

// The mouse over the page, its wheel turned down: the Settings screen scrolled to its end (the Updates part).
static void scroll_page_down(SDL_Window *w, App &app) {
int width = 0, height = 0;
SDL_GetWindowSize(w, &width, &height);
SDL_Event e;
SDL_zero(e);
e.type = SDL_EVENT_MOUSE_MOTION;
e.motion.windowID = SDL_GetWindowID(w);
e.motion.x = (float)width * 0.6f;
e.motion.y = (float)height * 0.5f;
SDL_PushEvent(&e);
pump(app, 2);
for (int i = 0; i < 4; i++) {
SDL_zero(e);
e.type = SDL_EVENT_MOUSE_WHEEL;
e.wheel.windowID = SDL_GetWindowID(w);
e.wheel.y = -10.0f;
e.wheel.mouse_x = (float)width * 0.6f;
e.wheel.mouse_y = (float)height * 0.5f;
SDL_PushEvent(&e);
pump(app, 2);
}
}

static void set_env(const char *name, const std::string &value) {
SDL_SetEnvironmentVariable(SDL_GetEnvironment(), name, value.c_str(), true);
}
Expand Down
Loading
Loading