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>
This commit is contained in:
Matthew Stanley
2026-07-21 16:29:08 -07:00
parent da64f44e82
commit 9228f32c00
36 changed files with 117 additions and 8891 deletions
-6
View File
@@ -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
+1 -1
View File
@@ -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).
+4 -14
View File
@@ -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`.
---
+1 -2
View File
@@ -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.
+2 -2
View File
@@ -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).
+4 -8
View File
@@ -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 |
+1 -1
View File
@@ -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).
+15
View File
@@ -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
+3 -4
View File
@@ -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
Submodule lib/RmlUi deleted from 7a06f27db0
Submodule lib/freetype deleted from 42608f77f2
+10
View File
@@ -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,
+9 -4
View File
@@ -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;
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 787 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

-473
View File
@@ -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 &#8212; 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>
File diff suppressed because it is too large Load Diff
-63
View File
@@ -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
-6
View File
@@ -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"
File diff suppressed because it is too large Load Diff
+4 -81
View File
@@ -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.
+38 -83
View File
@@ -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;
+25 -4
View File
@@ -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")
-148
View File
@@ -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"