runtime: keep Vulkan opt-in and remove old launcher
Remove the in-tree launcher implementation and its submodules now that game projects use recomp-ui for launcher support. Keep Vulkan hidden from the launcher unless a game explicitly sets [video] offer_vulkan = true, and clamp stale Vulkan settings back to OpenGL when it is not offered. Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
@@ -1,6 +0,0 @@
|
||||
[submodule "lib/RmlUi"]
|
||||
path = lib/RmlUi
|
||||
url = https://github.com/mikke89/RmlUi.git
|
||||
[submodule "lib/freetype"]
|
||||
path = lib/freetype
|
||||
url = https://github.com/freetype/freetype.git
|
||||
@@ -213,7 +213,7 @@ Findings (tracked for the next work cycle):
|
||||
(launcher.cpp:347/776); PSX_ENABLE_VULKAN defaults OFF (runtime.cmake:484);
|
||||
runtime downgrades renderer=vulkan -> opengl when not compiled (main.cpp:2463).
|
||||
Vulkan stays a dev/CLI-only backend (--renderer vulkan on a VK-enabled build).
|
||||
- Widescreen carries an EXPERIMENTAL tag in the RmlUi launcher (launcher.rml:362).
|
||||
- Widescreen carries an EXPERIMENTAL tag in the launcher.
|
||||
|
||||
**MERGED to master 2026-07-03** (runtime-only; codegen hash unchanged, no regen).
|
||||
Game pins bumped to the new psxrecomp master. No release builds cut (user directive).
|
||||
|
||||
@@ -521,15 +521,8 @@ discovery seeds). Either way, then close Issue #3 alongside.
|
||||
|
||||
### Symptom
|
||||
|
||||
The launcher dashboard art (disc / controllers / memory cards / logo)
|
||||
is cropped out of the design mockup and background-knocked-out to
|
||||
transparent by `tools/crop_launcher_assets.ps1`. The **memory-card and
|
||||
controller cutouts look rough / jaggy around the edges** — a hard
|
||||
luminance threshold in the edge flood-fill leaves a 1–2px aliased
|
||||
fringe (and the anti-aliased boundary pixels that sit just above the
|
||||
threshold are kept, so the silhouette is stair-stepped). The disc reads
|
||||
cleaner because its silver rim contrasts more strongly with the dark
|
||||
background.
|
||||
This issue tracked cosmetic problems in the removed in-tree launcher asset
|
||||
pipeline. It is retained only as historical context.
|
||||
|
||||
### Cause
|
||||
|
||||
@@ -554,11 +547,8 @@ fringe on them.
|
||||
|
||||
### Notes
|
||||
|
||||
Tooling: `tools/crop_launcher_assets.ps1` (crop + knockout),
|
||||
`tools/gen_launcher_assets.ps1` (procedural check/verdict icons),
|
||||
`tools/shot_launcher.ps1` (screenshot the GL launcher window). Mockup
|
||||
source: a local mockup PNG (1448×1086). Pure-cosmetic; does not block
|
||||
Phase 4/5 wiring.
|
||||
Tooling: this historical note referred to the removed in-tree launcher asset
|
||||
pipeline. The active launcher path is downstream `recomp-ui`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -273,8 +273,7 @@ prerequisites in [`docs/BUILDING.md`](docs/BUILDING.md)):
|
||||
|
||||
- A C/C++ toolchain: MSVC or MinGW/MSYS2 (Windows), Apple Clang (macOS),
|
||||
Clang/GCC (Linux). CMake 3.20+; on macOS/Linux also `ninja` and `pkg-config`.
|
||||
- SDL2 (system / bundled). RmlUi and FreeType come in as **git submodules** —
|
||||
clone with `--recurse-submodules`.
|
||||
- SDL2 (system / bundled).
|
||||
- A legally obtained `SCPH1001.BIN` BIOS dump. Not included.
|
||||
- For game projects, a legally obtained game disc/EXE dump. Not included.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ overlay DLLs uniformly — and extended well past what emulators ship with
|
||||
| Commit | What |
|
||||
|---|---|
|
||||
| `f8a29e8` | `[video] aspect_ratio = "W:H"` → GTE X-squash (`gte_set_display_aspect`, `runtime/src/gte.cpp`) + aspect-aware letterbox in both renderers + window opens at aspect. Config plumbed through `game.toml`, `settings.toml` (load+save), launcher seed (`recompiler/src/config_loader.{h,cpp}`, `runtime/src/main.cpp`, `runtime/src/gpu_gl_renderer.c`). |
|
||||
| `970670b` | Launcher **Aspect ratio** setting: 4:3 (Native) / 16:9 (Widescreen) / 21:9 (Ultrawide). One build does all. (`runtime/launcher/launcher.cpp`, `assets/launcher.rml`) |
|
||||
| `970670b` | Launcher **Aspect ratio** setting: 4:3 (Native) / 16:9 (Widescreen) / 21:9 (Ultrawide). One build does all. |
|
||||
| `cad8c96` | **Per-prim proportion correction.** Recompiler emits `psx_ws_sprite_tag(cpu)` at the entry of each `[widescreen] sprite_tag_funcs` function; runtime records prim ptr (`$a0`) → GTE-projected anchor and re-squashes tagged prims around their own anchor at GP0 execution, so characters/billboards are NOT stretched. Untagged textured rects (SPRT = screen-space HUD/menus) center-squash. (`recompiler/src/code_generator.cpp`, `runtime/src/gpu.c`) |
|
||||
| `0792086` | **FMV 4:3 pillarbox** (24-bit OR streamed 15-bit MDEC video, detected via `mdec_recently_active`), **menu-2D coherence** (lines/flat-rects/mono-sprites squash on full-2D screens so dialog borders match the SPRT boxes; fades exempt), **HUD edge anchoring** (in-game untagged SPRTs pivot by thirds — outer-third elements keep their wide-screen corner position at native proportions). |
|
||||
| `5151e5b` | **Authentic 4:3 BIOS boot** — squash held off until game entry PC fires (`fntrace_is_game_started`); Sony/PS logos + shell render 4:3, widescreen engages at game start. |
|
||||
@@ -213,7 +213,7 @@ title. Documented for Tomba; any new title needs the same investigation.
|
||||
- `recompiler/src/code_generator.cpp` — `psx_ws_sprite_tag` emit at tagged fn
|
||||
entry. **This is where the cull-widen emit (issue #1) belongs.**
|
||||
- `recompiler/src/config_loader.{h,cpp}` — `[widescreen]` parse, `aspect_ratio`.
|
||||
- `runtime/launcher/launcher.cpp` + `assets/launcher.rml` — aspect toggle.
|
||||
- Launcher aspect control — aspect toggle.
|
||||
- `TombaRecomp/tools/capture_game_window.ps1` — window-capture helper for
|
||||
visual verification (the VRAM `screenshot_file` shows the pre-stretch frame;
|
||||
this shows the post-stretch window).
|
||||
|
||||
@@ -26,8 +26,6 @@ legally-obtained **disc image**. This project ships none of those.
|
||||
| Dependency | How it's provided | Used for |
|
||||
|---|---|---|
|
||||
| **SDL2** | System. Windows/MSVC: prebuilt pack auto-found at `../sdl2-msvc/SDL2-*`. Windows/MinGW + macOS + Linux: found via pkg-config. | Window, input, GL/Vulkan context, audio, threads |
|
||||
| **RmlUi** | **git submodule** `lib/RmlUi` (static) | The launcher UI |
|
||||
| **FreeType** | **git submodule** `lib/freetype` (static) | RmlUi's font engine |
|
||||
| **fmt** 9.1.0 | vendored `recompiler/lib/fmt` | String formatting (runtime uses it header-only) |
|
||||
| **toml11** | vendored `recompiler/lib/toml11` | Parsing `game.toml` / configs |
|
||||
| **ELFIO** | vendored `recompiler/lib/ELFIO` | ELF parsing (recompiler only) |
|
||||
@@ -40,14 +38,12 @@ legally-obtained **disc image**. This project ships none of those.
|
||||
Developers building overlays locally just need `gcc` on `PATH` (the `gcc` tier);
|
||||
the bundled `tcc` matters only for end-user release packages.
|
||||
|
||||
### Get the source (with submodules)
|
||||
### Get the source
|
||||
|
||||
RmlUi and FreeType are submodules, so clone recursively (or init after the fact):
|
||||
This repository has no required git submodules:
|
||||
|
||||
```sh
|
||||
git clone --recurse-submodules https://github.com/mstan/psxrecomp.git
|
||||
# or, in an existing clone:
|
||||
git submodule update --init --recursive
|
||||
git clone https://github.com/mstan/psxrecomp.git
|
||||
```
|
||||
|
||||
## Per-platform prerequisites
|
||||
@@ -102,7 +98,7 @@ On Windows with MSVC or plain MinGW makefiles, swap `-G Ninja` for your generato
|
||||
|---|---|---|
|
||||
| `PSX_DEBUG_TOOLS` | ON for Debug/RelWithDebInfo, OFF for Release | TCP debug server + heartbeat + per-block recording |
|
||||
| `PSX_STATIC_RUNTIME` | ON for MinGW Release | Self-contained exe (statically links SDL2 + libgcc/libstdc++) |
|
||||
| `PSX_LAUNCHER` | ON | Build the RmlUi launcher UI |
|
||||
| `PSX_LAUNCHER` | ON | Allow downstream game projects to wire recomp-ui launcher support |
|
||||
| `PSX_ENABLE_VULKAN` | OFF | Build the experimental Vulkan renderer |
|
||||
| `PSX_BUILD_COSIM` | OFF | Build the first-divergence co-sim oracle target |
|
||||
|
||||
|
||||
@@ -434,7 +434,7 @@ the top of `psx_dispatch_impl` **after** the BIOS-HLE hook, gated by a lock-free
|
||||
of the existing renderer/aspect/FMV/turbo keys; `settings.toml` overrides
|
||||
`game.toml`, CLI overrides both — per the project's settings precedence).
|
||||
`"jp"`/`"off"` = passthrough (no translation).
|
||||
- **RmlUi launcher:** a "Language" dropdown populated from the table's `langs`
|
||||
- **Launcher:** a "Language" dropdown populated from the table's `langs`
|
||||
list; writes the choice to `game_options.toml`. Runtime switch = reselect →
|
||||
the module reloads the active language column (no restart needed; hot-reload
|
||||
already covers table edits).
|
||||
|
||||
@@ -186,6 +186,21 @@ turbo_audio_sink = true
|
||||
the guest SPU timeline advancing but discards accelerated samples before host
|
||||
playback, then fades normal output back in.
|
||||
|
||||
## Video Block
|
||||
|
||||
Runtime video defaults live in `[video]`:
|
||||
|
||||
```toml
|
||||
[video]
|
||||
renderer = "opengl" # "software", "opengl", or "vulkan"
|
||||
offer_vulkan = false # show Vulkan in the launcher only after game validation
|
||||
```
|
||||
|
||||
`renderer = "vulkan"` remains an experimental runtime choice and still requires
|
||||
a build compiled with Vulkan support. `offer_vulkan` controls launcher
|
||||
visibility only; it defaults to false so game projects must explicitly expose
|
||||
Vulkan after validating their visuals and stability.
|
||||
|
||||
Reserved future fields:
|
||||
- `default_disc_path` — game runtimes can pre-mount a disc
|
||||
- `default_game_root` — for sibling-junction setups
|
||||
|
||||
@@ -76,10 +76,9 @@ and Crash Bash diagnostics.
|
||||
|
||||
### Keep under review
|
||||
|
||||
- The self-contained stb launcher removes RmlUi and adds persisted UI scaling.
|
||||
It remains blocked by the missing GL loader and visual/layout review, as the PR
|
||||
itself records. Continue on the existing branch only after those prerequisites
|
||||
are explicit; do not use its runtime changes to justify merging the launcher.
|
||||
- The older in-tree launcher replacement remains blocked by missing GL loader
|
||||
and visual/layout review prerequisites. Do not use its runtime changes to
|
||||
justify merging launcher code.
|
||||
- `a39ab37` contains Linux shared-library overlay discovery, ABI preflight, and
|
||||
cache loading work. Re-derive the smallest useful parts on top of master's
|
||||
newer exact overlay dispatch (`b39387e`) and test save/load plus stale-cache
|
||||
|
||||
@@ -311,6 +311,9 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
else throw std::runtime_error(fmt::format(
|
||||
"[video] renderer must be \"software\", \"opengl\" or \"vulkan\": {}", mode));
|
||||
}
|
||||
if (video.contains("offer_vulkan")) {
|
||||
rt.video_offer_vulkan = toml::find<bool>(video, "offer_vulkan");
|
||||
}
|
||||
if (video.contains("crt_filter")) {
|
||||
const auto mode = toml::find<std::string>(video, "crt_filter");
|
||||
if (mode == "raw") rt.video_screen_kind = 0;
|
||||
@@ -885,7 +888,13 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
bool ws_nw_full_mirror = false;
|
||||
std::vector<WidescreenSignedBoundSite> ws_signed_x_bound_sites;
|
||||
bool ws_offered = true;
|
||||
bool vulkan_offered = false;
|
||||
bool ws_ultrawide_offered = false;
|
||||
if (cfg.contains("video")) {
|
||||
const toml::value& video = toml::find(cfg, "video");
|
||||
if (video.contains("offer_vulkan"))
|
||||
vulkan_offered = toml::find<bool>(video, "offer_vulkan");
|
||||
}
|
||||
// Optional [data_shards] block — memoized pure-function replay hooks.
|
||||
std::vector<uint32_t> data_shard_funcs;
|
||||
if (cfg.contains("data_shards")) {
|
||||
@@ -1257,6 +1266,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
/*ws_nw_full_mirror*/ ws_nw_full_mirror,
|
||||
/*ws_signed_x_bound_sites*/ ws_signed_x_bound_sites,
|
||||
/*ws_offered*/ ws_offered,
|
||||
/*vulkan_offered*/ vulkan_offered,
|
||||
/*ws_ultrawide_offered*/ ws_ultrawide_offered,
|
||||
/*ws_bg2d_count_site*/ ws_bg2d_count_site,
|
||||
/*ws_bg2d_startcol_site*/ ws_bg2d_startcol_site,
|
||||
|
||||
@@ -230,12 +230,16 @@ struct RuntimeConfig {
|
||||
// (smooths textures and 2D backgrounds). Stored as 0/1.
|
||||
int video_texture_filter = 0;
|
||||
|
||||
// renderer: "software" | "opengl" (default). Selects the rasterizer/present
|
||||
// backend. The OpenGL backend is a hardware-accelerated alternative; the
|
||||
// software rasterizer remains the explicit fallback. Stored as
|
||||
// VIDEO_RENDERER_*.
|
||||
// renderer: "software" | "opengl" (default) | "vulkan". Selects the
|
||||
// rasterizer/present backend. The software rasterizer remains the explicit
|
||||
// fallback. Stored as VIDEO_RENDERER_*.
|
||||
int video_renderer = DEFAULT_VIDEO_RENDERER;
|
||||
|
||||
// offer_vulkan: expose the experimental Vulkan renderer in the launcher.
|
||||
// Defaults false even for Vulkan-enabled builds; developers must opt in per
|
||||
// game once visuals are validated.
|
||||
bool video_offer_vulkan = false;
|
||||
|
||||
// low_latency_input: re-sample the pad after the wall-clock pacer (just
|
||||
// before present) so the next CPU frame reads near-fresh input instead of
|
||||
// input ~one frame stale. Default on. vsync: present/swap mode —
|
||||
@@ -687,6 +691,7 @@ struct GameConfig {
|
||||
// 16:9 in settings.toml can never engage the hack. Runtime-only (read at
|
||||
// startup; no codegen impact) — no regen required.
|
||||
bool ws_offered = true;
|
||||
bool vulkan_offered = false;
|
||||
|
||||
// [widescreen] offer_ultrawide — expose a separate experimental 21:9
|
||||
// launcher choice for titles that have explicitly tested it. Default off;
|
||||
|
||||
|
Before Width: | Height: | Size: 330 B |
|
Before Width: | Height: | Size: 789 B |
|
Before Width: | Height: | Size: 787 B |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,473 +0,0 @@
|
||||
<rml>
|
||||
<head>
|
||||
<title>PSX Launcher</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: LatoLatin;
|
||||
color: #e6e9ef;
|
||||
background-color: #0b0e14;
|
||||
font-size: 15dp;
|
||||
}
|
||||
#root { width: 100%; height: 100%; display: block; overflow-y: auto; }
|
||||
#frame { display: block; padding: 22dp 26dp; }
|
||||
|
||||
/* ---- top bar ---- */
|
||||
.topbar { display: flex; flex-direction: row; align-items: center; margin-bottom: 18dp; }
|
||||
.logo { width: 46dp; height: 46dp; margin-right: 16dp; decorator: image(img/logo.png contain); }
|
||||
.title-block { display: block; flex: 1 1 auto; }
|
||||
.topbar-actions { display: flex; flex-direction: row; align-items: center; }
|
||||
.top-btn {
|
||||
background-color: #1b2330; border: 1dp #2b3543; border-radius: 7dp;
|
||||
color: #e6e9ef; padding: 9dp 18dp; font-size: 15dp; font-weight: bold;
|
||||
}
|
||||
.top-btn:hover { background-color: #232d3c; border-color: #3b82f6; }
|
||||
.top-btn.primary { background-color: #1e2733; }
|
||||
h1 { display: block; font-size: 27dp; font-weight: bold; color: #ffffff; }
|
||||
.subtitle { display: block; color: #7d8590; font-size: 14dp; }
|
||||
|
||||
/* ---- generic panels / cards ---- */
|
||||
.panel {
|
||||
display: block;
|
||||
background-color: #11161f;
|
||||
border: 1dp #1e2733;
|
||||
border-radius: 12dp;
|
||||
padding: 18dp 22dp;
|
||||
margin-bottom: 13dp;
|
||||
}
|
||||
/* Settings view: two side-by-side columns so the panels use the horizontal
|
||||
space and the view fits without scrolling. SYSTEM stays full-width below
|
||||
(its BIOS/disc paths need the room). */
|
||||
.settings-cols { display: flex; flex-direction: row; align-items: flex-start; }
|
||||
.settings-col { display: block; flex: 1 1 0; min-width: 0; }
|
||||
.settings-col:first-child { margin-right: 13dp; }
|
||||
.cards-row { display: flex; flex-direction: row; align-items: stretch; margin-bottom: 13dp; }
|
||||
.card {
|
||||
display: block;
|
||||
/* flex-basis 0 + min-width 0 so the two cards always split the row exactly
|
||||
* 50/50 regardless of their content. (flex-basis:auto sized each card to its
|
||||
* content, so selecting a controller widened that card.) */
|
||||
flex: 1 1 0dp;
|
||||
min-width: 0dp;
|
||||
background-color: #11161f;
|
||||
border: 1dp #1e2733;
|
||||
border-radius: 12dp;
|
||||
padding: 18dp 20dp;
|
||||
}
|
||||
.card.left { margin-right: 8dp; }
|
||||
.card.right { margin-left: 8dp; }
|
||||
.eyebrow { display: block; color: #4493f8; font-size: 12dp; font-weight: bold; margin-bottom: 8dp; }
|
||||
|
||||
/* ---- disc verification panel ---- */
|
||||
.disc-panel { display: flex; flex-direction: row; align-items: flex-start; position: relative; }
|
||||
.disc-art { width: 216dp; height: 216dp; margin-right: 24dp; decorator: image(img/disc.png contain); flex: 0 0 216dp; }
|
||||
.disc-info { display: block; flex: 1 1 auto; }
|
||||
.disc-title { display: block; font-size: 32dp; font-weight: bold; color: #ffffff; }
|
||||
.disc-sub { display: block; color: #9aa4b2; font-size: 16dp; margin-bottom: 16dp; }
|
||||
.meta-row { display: flex; flex-direction: row; margin-bottom: 16dp; }
|
||||
.meta { display: block; margin-right: 40dp; }
|
||||
.meta-k { display: block; color: #6e7781; font-size: 11dp; font-weight: bold; margin-bottom: 3dp; }
|
||||
.meta-v { display: block; color: #e6e9ef; font-size: 15dp; }
|
||||
.results { display: block; border-top: 1dp #1e2733; padding-top: 12dp; }
|
||||
.results-title { display: block; color: #4493f8; font-size: 11dp; font-weight: bold; margin-bottom: 8dp; }
|
||||
.checks { display: flex; flex-direction: row; }
|
||||
.check { display: flex; flex-direction: row; align-items: center; margin-right: 28dp; }
|
||||
.check-icon { width: 18dp; height: 18dp; margin-right: 7dp; decorator: image(img/check_off.png); }
|
||||
.check-icon.on { decorator: image(img/check_on.png); }
|
||||
.check-lbl { color: #c2c9d2; font-size: 14dp; }
|
||||
|
||||
/* verdict column on the right of the disc panel */
|
||||
.disc-verdict { display: flex; flex-direction: row; align-items: center; flex: 0 0 290dp;
|
||||
margin-left: 24dp; border-left: 1dp #1e2733; padding-left: 24dp; }
|
||||
.verdict-icon { width: 50dp; height: 50dp; margin-right: 14dp; }
|
||||
.verdict-icon.ok { decorator: image(img/verdict_ok.png); }
|
||||
.verdict-icon.warn { decorator: image(img/verdict_warn.png); }
|
||||
.verdict-icon.bad { decorator: image(img/verdict_bad.png); }
|
||||
.verdict-icon.none { decorator: image(img/verdict_none.png); }
|
||||
.verdict-text { display: block; }
|
||||
.verdict-title { display: block; font-size: 18dp; font-weight: bold; color: #ffffff; }
|
||||
.verdict-detail { display: block; color: #8b949e; font-size: 13dp; }
|
||||
.change-iso { position: absolute; top: 20dp; right: 22dp; }
|
||||
|
||||
button {
|
||||
background-color: #1b2330;
|
||||
border: 1dp #2b3543;
|
||||
border-radius: 7dp;
|
||||
color: #e6e9ef;
|
||||
padding: 8dp 14dp;
|
||||
}
|
||||
button:hover { background-color: #232d3c; }
|
||||
|
||||
/* ---- player / memcard card chrome ---- */
|
||||
.card-head { display: flex; flex-direction: row; align-items: center; margin-bottom: 14dp; }
|
||||
.card-icon { width: 18dp; height: 18dp; margin-right: 8dp; }
|
||||
.card-name { color: #cdd5df; font-size: 12dp; font-weight: bold; flex: 1 1 auto; }
|
||||
.enable-lbl { color: #8b949e; font-size: 13dp; margin-right: 8dp; }
|
||||
.switch { width: 42dp; height: 22dp; border-radius: 11dp; background-color: #2b3543; display: block; }
|
||||
.switch.on { background-color: #2563eb; }
|
||||
.knob { display: block; width: 16dp; height: 16dp; border-radius: 8dp; background-color: #e6e9ef; margin: 3dp 0dp 0dp 3dp; }
|
||||
.switch.on .knob { margin-left: 23dp; }
|
||||
|
||||
/* Pad-mode 3-way segmented selector (Hybrid | Analog | D-Pad). */
|
||||
.seg { display: flex; flex-direction: row; align-items: center; position: relative;
|
||||
background-color: #161d28; border: 1dp #2b3543; border-radius: 7dp; }
|
||||
.seg-opt { display: block; padding: 5dp 11dp; color: #8b949e; font-size: 12dp; border-radius: 6dp; }
|
||||
.seg-opt:hover { color: #cdd5df; }
|
||||
.seg-opt.on { background-color: #2563eb; color: #ffffff; }
|
||||
/* Hybrid carries the explanatory tooltip, revealed on hover. */
|
||||
.seg-tip { display: none; position: absolute; top: 34dp; right: 0dp; width: 250dp; z-index: 80;
|
||||
background-color: #0d1219; border: 1dp #3b82f6; border-radius: 7dp;
|
||||
padding: 10dp 12dp; color: #cdd5df; font-size: 11dp; line-height: 16dp; }
|
||||
.seg-opt.hybrid:hover .seg-tip { display: block; }
|
||||
|
||||
.card-body { display: flex; flex-direction: row; align-items: center; }
|
||||
.card-body.disabled { opacity: 0.4; }
|
||||
/* Player card body is a fixed height so toggling the pad type never reflows. */
|
||||
.card-body.player { height: 134dp; }
|
||||
.pad-art { width: 206dp; height: 128dp; margin-right: 14dp; flex: 0 0 206dp; }
|
||||
.pad-art.digital { decorator: image(img/pad_digital.png contain); }
|
||||
.pad-art.analog { decorator: image(img/pad_analog.png contain); }
|
||||
.card-col { display: block; flex: 1 1 auto; }
|
||||
.field-lbl { display: block; color: #8b949e; font-size: 12dp; margin-bottom: 5dp; }
|
||||
.dropdown-wrap { display: block; position: relative; margin-bottom: 12dp; }
|
||||
.dropdown {
|
||||
display: flex; flex-direction: row; align-items: center;
|
||||
background-color: #161d28; border: 1dp #2b3543; border-radius: 7dp;
|
||||
padding: 9dp 12dp; color: #e6e9ef;
|
||||
}
|
||||
.dropdown:hover { border-color: #3b82f6; }
|
||||
.dd-label { flex: 1 1 auto; white-space: nowrap; overflow: hidden; }
|
||||
.caret { width: 13dp; height: 13dp; margin-left: 8dp; flex: 0 0 13dp;
|
||||
decorator: image(img/caret.png contain); }
|
||||
.dd-panel {
|
||||
position: absolute; left: 0dp; top: 44dp; width: 100%; z-index: 50;
|
||||
background-color: #0d131c; border: 1dp #2b3543; border-radius: 7dp; padding: 4dp;
|
||||
}
|
||||
.dd-opt { display: block; padding: 8dp 10dp; border-radius: 5dp; color: #c2c9d2; }
|
||||
.dd-opt:hover { background-color: #1b2330; color: #ffffff; }
|
||||
.status-row { display: flex; flex-direction: row; align-items: center; }
|
||||
.dot { width: 9dp; height: 9dp; border-radius: 5dp; background-color: #3fb950; margin-right: 7dp; flex: 0 0 9dp; }
|
||||
.dot.off { background-color: #6e7781; }
|
||||
.status-txt { color: #9aa4b2; font-size: 13dp; flex: 1 1 auto;
|
||||
white-space: nowrap; overflow: hidden; min-width: 0dp; }
|
||||
|
||||
/* memcard */
|
||||
.mc-art { width: 118dp; height: 132dp; margin-right: 16dp; flex: 0 0 118dp; decorator: image(img/memcard.png contain); }
|
||||
.mc-name { display: block; font-size: 20dp; font-weight: bold; color: #ffffff; margin-bottom: 10dp; }
|
||||
.mc-btns { display: flex; flex-direction: row; margin-bottom: 12dp; }
|
||||
.mc-btns button { margin-right: 8dp; }
|
||||
.mc-stats { display: flex; flex-direction: row; margin-bottom: 10dp; }
|
||||
.mc-stat { display: block; margin-right: 30dp; }
|
||||
.blocks { display: flex; flex-direction: row; }
|
||||
.blk { width: 13dp; height: 16dp; border: 1dp #2b3543; border-radius: 2dp; margin-right: 2dp; }
|
||||
.blk.b { background-color: #2563eb; border-color: #2563eb; }
|
||||
.blk.p { background-color: #8957e5; border-color: #8957e5; }
|
||||
.mc-foot { display: block; color: #6e7781; font-size: 12dp; margin-top: 8dp; }
|
||||
|
||||
/* ---- settings view ---- */
|
||||
.set-row { display: flex; flex-direction: row; align-items: center; height: 34dp; margin-bottom: 8dp; }
|
||||
.set-lbl { width: 230dp; flex: 0 0 230dp; color: #adbac7; }
|
||||
.set-ctl { flex: 1 1 auto; display: flex; flex-direction: row; align-items: center; }
|
||||
.toggle { width: 132dp; text-align: center; }
|
||||
.toggle.wide { width: 164dp; }
|
||||
.value { color: #ffffff; font-weight: bold; }
|
||||
.exp-tag { display: inline-block; font-size: 10dp; color: #d29922; background-color: #2a2410;
|
||||
border: 1dp #5c4a12; border-radius: 4dp; padding: 0dp 5dp; margin-left: 8dp; font-weight: bold; }
|
||||
.ws-hint { color: #7d8590; font-size: 12dp; margin-left: 12dp; }
|
||||
.path { flex: 1 1 auto; min-width: 0dp; margin-left: 12dp; color: #7d8590;
|
||||
white-space: nowrap; overflow: hidden; }
|
||||
.path.empty { color: #f85149; }
|
||||
|
||||
/* ---- controls / keybind rebind view ---- */
|
||||
.kb-head { display: flex; flex-direction: row; align-items: center; margin-bottom: 16dp; }
|
||||
.kb-player-lbl { color: #adbac7; margin-right: 10dp; }
|
||||
.kb-hint { color: #7d8590; font-size: 12dp; margin-left: 18dp; flex: 1 1 auto;
|
||||
white-space: nowrap; overflow: hidden; }
|
||||
.kb-reset { background-color: #1b2330; border: 1dp #2b3543; border-radius: 7dp;
|
||||
color: #e6e9ef; padding: 7dp 14dp; margin-left: 12dp; }
|
||||
.kb-reset:hover { border-color: #3b82f6; }
|
||||
.rebind-list { display: block; }
|
||||
.rb-row { display: flex; flex-direction: row; align-items: center; margin-bottom: 8dp; }
|
||||
.rb-label { width: 130dp; flex: 0 0 130dp; color: #adbac7; font-size: 13dp; }
|
||||
.rb-chip { width: 150dp; flex: 0 0 150dp; text-align: center; margin-right: 34dp;
|
||||
background-color: #161d28; border: 1dp #2b3543; border-radius: 7dp;
|
||||
color: #e6e9ef; padding: 7dp 10dp; }
|
||||
.rb-chip:hover { border-color: #3b82f6; }
|
||||
.rb-chip--scan { border-color: #f0b429; color: #f0b429; }
|
||||
|
||||
/* ---- footer ---- */
|
||||
.footer { display: flex; flex-direction: row; align-items: center; margin-top: 6dp;
|
||||
border-top: 1dp #1e2733; padding-top: 16dp; }
|
||||
.foot-link { color: #8b949e; margin-right: 22dp; }
|
||||
.foot-link:hover { color: #e6e9ef; }
|
||||
.skip-lbl { color: #adbac7; font-size: 13dp; margin-left: 9dp; margin-right: 22dp; }
|
||||
.foot-status { display: flex; flex-direction: row; align-items: center; flex: 1 1 auto; }
|
||||
#launch {
|
||||
background-color: #2563eb; border-color: #3b82f6; color: #ffffff; font-weight: bold;
|
||||
font-size: 16dp; padding: 12dp 34dp;
|
||||
}
|
||||
#launch:hover { background-color: #3b82f6; }
|
||||
.back-btn { margin-right: 14dp; }
|
||||
|
||||
/* ---- confirmation modal ---- */
|
||||
.modal-backdrop { position: fixed; left: 0dp; top: 0dp; width: 100%; height: 100%;
|
||||
background-color: #000000cc; display: flex; flex-direction: row; align-items: center; z-index: 100; }
|
||||
.modal-box { display: block; width: 460dp; margin: 0dp auto;
|
||||
background-color: #11161f; border: 1dp #2b3543; border-radius: 12dp; padding: 22dp 24dp; }
|
||||
.modal-title { display: block; font-size: 19dp; font-weight: bold; color: #ffffff; margin-bottom: 10dp; }
|
||||
.modal-text { display: block; color: #adbac7; font-size: 14dp; line-height: 20dp; margin-bottom: 20dp; }
|
||||
.modal-btns { display: flex; flex-direction: row; align-items: center; }
|
||||
.modal-spacer { flex: 1 1 auto; }
|
||||
.modal-btns button { margin-left: 10dp; }
|
||||
.modal-confirm { background-color: #2563eb; border-color: #3b82f6; color: #ffffff; font-weight: bold; }
|
||||
.modal-confirm:hover { background-color: #3b82f6; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body id="root" data-model="settings">
|
||||
<div id="frame">
|
||||
|
||||
<div class="topbar">
|
||||
<div class="logo"></div>
|
||||
<div class="title-block">
|
||||
<h1>{{game_name}} Recomp</h1>
|
||||
<div class="subtitle">{{game_name}} launcher</div>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<button class="top-btn" data-if="view == 'dashboard'" data-event-click="show_controls">Controls</button>
|
||||
<button class="top-btn" data-if="view == 'dashboard'" data-event-click="show_settings">Settings</button>
|
||||
<button class="top-btn primary" data-if="view == 'settings'" data-event-click="show_dashboard">‹ Back to dashboard</button>
|
||||
<button class="top-btn primary" data-if="view == 'controls'" data-event-click="show_dashboard">‹ Back to dashboard</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= DASHBOARD VIEW ================= -->
|
||||
<div data-if="view == 'dashboard'">
|
||||
|
||||
<div class="panel disc-panel">
|
||||
<div class="disc-art"></div>
|
||||
<div class="disc-info">
|
||||
<div class="eyebrow">DISC VERIFICATION</div>
|
||||
<div class="disc-title">{{game_name}}</div>
|
||||
<div class="disc-sub">PlayStation disc image</div>
|
||||
<div class="meta-row">
|
||||
<div class="meta"><div class="meta-k">FILE</div><div class="meta-v">{{disc_file}}</div></div>
|
||||
<div class="meta"><div class="meta-k">REGION</div><div class="meta-v">{{disc_region}}</div></div>
|
||||
<div class="meta"><div class="meta-k">SERIAL</div><div class="meta-v">{{disc_serial}}</div></div>
|
||||
</div>
|
||||
<div class="results">
|
||||
<div class="results-title">VERIFICATION RESULTS</div>
|
||||
<div class="checks">
|
||||
<span class="check"><span class="check-icon" data-class-on="v_header"></span><span class="check-lbl">Header Match</span></span>
|
||||
<span class="check"><span class="check-icon" data-class-on="v_crc"></span><span class="check-lbl">CRC / Hash Match</span></span>
|
||||
<span class="check"><span class="check-icon" data-class-on="v_verified"></span><span class="check-lbl">Disc Verified</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="change-iso" data-event-click="change_iso">Change ISO</button>
|
||||
</div>
|
||||
|
||||
<div class="cards-row" data-if="device_locked == false">
|
||||
<div class="card left">
|
||||
<div class="card-head">
|
||||
<span class="card-name">PLAYER 1</span>
|
||||
<div class="seg" data-if="mode_selectable">
|
||||
<span class="seg-opt hybrid" data-if="allow_hybrid" data-class-on="p1_mode == 0" data-event-click="set_mode_p1(0)">Hybrid<span class="seg-tip">Nudge the analog stick for smooth analog movement, or use the D-pad for classic digital control. It switches automatically based on what you touch.</span></span>
|
||||
<span class="seg-opt" data-class-on="p1_mode == 1" data-event-click="set_mode_p1(1)">Analog</span>
|
||||
<span class="seg-opt" data-class-on="p1_mode == 2" data-event-click="set_mode_p1(2)">D-Pad</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body player">
|
||||
<div class="pad-art" data-class-analog="p1_mode != 2" data-class-digital="p1_mode == 2"></div>
|
||||
<div class="card-col">
|
||||
<div class="field-lbl">Device</div>
|
||||
<div class="dropdown-wrap">
|
||||
<div class="dropdown" data-event-click="open_dd(0)">
|
||||
<span class="dd-label">{{p1_dev_label}}</span><span class="caret"></span>
|
||||
</div>
|
||||
<div class="dd-panel" data-if="dd_open == 'p1'" data-rml="p1_options"></div>
|
||||
</div>
|
||||
<div class="status-row"><span class="dot" data-class-off="p1_dot == 'off'"></span>
|
||||
<span class="status-txt">{{p1_status}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card right">
|
||||
<div class="card-head">
|
||||
<span class="card-name">PLAYER 2</span>
|
||||
<div class="seg" data-if="mode_selectable">
|
||||
<span class="seg-opt hybrid" data-if="allow_hybrid" data-class-on="p2_mode == 0" data-event-click="set_mode_p2(0)">Hybrid<span class="seg-tip">Nudge the analog stick for smooth analog movement, or use the D-pad for classic digital control. It switches automatically based on what you touch.</span></span>
|
||||
<span class="seg-opt" data-class-on="p2_mode == 1" data-event-click="set_mode_p2(1)">Analog</span>
|
||||
<span class="seg-opt" data-class-on="p2_mode == 2" data-event-click="set_mode_p2(2)">D-Pad</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body player">
|
||||
<div class="pad-art" data-class-analog="p2_mode != 2" data-class-digital="p2_mode == 2"></div>
|
||||
<div class="card-col">
|
||||
<div class="field-lbl">Device</div>
|
||||
<div class="dropdown-wrap">
|
||||
<div class="dropdown" data-event-click="open_dd(1)">
|
||||
<span class="dd-label">{{p2_dev_label}}</span><span class="caret"></span>
|
||||
</div>
|
||||
<div class="dd-panel" data-if="dd_open == 'p2'" data-rml="p2_options"></div>
|
||||
</div>
|
||||
<div class="status-row"><span class="dot" data-class-off="p2_dot == 'off'"></span>
|
||||
<span class="status-txt">{{p2_status}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cards-row">
|
||||
<div class="card left">
|
||||
<div class="card-head">
|
||||
<span class="card-name">MEMORY CARD 1</span>
|
||||
<span class="enable-lbl">Enabled</span>
|
||||
<span class="switch" data-class-on="mc1_enabled" data-event-click="toggle_mc1"><span class="knob"></span></span>
|
||||
</div>
|
||||
<div class="card-body" data-class-disabled="mc1_enabled == false">
|
||||
<div class="mc-art"></div>
|
||||
<div class="card-col">
|
||||
<div class="mc-name">{{mc1_name}}</div>
|
||||
<div class="mc-btns"><button data-event-click="browse_mc1">Browse</button><button data-event-click="new_mc1">New</button></div>
|
||||
<div class="mc-stats">
|
||||
<div class="mc-stat"><div class="field-lbl">Card Size</div><div class="meta-v">{{mc1_size}}</div></div>
|
||||
<div class="mc-stat"><div class="field-lbl">Used Blocks</div><div class="meta-v">{{mc1_used}}</div></div>
|
||||
</div>
|
||||
<div class="blocks" data-rml="mc1_grid"></div>
|
||||
<div class="mc-foot">{{mc1_foot}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card right">
|
||||
<div class="card-head">
|
||||
<span class="card-name">MEMORY CARD 2</span>
|
||||
<span class="enable-lbl">Enabled</span>
|
||||
<span class="switch" data-class-on="mc2_enabled" data-event-click="toggle_mc2"><span class="knob"></span></span>
|
||||
</div>
|
||||
<div class="card-body" data-class-disabled="mc2_enabled == false">
|
||||
<div class="mc-art"></div>
|
||||
<div class="card-col">
|
||||
<div class="mc-name">{{mc2_name}}</div>
|
||||
<div class="mc-btns"><button data-event-click="browse_mc2">Browse</button><button data-event-click="new_mc2">New</button></div>
|
||||
<div class="mc-stats">
|
||||
<div class="mc-stat"><div class="field-lbl">Card Size</div><div class="meta-v">{{mc2_size}}</div></div>
|
||||
<div class="mc-stat"><div class="field-lbl">Used Blocks</div><div class="meta-v">{{mc2_used}}</div></div>
|
||||
</div>
|
||||
<div class="blocks" data-rml="mc2_grid"></div>
|
||||
<div class="mc-foot">{{mc2_foot}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= SETTINGS VIEW ================= -->
|
||||
<div data-if="view == 'settings'">
|
||||
<div class="settings-cols">
|
||||
<div class="settings-col">
|
||||
<div class="panel">
|
||||
<div class="eyebrow">VIDEO</div>
|
||||
<div class="set-row"><span class="set-lbl">Window size</span><span class="set-ctl">
|
||||
<button class="toggle wide" data-event-click="cycle_winsize"><span class="value">{{winsize_label}}</span></button></span></div>
|
||||
<div class="set-row"><span class="set-lbl">Renderer</span><span class="set-ctl">
|
||||
<button class="toggle" data-event-click="cycle_renderer"><span class="value">{{renderer_label}}</span></button></span></div>
|
||||
<div class="set-row" data-if="opengl_renderer"><span class="set-lbl">Frame interpolation</span><span class="set-ctl">
|
||||
<button class="toggle" data-event-click="toggle_frame_interpolation"><span class="value" data-if="frame_interpolation">On</span><span class="value" data-if="frame_interpolation == false">Off</span></button></span></div>
|
||||
<div class="set-row" data-if="interpolation_target_visible"><span class="set-lbl">Presentation target</span><span class="set-ctl">
|
||||
<button class="toggle wide" data-event-click="cycle_interpolation_fps"><span class="value">{{interpolation_fps_label}}</span></button>
|
||||
<span class="ws-hint">Gameplay remains at its original speed</span></span></div>
|
||||
<div class="set-row"><span class="set-lbl">Supersampling</span><span class="set-ctl">
|
||||
<button class="toggle" data-event-click="cycle_ss"><span class="value">{{supersampling}}x</span></button></span></div>
|
||||
<div class="set-row"><span class="set-lbl">Antialiasing</span><span class="set-ctl">
|
||||
<button class="toggle" data-event-click="toggle_aa"><span class="value" data-if="antialiasing">On</span><span class="value" data-if="antialiasing == false">Off</span></button></span></div>
|
||||
<div class="set-row"><span class="set-lbl">Texture filtering</span><span class="set-ctl">
|
||||
<button class="toggle" data-event-click="cycle_texfilter"><span class="value">{{texfilter_label}}</span></button></span></div>
|
||||
<div class="set-row"><span class="set-lbl">Screen model</span><span class="set-ctl">
|
||||
<button class="toggle" data-event-click="cycle_crt"><span class="value">{{crt_label}}</span></button></span></div>
|
||||
<div class="set-row"><span class="set-lbl">Skip FMVs</span><span class="set-ctl">
|
||||
<button class="toggle" data-event-click="toggle_skip_fmv"><span class="value" data-if="auto_skip_fmv">On</span><span class="value" data-if="auto_skip_fmv == false">Off</span></button></span></div>
|
||||
<div class="set-row"><span class="set-lbl">Turbo loads</span><span class="set-ctl">
|
||||
<button class="toggle" data-event-click="toggle_turbo_loads"><span class="value" data-if="turbo_loads">On</span><span class="value" data-if="turbo_loads == false">Off</span></button>
|
||||
</span></div>
|
||||
<div class="set-row"><span class="set-lbl">Fullscreen</span><span class="set-ctl">
|
||||
<button class="toggle" data-event-click="cycle_fullscreen"><span class="value">{{fullscreen_label}}</span></button></span></div>
|
||||
<div class="set-row" data-if="ws_eligible"><span class="set-lbl">Widescreen <span class="exp-tag">EXPERIMENTAL</span></span><span class="set-ctl">
|
||||
<button class="toggle" data-event-click="toggle_widescreen"><span class="value" data-if="widescreen">On</span><span class="value" data-if="widescreen == false">Off</span></button>
|
||||
<span class="ws-hint" data-if="widescreen">16:9 native field-of-view</span></span></div>
|
||||
<div class="setting-row" data-if="uw_eligible"><span class="setting-name">Ultrawide <span class="exp">EXPERIMENTAL</span></span><span class="setting-control">
|
||||
<button class="toggle" data-event-click="toggle_ultrawide"><span class="value" data-if="ultrawide">On</span><span class="value" data-if="ultrawide == false">Off</span></button>
|
||||
<span class="ws-hint" data-if="ultrawide">21:9 native field-of-view</span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-col">
|
||||
<div class="panel" data-if="lang_menu">
|
||||
<div class="eyebrow">LOCALIZATION</div>
|
||||
<div class="set-row"><span class="set-lbl">Language</span><span class="set-ctl">
|
||||
<button class="toggle wide" data-event-click="cycle_language"><span class="value">{{lang_label}}</span></button></span></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="eyebrow">AUDIO</div>
|
||||
<div class="set-row"><span class="set-lbl">SPU high-quality (float shadow)</span><span class="set-ctl">
|
||||
<button class="toggle" data-event-click="toggle_spu"><span class="value" data-if="spu_hq">On</span><span class="value" data-if="spu_hq == false">Off</span></button></span></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="eyebrow">CONTROLLER</div>
|
||||
<div class="set-row"><span class="set-lbl">Analog stick deadzone</span><span class="set-ctl">
|
||||
<button class="toggle" data-event-click="cycle_deadzone"><span class="value">{{deadzone_pct}}%</span></button></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="eyebrow">SYSTEM</div>
|
||||
<div class="set-row"><span class="set-lbl">BIOS</span><span class="set-ctl">
|
||||
<button data-event-click="browse_bios">Browse</button>
|
||||
<span class="path" data-if="bios_path != ''">{{bios_path}}</span>
|
||||
<span class="path empty" data-if="bios_path == ''">(not set)</span></span></div>
|
||||
<div class="set-row"><span class="set-lbl">Disc</span><span class="set-ctl">
|
||||
<button data-event-click="browse_disc">Browse</button>
|
||||
<span class="path" data-if="disc_path != ''">{{disc_path}}</span>
|
||||
<span class="path empty" data-if="disc_path == ''">(not set)</span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= CONTROLS (KEYBOARD KEYBINDS) VIEW ================= -->
|
||||
<div data-if="view == 'controls'">
|
||||
<div class="panel">
|
||||
<div class="eyebrow">KEYBOARD CONTROLS</div>
|
||||
<div class="kb-head">
|
||||
<span class="kb-player-lbl">Player</span>
|
||||
<div class="seg">
|
||||
<span class="seg-opt" data-class-on="cfg_player == 0" data-event-click="cfg_player_1">1</span>
|
||||
<span class="seg-opt" data-class-on="cfg_player == 1" data-event-click="cfg_player_2">2</span>
|
||||
</div>
|
||||
<span class="kb-hint">Click a key, then press the new key to bind it. Esc cancels. Changes save instantly.</span>
|
||||
<button class="kb-reset" data-event-click="rebind_reset">Reset to defaults</button>
|
||||
</div>
|
||||
<div id="rebind-list" class="rebind-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= FOOTER ================= -->
|
||||
<div class="footer">
|
||||
<span class="switch" data-class-on="skip_launcher" data-event-click="toggle_skip_launcher"><span class="knob"></span></span>
|
||||
<span class="skip-lbl">Skip Launcher on Boot</span>
|
||||
<span class="foot-status"><span class="dot"></span><span class="status-txt">Recompiler ready</span></span>
|
||||
<button id="launch" data-event-click="launch">LAUNCH GAME</button>
|
||||
</div>
|
||||
|
||||
<!-- ================= SKIP-LAUNCHER CONFIRM MODAL ================= -->
|
||||
<div class="modal-backdrop" data-if="show_skip_modal">
|
||||
<div class="modal-box">
|
||||
<span class="modal-title">Skip the launcher on boot?</span>
|
||||
<span class="modal-text">The launcher will no longer appear — the game boots straight in every time. To get the launcher back, run the game with the --launcher argument, or set skip_launcher = false in settings.toml.</span>
|
||||
<div class="modal-btns">
|
||||
<span class="modal-spacer"></span>
|
||||
<button data-event-click="skip_modal_cancel">Cancel</button>
|
||||
<button class="modal-confirm" data-event-click="skip_modal_confirm">Skip on Boot</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</rml>
|
||||
@@ -1,63 +0,0 @@
|
||||
// launcher.h — integrated RmlUi launcher front-end.
|
||||
//
|
||||
// Shown in the runtime's SDL/OpenGL window before the emulator boots: the user
|
||||
// picks renderer / supersampling / AA / colour model / SPU-HQ, the BIOS, disc,
|
||||
// memory cards and controllers, then presses LAUNCH. The chosen values are
|
||||
// written back into the UserSettings the runtime then applies (and persisted to
|
||||
// settings.toml by the caller).
|
||||
//
|
||||
// Design note — the launcher does NOT create or own the window or GL context;
|
||||
// the caller passes an already-current GL 3.3 context. This keeps the module a
|
||||
// pure overlay so a future "re-open settings while the game is running" path can
|
||||
// reuse it without owning the window lifecycle.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace PSXRecompV4 { struct UserSettings; }
|
||||
|
||||
namespace psx_launcher {
|
||||
|
||||
enum class Result {
|
||||
Launch, // user pressed LAUNCH — proceed to boot with `io`
|
||||
Quit, // user closed the window — caller should exit
|
||||
Unavailable, // launcher could not initialise (assets/GL); caller boots as if skipped
|
||||
};
|
||||
|
||||
// Static facts about the game the launcher is configuring. Drives the title and
|
||||
// the disc-verification badge. Extends naturally for later phases.
|
||||
struct GameInfo {
|
||||
const char* name = nullptr; // display name, e.g. "Tomba!"
|
||||
const char* expected_serial = nullptr; // game id "SCUS-94236" (null = no serial check)
|
||||
uint32_t expected_crc = 0; // full-file CRC32 of the data track
|
||||
bool has_expected_crc = false; // whether expected_crc is meaningful
|
||||
bool allow_hybrid = true; // offer the "Hybrid" pad mode (false => Analog | D-Pad only)
|
||||
bool lock_mode = false; // hide the whole pad-mode selector and force locked_mode (single-pad-type games)
|
||||
int locked_mode = 2; // PAD_MODE_DIGITAL; the mode forced when lock_mode is true
|
||||
bool lock_device = false; // hide the Player 1/2 controller cards entirely (fixed, auto-bound pad type; e.g. Ape Escape DualShock)
|
||||
bool ws_offered = true; // offer the EXPERIMENTAL Widescreen toggle (false = hidden, game ships 4:3 only)
|
||||
bool ws_ultrawide_offered = false; // separately offer experimental 21:9
|
||||
|
||||
// Optional "Localization" dropdown. Populated from game.toml
|
||||
// [localization].languages. When non-empty the launcher shows a language
|
||||
// menu; empty => no menu (the general case — only Tsumu declares languages).
|
||||
// code feeds the runtime's translation layer ("off"/"jp"/"" = untranslated).
|
||||
struct Language { std::string code; std::string label; };
|
||||
std::vector<Language> languages;
|
||||
};
|
||||
|
||||
// Run the launcher loop to completion. `gl_context` is an SDL_GLContext (void*
|
||||
// to avoid leaking SDL types into this header) already created and current on
|
||||
// `window`. `io` is seeded with the effective settings (game.toml ∪ settings.toml)
|
||||
// and, on Result::Launch, updated in place with the user's choices. `assets_dir`
|
||||
// is the directory holding launcher.rml / .rcss / fonts.
|
||||
Result run(SDL_Window* window, void* gl_context,
|
||||
PSXRecompV4::UserSettings& io,
|
||||
const GameInfo& game, const char* assets_dir);
|
||||
|
||||
} // namespace psx_launcher
|
||||
@@ -1,6 +0,0 @@
|
||||
// stb_image implementation TU. Kept separate so the (large) decoder lives in
|
||||
// its own translation unit and launcher.cpp only sees the declarations.
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STBI_NO_STDIO // we feed it memory buffers via RmlUi's FileInterface
|
||||
#define STBI_ONLY_PNG // launcher art is PNG; keeps the decoder small
|
||||
#include "third_party/stb_image.h"
|
||||
@@ -95,55 +95,10 @@ else()
|
||||
option(PSX_STATIC_RUNTIME "Statically link SDL2 + libgcc/libstdc++ for a self-contained exe" OFF)
|
||||
endif()
|
||||
|
||||
# PSX_LAUNCHER: build the integrated RmlUi launcher UI (settings/disc/memcard/
|
||||
# controller front-end shown before the emulator boots). Needs the vendored
|
||||
# lib/RmlUi + lib/freetype submodules. ON by default; the oracle/beetle builds
|
||||
# never include it. Turn OFF for a launcher-less runtime (boots straight in).
|
||||
option(PSX_LAUNCHER "Build the integrated RmlUi launcher UI" ON)
|
||||
|
||||
# Build the vendored RmlUi + FreeType once per CMake project (idempotent — the
|
||||
# rmlui_core target guards re-entry). Both are linked statically into the exe so
|
||||
# the self-contained-binary guarantee (PSX_STATIC_RUNTIME) still holds.
|
||||
function(psxrecomp_ensure_launcher_libs)
|
||||
if(TARGET rmlui_core)
|
||||
return()
|
||||
endif()
|
||||
if(NOT EXISTS "${PSXRECOMP_ROOT}/lib/RmlUi/CMakeLists.txt" OR
|
||||
NOT EXISTS "${PSXRECOMP_ROOT}/lib/freetype/CMakeLists.txt")
|
||||
message(FATAL_ERROR
|
||||
"PSX_LAUNCHER=ON but lib/RmlUi or lib/freetype is missing. "
|
||||
"Run: git submodule update --init --recursive")
|
||||
endif()
|
||||
|
||||
# Static libs fold into the exe (keeps the self-contained binary intact).
|
||||
set(BUILD_SHARED_LIBS OFF)
|
||||
|
||||
# FreeType — RmlUi's font engine. Disable every optional codec/dep so the
|
||||
# only thing we pull in is the core font rasteriser.
|
||||
set(FT_DISABLE_ZLIB TRUE CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_BZIP2 TRUE CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_PNG TRUE CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_HARFBUZZ TRUE CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_BROTLI TRUE CACHE BOOL "" FORCE)
|
||||
add_subdirectory("${PSXRECOMP_ROOT}/lib/freetype"
|
||||
"${CMAKE_BINARY_DIR}/_deps/freetype-build" EXCLUDE_FROM_ALL)
|
||||
# FreeType's add_subdirectory exports the target `freetype` but not the
|
||||
# namespaced alias (that's install-only). RmlUi's soft dependency check
|
||||
# looks for Freetype::Freetype, so create it here.
|
||||
if(NOT TARGET Freetype::Freetype)
|
||||
add_library(Freetype::Freetype ALIAS freetype)
|
||||
endif()
|
||||
|
||||
# RmlUi — core only, FreeType font engine, no samples/tests/PCH.
|
||||
set(RMLUI_SAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(RMLUI_FONT_ENGINE "freetype" CACHE STRING "" FORCE)
|
||||
set(RMLUI_PRECOMPILED_HEADERS OFF CACHE BOOL "" FORCE)
|
||||
# The bundled robin_hood hash map fails its bitness detection under MinGW
|
||||
# GCC ("#error Unsupported bitness"); use std:: containers instead.
|
||||
set(RMLUI_THIRDPARTY_CONTAINERS OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory("${PSXRECOMP_ROOT}/lib/RmlUi"
|
||||
"${CMAKE_BINARY_DIR}/_deps/RmlUi-build" EXCLUDE_FROM_ALL)
|
||||
endfunction()
|
||||
# PSX_LAUNCHER remains a game-level opt-in used by downstream game CMake files
|
||||
# to include recomp-ui. Targets gain launcher code only when recomp-ui defines
|
||||
# RECOMP_LAUNCHER on that target.
|
||||
option(PSX_LAUNCHER "Allow downstream game projects to wire recomp-ui launcher support" ON)
|
||||
|
||||
set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/main.cpp
|
||||
@@ -504,38 +459,6 @@ function(psxrecomp_add_runtime_target target)
|
||||
target_compile_definitions(${target} PRIVATE PSX_COSIM=1 PSX_NO_DEBUG_TOOLS=1)
|
||||
endif()
|
||||
|
||||
# Integrated RmlUi launcher (not in the oracle build — that's headless).
|
||||
if(PSX_LAUNCHER AND NOT PSXRT_ORACLE)
|
||||
psxrecomp_ensure_launcher_libs()
|
||||
target_sources(${target} PRIVATE
|
||||
${PSXRECOMP_ROOT}/runtime/launcher/launcher.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/launcher/stb_image_impl.cpp
|
||||
${PSXRECOMP_ROOT}/lib/RmlUi/Backends/RmlUi_Platform_SDL.cpp
|
||||
${PSXRECOMP_ROOT}/lib/RmlUi/Backends/RmlUi_Renderer_GL3.cpp
|
||||
)
|
||||
# The vendored GL3 backend uses std::all_of without including
|
||||
# <algorithm>; force-include it rather than patching the pinned
|
||||
# submodule. (GCC/Clang only; MSVC pulls it in transitively.)
|
||||
if(NOT MSVC)
|
||||
set_source_files_properties(
|
||||
${PSXRECOMP_ROOT}/lib/RmlUi/Backends/RmlUi_Renderer_GL3.cpp
|
||||
PROPERTIES COMPILE_OPTIONS "-include;algorithm")
|
||||
endif()
|
||||
target_include_directories(${target} PRIVATE
|
||||
${PSXRECOMP_ROOT}/lib/RmlUi/Include
|
||||
${PSXRECOMP_ROOT}/lib/RmlUi/Backends
|
||||
${PSXRECOMP_ROOT}/runtime/launcher
|
||||
)
|
||||
target_link_libraries(${target} PRIVATE RmlUi::Core)
|
||||
target_compile_definitions(${target} PRIVATE PSX_LAUNCHER=1)
|
||||
# Ship the launcher assets (RML/RCSS/fonts) next to the exe.
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${PSXRECOMP_ROOT}/runtime/launcher/assets"
|
||||
"$<TARGET_FILE_DIR:${target}>"
|
||||
COMMENT "Copying launcher assets next to ${target}")
|
||||
endif()
|
||||
|
||||
if(WIN32 OR MINGW)
|
||||
# opengl32: GL backend (gpu_gl_renderer.c). GL 1.x is exported directly
|
||||
# by opengl32; Phase 2b will load modern GL via SDL_GL_GetProcAddress.
|
||||
|
||||
@@ -54,9 +54,6 @@ extern "C" void psx_event_step_conservative_env_init(void);
|
||||
#include "disc_identity.h"
|
||||
#include "iso_reader.h" /* text-image guard: extract the boot EXE from the disc */
|
||||
#include "psx_keybinds.h" /* configurable keyboard->DualShock keybinds (keybinds.ini) */
|
||||
#if defined(PSX_LAUNCHER)
|
||||
#include "launcher.h"
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
#include "recomp_launcher.h" /* shared recomp-ui Dear ImGui launcher (prototype) */
|
||||
#include "launcher_profile.h" /* per-system variant profile (theme/caps bundle) */
|
||||
@@ -3249,8 +3246,7 @@ static void sdl_vblank_present(void) {
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
// Host verification/inspection callbacks for the shared recomp-ui launcher.
|
||||
// The launcher re-runs these on every disc/memory-card change so the "Disc
|
||||
// verified" verdict + memcard block grids reflect the REAL images (parity with
|
||||
// the old RmlUi launcher, which computed the same facts inline). The expected
|
||||
// verified" verdict + memcard block grids reflect the REAL images. The expected
|
||||
// serial/CRC are passed via these file-scope statics (set just before
|
||||
// recomp_launcher_run_window) because the C-ABI callback can't capture locals.
|
||||
namespace {
|
||||
@@ -3268,7 +3264,7 @@ namespace {
|
||||
std::snprintf(out->serial, sizeof(out->serial), "%s", serial.c_str());
|
||||
std::snprintf(out->region, sizeof(out->region), "%s", id.region.c_str());
|
||||
out->iso_ok = id.has_header ? 1 : 0;
|
||||
// Verdict, mirroring the RmlUi launcher's state machine.
|
||||
// Verdict state consumed by the launcher UI.
|
||||
if (!id.opened || !id.has_header) out->verdict = 3; // bad
|
||||
else if (id.expected_serial_given && !id.serial_matches) out->verdict = 3; // wrong disc
|
||||
else if (id.expected_crc_given && id.crc_computed && !id.crc_matches) out->verdict = 2; // warn
|
||||
@@ -3432,6 +3428,7 @@ int main(int argc, char** argv) {
|
||||
int ctrl_locked_p2_mode = PSXRecompV4::PAD_MODE_HYBRID;
|
||||
bool ws_offered = true; /* game.toml [widescreen] offer; false hides the launcher toggle + clamps 4:3 */
|
||||
bool ws_ultrawide_offered = false;
|
||||
bool vulkan_offered = false; /* game.toml [video] offer_vulkan; developer opt-in for launcher visibility */
|
||||
int resolved_deadzone = -1; /* <0 => keep input.ini/runtime default (12000) */
|
||||
/* Localization: the effective language (game.toml default -> settings.toml ->
|
||||
* launcher choice), applied to the translation layer AFTER the launcher runs.
|
||||
@@ -3603,6 +3600,7 @@ int main(int argc, char** argv) {
|
||||
gc.ws_cull_h_imms.data(), (int)gc.ws_cull_h_imms.size());
|
||||
ws_offered = gc.ws_offered;
|
||||
ws_ultrawide_offered = gc.ws_ultrawide_offered;
|
||||
vulkan_offered = gc.vulkan_offered;
|
||||
/* Register the [widescreen.backdrop] store PCs so the dirty-RAM
|
||||
* interpreter applies the backdrop screenX squash on the interp
|
||||
* path (overlay backdrop handlers run interpreted when no cache
|
||||
@@ -3870,7 +3868,16 @@ int main(int argc, char** argv) {
|
||||
"again from the launcher (a fresh settings.toml will be written).");
|
||||
}
|
||||
if (us.has_skip_launcher) skip_launcher_setting = us.skip_launcher;
|
||||
if (us.has_renderer) g_video_renderer = us.renderer;
|
||||
if (us.has_renderer) {
|
||||
if (us.renderer == 2 && !vulkan_offered) {
|
||||
g_video_renderer = 1;
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: settings requested Vulkan, but this game does not "
|
||||
"offer Vulkan in the launcher; using OpenGL.\n");
|
||||
} else {
|
||||
g_video_renderer = us.renderer;
|
||||
}
|
||||
}
|
||||
if (us.has_supersampling) g_video_scale = us.supersampling;
|
||||
if (us.has_window_width) g_video_win_w = us.window_width;
|
||||
if (us.has_antialiasing) g_video_aa = us.antialiasing;
|
||||
@@ -4024,8 +4031,8 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(PSX_LAUNCHER)
|
||||
/* Integrated launcher: shown in its own GL window before the emulator
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
/* Integrated recomp-ui launcher: shown in its own GL window before the emulator
|
||||
* boots. Seeded with the effective settings (game.toml ∪ settings.toml);
|
||||
* on LAUNCH the user's choices are persisted to settings.toml and applied.
|
||||
* The launcher window/context is fully torn down before the emulator's own
|
||||
@@ -4075,16 +4082,8 @@ int main(int argc, char** argv) {
|
||||
seed.has_deadzone = true;
|
||||
seed.window_width = g_video_win_w; seed.has_window_width = true;
|
||||
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
/* ---- recomp-ui (Dear ImGui) launcher path — PROTOTYPE ----------
|
||||
* Runs the shared, console-agnostic recomp-ui launcher (see
|
||||
* F:\Projects\recomp-ui) INSTEAD OF the RmlUi launcher above. Unlike
|
||||
* psx_launcher::run, recomp_launcher_run_window() creates + owns its
|
||||
* own SDL2/GL window internally, so there is no lwin/lctx to manage
|
||||
* here and no GL-attribute pre-configuration needed. The result is
|
||||
* funneled into the same `seed` / `lr` locals the shared
|
||||
* post-launcher block below (Quit / Launch handling + settings.toml
|
||||
* persistence) already consumes, so that code needs no changes. */
|
||||
/* recomp-ui creates + owns its SDL2/GL window internally, so there
|
||||
* is no launcher window/context to manage here. */
|
||||
std::string assets_dir_str = exe_dir_from_argv(argv[0]).string();
|
||||
std::string rui_initial_disc = resolved_disc.string();
|
||||
std::string rui_title = (game_name.empty() ? std::string("PSX") : game_name)
|
||||
@@ -4121,6 +4120,8 @@ int main(int argc, char** argv) {
|
||||
* below). Sourced 1:1 from PSXRecompV4::UserSettings (config_loader.h). */
|
||||
ls.window_width = seed.window_width;
|
||||
ls.renderer = seed.renderer;
|
||||
if (ls.renderer < 0 || ls.renderer > (vulkan_offered ? 2 : 1))
|
||||
ls.renderer = 1;
|
||||
ls.supersampling = seed.supersampling;
|
||||
ls.antialiasing = seed.antialiasing ? 1 : 0;
|
||||
ls.texture_filter = seed.texture_filter;
|
||||
@@ -4132,8 +4133,7 @@ int main(int argc, char** argv) {
|
||||
ls.turbo_loads = seed.turbo_loads ? 1 : 0;
|
||||
/* Localization: index of resolved_language within lang_menu_options
|
||||
* (match by code; games with no [runtime].languages list leave
|
||||
* lang_menu_options empty and this stays 0 / unused), mirroring
|
||||
* lang_index_for() in launcher.cpp (RmlUi path, below). */
|
||||
* lang_menu_options empty and this stays 0 / unused). */
|
||||
ls.language_index = 0;
|
||||
for (size_t li = 0; li < lang_menu_options.size(); li++) {
|
||||
if (lang_menu_options[li].code == resolved_language) {
|
||||
@@ -4168,8 +4168,7 @@ int main(int argc, char** argv) {
|
||||
const std::string rui_region =
|
||||
!game_region.empty() ? game_region : region_label_from_serial(game_id);
|
||||
|
||||
/* Localization: parallels the RmlUi ginfo.languages push_back loop
|
||||
* below (#else arm) — one C-string array pointing at lang_menu_options'
|
||||
/* Localization: one C-string array pointing at lang_menu_options'
|
||||
* own storage (that vector outlives this call, it's a main()-scope
|
||||
* local), so no separate copy is needed. Empty when the game declares
|
||||
* no [runtime].languages -> Localization stays hidden (num_languages=0). */
|
||||
@@ -4178,6 +4177,11 @@ int main(int argc, char** argv) {
|
||||
for (const auto& lo : lang_menu_options) rui_lang_labels.push_back(lo.label.c_str());
|
||||
|
||||
RecompLauncherCGameInfo gi{};
|
||||
static const char* const kPsxRendererLabels[] = {
|
||||
"Software",
|
||||
"OpenGL (Recommended)",
|
||||
"Vulkan"
|
||||
};
|
||||
/* System identity + full PS1 settings-surface capability set (theme,
|
||||
* platform label, rom_noun, pad-mode support, aspect mask base, and
|
||||
* all has_* deep-settings flags) — one profile call keeps them from
|
||||
@@ -4193,18 +4197,16 @@ int main(int argc, char** argv) {
|
||||
gi.num_players = game_players;
|
||||
gi.msu1_supported = 0;
|
||||
gi.sram_path = nullptr; /* PSX uses memory cards, not SRAM -> hide SAVES */
|
||||
/* Pad-mode + aspect capabilities, sourced the same way the RmlUi
|
||||
* launcher path (psx_launcher::GameInfo, below in the #else arm)
|
||||
* sources ginfo.allow_hybrid/lock_mode/locked_mode/lock_device/
|
||||
* ws_offered/ws_ultrawide_offered — from game.toml [controller]/
|
||||
* [widescreen] via GameConfig. PSX always has pad modes. */
|
||||
/* Pad-mode + aspect capabilities sourced from game.toml
|
||||
* [controller]/[widescreen] via GameConfig. PSX always has pad modes. */
|
||||
gi.pad_mode_selectable = ctrl_lock_mode ? 0 : 1;
|
||||
gi.allow_hybrid = ctrl_allow_hybrid ? 1 : 0;
|
||||
gi.locked_pad_mode = p1_mode; /* force the game's declared mode (default_mode) */
|
||||
gi.lock_device = ctrl_lock_device ? 1 : 0;
|
||||
gi.aspect_mask = 0x1 | (ws_offered ? 0x2 : 0) | (ws_ultrawide_offered ? 0x4 : 0);
|
||||
/* Localization menu: shown only when the game declares languages
|
||||
* (mirrors the RmlUi ginfo.languages parity below). */
|
||||
gi.renderer_labels = kPsxRendererLabels;
|
||||
gi.num_renderers = vulkan_offered ? 3 : 2;
|
||||
/* Localization menu: shown only when the game declares languages. */
|
||||
if (!rui_lang_labels.empty()) {
|
||||
gi.language_labels = rui_lang_labels.data();
|
||||
gi.num_languages = (int)rui_lang_labels.size();
|
||||
@@ -4213,7 +4215,7 @@ int main(int argc, char** argv) {
|
||||
/* Real disc-verify + memcard-inspect (identify_disc / memcard_summary_path
|
||||
* via the ae_* callbacks). The launcher re-runs these whenever the
|
||||
* user changes the disc or a memory card, so the verdict + block
|
||||
* grids reflect the actual images — parity with the RmlUi launcher.
|
||||
* grids reflect the actual images.
|
||||
* The expected serial/CRC ride on file-scope statics (the C-ABI
|
||||
* callback can't capture these locals). */
|
||||
g_lnch_expected_serial = game_id;
|
||||
@@ -4227,12 +4229,9 @@ int main(int argc, char** argv) {
|
||||
rui_title.c_str(), &ls, &gi, assets_dir_str.c_str(),
|
||||
rui_initial_disc.c_str(), rui_out_disc, sizeof(rui_out_disc));
|
||||
|
||||
psx_launcher::Result lr =
|
||||
(rui_rc == 0) ? psx_launcher::Result::Launch :
|
||||
(rui_rc == 1) ? psx_launcher::Result::Quit :
|
||||
psx_launcher::Result::Unavailable;
|
||||
int lr = rui_rc; /* 0=Launch, 1=Quit, other=Unavailable */
|
||||
|
||||
if (lr == psx_launcher::Result::Launch) {
|
||||
if (lr == 0) {
|
||||
if (rui_out_disc[0]) {
|
||||
seed.disc_path = rui_out_disc;
|
||||
seed.has_disc_path = true;
|
||||
@@ -4284,63 +4283,19 @@ int main(int argc, char** argv) {
|
||||
|
||||
/* Localization: persist the chosen language code (only meaningful
|
||||
* when the game declared a menu; otherwise leave seed.language
|
||||
* untouched) -- mirrors launcher.cpp's io.language write-back
|
||||
* (RmlUi path, below) exactly, down to the bounds check. */
|
||||
* untouched. */
|
||||
if (!lang_menu_options.empty() && ls.language_index >= 0 &&
|
||||
ls.language_index < (int)lang_menu_options.size()) {
|
||||
seed.language = lang_menu_options[ls.language_index].code;
|
||||
seed.has_language = true;
|
||||
}
|
||||
}
|
||||
#else
|
||||
configure_core_gl_context_attributes();
|
||||
|
||||
/* Launcher opens at the same 4:3 size the game will, so there's no
|
||||
* jarring resize on LAUNCH. The dense dashboard needs a usable
|
||||
* minimum, so the launcher floor is 1280 wide even if the game
|
||||
* window is set smaller. */
|
||||
int lwin_w = g_video_win_w < 1280 ? 1280 : g_video_win_w;
|
||||
int lwin_h = 0;
|
||||
clamp_window_aspect(&lwin_w, &lwin_h, 4, 3);
|
||||
std::string lwin_title = (game_name.empty() ? std::string("PSX") : game_name)
|
||||
+ " \xE2\x80\x94 Launcher";
|
||||
SDL_Window* lwin = SDL_CreateWindow(
|
||||
lwin_title.c_str(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
lwin_w, lwin_h, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
|
||||
psx_launcher::Result lr = psx_launcher::Result::Unavailable;
|
||||
if (lwin) {
|
||||
SDL_GLContext lctx = SDL_GL_CreateContext(lwin);
|
||||
if (lctx) {
|
||||
SDL_GL_MakeCurrent(lwin, lctx);
|
||||
SDL_GL_SetSwapInterval(1);
|
||||
std::string assets = exe_dir_from_argv(argv[0]).string();
|
||||
psx_launcher::GameInfo ginfo;
|
||||
ginfo.name = game_name.empty() ? nullptr : game_name.c_str();
|
||||
ginfo.expected_serial = game_id.empty() ? nullptr : game_id.c_str();
|
||||
ginfo.expected_crc = game_disc_crc;
|
||||
ginfo.has_expected_crc = game_has_disc_crc;
|
||||
ginfo.allow_hybrid = ctrl_allow_hybrid;
|
||||
ginfo.lock_mode = ctrl_lock_mode;
|
||||
ginfo.locked_mode = p1_mode; /* force the game's declared mode (default_mode) */
|
||||
ginfo.lock_device = ctrl_lock_device;
|
||||
ginfo.ws_offered = ws_offered;
|
||||
ginfo.ws_ultrawide_offered = ws_ultrawide_offered;
|
||||
for (const auto& lo : lang_menu_options)
|
||||
ginfo.languages.push_back({ lo.code, lo.label });
|
||||
lr = psx_launcher::run(lwin, lctx, seed, ginfo, assets.c_str());
|
||||
SDL_GL_DeleteContext(lctx);
|
||||
}
|
||||
SDL_DestroyWindow(lwin);
|
||||
}
|
||||
/* Reset GL attributes so the emulator window starts from defaults. */
|
||||
SDL_GL_ResetAttributes();
|
||||
#endif
|
||||
|
||||
if (lr == psx_launcher::Result::Quit) {
|
||||
if (lr == 1) {
|
||||
std::fprintf(stdout, "psxrecomp: launcher closed; exiting.\n");
|
||||
return 0;
|
||||
}
|
||||
if (lr == psx_launcher::Result::Launch) {
|
||||
if (lr == 0) {
|
||||
g_video_renderer = seed.renderer;
|
||||
g_video_scale = seed.supersampling;
|
||||
g_video_aa = seed.antialiasing;
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
|
||||
source = (Path(__file__).parents[1] / "launcher" / "launcher.cpp").read_text()
|
||||
assert 'v == 2 ? "Vulkan"' in source
|
||||
assert "m.renderer = (m.renderer + 1) % 3" in source
|
||||
assert "m.renderer == 1" in source # OpenGL-only controls remain OpenGL-only.
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
main = (root / "runtime" / "src" / "main.cpp").read_text()
|
||||
config_h = (root / "recompiler" / "src" / "config_loader.h").read_text()
|
||||
config_cpp = (root / "recompiler" / "src" / "config_loader.cpp").read_text()
|
||||
runtime_cmake = (root / "runtime" / "runtime.cmake").read_text()
|
||||
|
||||
assert "runtime/" + "launcher" not in runtime_cmake
|
||||
assert "Rml" + "Ui" not in runtime_cmake
|
||||
assert '#include "launcher.h"' not in main
|
||||
|
||||
assert "bool video_offer_vulkan = false;" in config_h
|
||||
assert "bool vulkan_offered = false;" in config_h
|
||||
assert 'video.contains("offer_vulkan")' in config_cpp
|
||||
assert 'rt.video_offer_vulkan = toml::find<bool>(video, "offer_vulkan");' in config_cpp
|
||||
assert "bool vulkan_offered = false;" in config_cpp
|
||||
assert "/*vulkan_offered*/" in config_cpp
|
||||
|
||||
assert '"Software"' in main
|
||||
assert '"OpenGL (Recommended)"' in main
|
||||
assert '"Vulkan"' in main
|
||||
assert "gi.renderer_labels = kPsxRendererLabels;" in main
|
||||
assert "gi.num_renderers = vulkan_offered ? 3 : 2;" in main
|
||||
assert "ls.renderer < 0 || ls.renderer > (vulkan_offered ? 2 : 1)" in main
|
||||
assert "settings requested Vulkan, but this game does not" in main
|
||||
|
||||
print("Launcher Vulkan-option test passed")
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
# Generates placeholder launcher art into runtime/launcher/assets/img.
|
||||
# Stylized reproductions of the mockup (disc, controllers, memory card, PS-glyph
|
||||
# logo, verification icons). Swap with real renders later — the <img>/decorator
|
||||
# pipeline (stb_image PNG loader) is the same either way.
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$out = "runtime\launcher\assets\img"
|
||||
if (-not (Test-Path $out)) { New-Item -ItemType Directory -Force $out | Out-Null }
|
||||
|
||||
function New-Canvas($w, $h) {
|
||||
$bmp = New-Object System.Drawing.Bitmap($w, $h)
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
|
||||
$g.Clear([System.Drawing.Color]::Transparent)
|
||||
return $bmp, $g
|
||||
}
|
||||
function Save($bmp, $name) {
|
||||
$p = Join-Path $out $name
|
||||
$bmp.Save($p, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
Write-Output "wrote $name"
|
||||
}
|
||||
function RGB($r,$g,$b) { [System.Drawing.Color]::FromArgb(255,$r,$g,$b) }
|
||||
function ARGB($a,$r,$g,$b) { [System.Drawing.Color]::FromArgb($a,$r,$g,$b) }
|
||||
|
||||
# ---- check icons (18) ----
|
||||
function Make-Check($name, $on) {
|
||||
$bmp,$g = New-Canvas 36 36
|
||||
if ($on) {
|
||||
$br = New-Object System.Drawing.SolidBrush (RGB 63 185 80)
|
||||
$g.FillEllipse($br, 1, 1, 34, 34)
|
||||
$pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::White), 3.5
|
||||
$pen.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
|
||||
$pen.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
|
||||
$g.DrawLines($pen, @((New-Object System.Drawing.PointF(10,18)),
|
||||
(New-Object System.Drawing.PointF(16,24)),
|
||||
(New-Object System.Drawing.PointF(26,12))))
|
||||
} else {
|
||||
$pen = New-Object System.Drawing.Pen (RGB 60 70 84), 2.5
|
||||
$g.DrawEllipse($pen, 2, 2, 32, 32)
|
||||
}
|
||||
Save $bmp $name
|
||||
}
|
||||
Make-Check "check_on.png" $true
|
||||
Make-Check "check_off.png" $false
|
||||
|
||||
# ---- verdict icons (50) ----
|
||||
function Make-Verdict($name, $col, $kind) {
|
||||
$bmp,$g = New-Canvas 100 100
|
||||
$pen = New-Object System.Drawing.Pen $col, 5
|
||||
$pen.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
|
||||
$pen.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
|
||||
$g.DrawEllipse($pen, 6, 6, 88, 88)
|
||||
switch ($kind) {
|
||||
"ok" { $g.DrawLines($pen, @((New-Object System.Drawing.PointF(30,52)),
|
||||
(New-Object System.Drawing.PointF(45,67)),
|
||||
(New-Object System.Drawing.PointF(72,33)))) }
|
||||
"warn" { $g.DrawLine($pen,50,28,50,60); $g.FillEllipse((New-Object System.Drawing.SolidBrush $col),46,68,8,8) }
|
||||
"bad" { $g.DrawLine($pen,36,36,64,64); $g.DrawLine($pen,64,36,36,64) }
|
||||
"none" { $g.DrawLine($pen,38,40,62,40) }
|
||||
}
|
||||
Save $bmp $name
|
||||
}
|
||||
Make-Verdict "verdict_ok.png" (RGB 63 185 80) "ok"
|
||||
Make-Verdict "verdict_warn.png" (RGB 210 153 34) "warn"
|
||||
Make-Verdict "verdict_bad.png" (RGB 248 81 73) "bad"
|
||||
Make-Verdict "verdict_none.png" (RGB 110 119 129) "none"
|
||||
|
||||
# ---- PS-glyph logo (46) ----
|
||||
$bmp,$g = New-Canvas 92 92
|
||||
$pen = New-Object System.Drawing.Pen (RGB 0 0 0), 5
|
||||
$pen.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
|
||||
$pen.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
|
||||
# square (top-left, pink)
|
||||
$pen.Color = RGB 233 92 138; $g.DrawRectangle($pen, 10, 10, 28, 28)
|
||||
# triangle (top-right, teal)
|
||||
$pen.Color = RGB 45 212 191
|
||||
$g.DrawPolygon($pen, @((New-Object System.Drawing.PointF(68,10)),
|
||||
(New-Object System.Drawing.PointF(54,38)),
|
||||
(New-Object System.Drawing.PointF(82,38))))
|
||||
# X (bottom-left, purple)
|
||||
$pen.Color = RGB 137 87 229
|
||||
$g.DrawLine($pen,12,56,38,82); $g.DrawLine($pen,38,56,12,82)
|
||||
# circle (bottom-right, red)
|
||||
$pen.Color = RGB 239 83 110; $g.DrawEllipse($pen, 54, 54, 28, 28)
|
||||
Save $bmp "logo.png"
|
||||
|
||||
# ---- disc (200) ----
|
||||
$bmp,$g = New-Canvas 240 240
|
||||
$silver = RGB 196 201 208
|
||||
$g.FillEllipse((New-Object System.Drawing.SolidBrush $silver), 6, 6, 228, 228)
|
||||
$pen = New-Object System.Drawing.Pen (RGB 150 156 165), 2
|
||||
$g.DrawEllipse($pen, 6, 6, 228, 228)
|
||||
# subtle ring
|
||||
$g.DrawEllipse((New-Object System.Drawing.Pen ((ARGB 90 255 255 255), 1)), 40, 40, 160, 160)
|
||||
# hub + hole
|
||||
$g.FillEllipse((New-Object System.Drawing.SolidBrush (RGB 170 176 184)), 78, 78, 84, 84)
|
||||
$g.FillEllipse((New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::Transparent)), 104, 104, 32, 32)
|
||||
$g.FillEllipse((New-Object System.Drawing.SolidBrush (RGB 11 14 20)), 104, 104, 32, 32)
|
||||
# faint inner data ring for a subtle CD sheen
|
||||
$g.DrawEllipse((New-Object System.Drawing.Pen ((ARGB 60 255 255 255), 1)), 64, 64, 112, 112)
|
||||
Save $bmp "disc.png"
|
||||
|
||||
# ---- controller ----
|
||||
function Make-Pad($name, $analog) {
|
||||
$bmp,$g = New-Canvas 280 200
|
||||
$body = New-Object System.Drawing.SolidBrush (RGB 154 161 171)
|
||||
$dark = New-Object System.Drawing.SolidBrush (RGB 64 70 82)
|
||||
# grips + body (rounded rects approximated with ellipse caps)
|
||||
$g.FillEllipse($body, 8, 70, 90, 120) # left grip
|
||||
$g.FillEllipse($body, 182, 70, 90, 120) # right grip
|
||||
$g.FillRectangle($body, 60, 60, 160, 80) # center
|
||||
$g.FillEllipse($body, 40, 55, 90, 90)
|
||||
$g.FillEllipse($body, 150, 55, 90, 90)
|
||||
# d-pad (left)
|
||||
$g.FillRectangle($dark, 70, 88, 36, 12)
|
||||
$g.FillRectangle($dark, 82, 76, 12, 36)
|
||||
# face buttons (right) — PS colors
|
||||
$g.FillEllipse((New-Object System.Drawing.SolidBrush (RGB 233 92 138)), 188, 82, 14, 14) # square pink
|
||||
$g.FillEllipse((New-Object System.Drawing.SolidBrush (RGB 45 212 191)), 200, 70, 14, 14) # triangle teal
|
||||
$g.FillEllipse((New-Object System.Drawing.SolidBrush (RGB 239 83 110)), 212, 82, 14, 14) # circle red
|
||||
$g.FillEllipse((New-Object System.Drawing.SolidBrush (RGB 137 87 229)), 200, 94, 14, 14) # x purple
|
||||
if ($analog) {
|
||||
$g.FillEllipse($dark, 104, 120, 30, 30)
|
||||
$g.FillEllipse($dark, 150, 120, 30, 30)
|
||||
}
|
||||
Save $bmp $name
|
||||
}
|
||||
Make-Pad "pad_digital.png" $false
|
||||
Make-Pad "pad_analog.png" $true
|
||||
|
||||
# ---- memory card (110x132) ----
|
||||
$bmp,$g = New-Canvas 220 264
|
||||
$body = New-Object System.Drawing.SolidBrush (RGB 150 157 167)
|
||||
$g.FillRectangle($body, 20, 16, 180, 230)
|
||||
# label area
|
||||
$g.FillRectangle((New-Object System.Drawing.SolidBrush (RGB 176 182 191)), 36, 40, 148, 120)
|
||||
# connector notch
|
||||
$g.FillRectangle((New-Object System.Drawing.SolidBrush (RGB 64 70 82)), 70, 210, 80, 22)
|
||||
Save $bmp "memcard.png"
|
||||
|
||||
# ---- dropdown caret (16x16, downward triangle) ----
|
||||
$bmp,$g = New-Canvas 32 32
|
||||
$tri = New-Object System.Drawing.SolidBrush (RGB 173 186 199)
|
||||
$g.FillPolygon($tri, @((New-Object System.Drawing.PointF(7,11)),
|
||||
(New-Object System.Drawing.PointF(25,11)),
|
||||
(New-Object System.Drawing.PointF(16,22))))
|
||||
Save $bmp "caret.png"
|
||||
|
||||
Write-Output "done"
|
||||