& opts) {
std::string s;
for (size_t i = 0; i < opts.size(); i++) {
s += "";
s += rml_escape(opts[i].label);
s += "
";
}
return s;
}
// Recompute a player's derived display fields from its selected option index.
void refresh_player(LauncherModel& m, int player, const std::vector& opts) {
int& idx = player == 0 ? m.p1_dev_index : m.p2_dev_index;
Rml::String& label = player == 0 ? m.p1_dev_label : m.p2_dev_label;
Rml::String& status = player == 0 ? m.p1_status : m.p2_status;
Rml::String& dot = player == 0 ? m.p1_dot : m.p2_dot;
Rml::String& options= player == 0 ? m.p1_options : m.p2_options;
const bool analog = player == 0 ? m.p1_analog : m.p2_analog;
if (idx < 0 || idx >= (int)opts.size()) idx = 0;
const DeviceOption& o = opts[idx];
label = o.label;
options = build_options_rml(player, opts);
const char* type = analog ? "DualShock (analog)" : "digital pad";
if (o.kind == 0) { status = "No device — port empty"; dot = "off"; }
else if (o.kind == 1) { status = Rml::String("Keyboard \xE2\x80\x94 ") + type; dot = ""; }
else { status = o.label + Rml::String(" \xE2\x80\x94 ") + type; dot = ""; }
}
const char* renderer_name(int v) { return v == 1 ? "OpenGL" : "Software"; }
const char* texfilter_name(int v) { return v == 1 ? "Bilinear" : "Nearest"; }
const char* crt_name(int v) {
switch (v) {
case 1: return "CRT";
case 2: return "Composite";
case 3: return "Trinitron";
default: return "Raw (off)";
}
}
// Offered display aspects. 4:3 is the native presentation every game ships
// with; wider aspects enable the runtime widescreen hack (GTE X-squash +
// stretched present — see [video] aspect_ratio in config_loader.h).
const int kAspects[][2] = { {4, 3}, {16, 9}, {21, 9} };
const int kNumAspects = (int)(sizeof(kAspects) / sizeof(kAspects[0]));
const char* aspect_name(int i) {
switch (i) {
case 1: return "16:9 (Widescreen)";
case 2: return "21:9 (Ultrawide)";
default: return "4:3 (Native)";
}
}
int aspect_index_for(int num, int den) {
for (int i = 0; i < kNumAspects; i++)
if (kAspects[i][0] == num && kAspects[i][1] == den) return i;
return 0;
}
// Offered window widths (height follows the chosen aspect). The toggle cycles
// through these.
const int kWinWidths[] = { 960, 1280, 1600, 1920 };
const int kNumWinWidths = (int)(sizeof(kWinWidths) / sizeof(kWinWidths[0]));
// Snap an arbitrary width to the nearest offered option index.
int winsize_index(int width) {
int best = 1, bestd = 1 << 30; // default to 1280
for (int i = 0; i < kNumWinWidths; i++) {
int d = width > kWinWidths[i] ? width - kWinWidths[i] : kWinWidths[i] - width;
if (d < bestd) { bestd = d; best = i; }
}
return best;
}
std::string winsize_label_for(int width, int aspect_index) {
const int num = kAspects[aspect_index][0], den = kAspects[aspect_index][1];
return std::to_string(width) + " \xC3\x97 " + std::to_string(width * den / num); // "1280 × 960"
}
void refresh_labels(LauncherModel& m) {
m.renderer_label = renderer_name(m.renderer);
m.crt_label = crt_name(m.crt);
m.texfilter_label = texfilter_name(m.texture_filter);
m.aspect_label = aspect_name(m.aspect_index);
m.winsize_label = winsize_label_for(m.window_width, m.aspect_index);
}
std::string region_long(const std::string& r) {
if (r == "NTSC-U") return "NTSC-U (USA)";
if (r == "NTSC-J") return "NTSC-J (Japan)";
if (r == "PAL") return "PAL (Europe)";
return r;
}
// Re-run disc verification against m.disc_path and update the panel fields.
// `expected_serial`/`expected_crc` come from game.toml (via GameInfo);
// `game_name` is used for the verdict headline.
void refresh_disc_status(LauncherModel& m, const std::string& game_name,
const std::string& expected_serial,
uint32_t expected_crc, bool has_expected_crc) {
m.v_header = m.v_crc = m.v_verified = false;
m.disc_region = m.disc_serial = "—";
m.disc_file = "—";
if (m.disc_path.empty()) {
m.verdict_title = "No disc selected";
m.verdict_detail = "Choose a disc image to verify it against this build.";
m.verdict_state = "none";
return;
}
m.disc_file = fs::path(std::string(m.disc_path)).filename().generic_string();
// Only spend time hashing when there is an expected CRC to compare against.
const PSXRecompV4::DiscIdentity id = PSXRecompV4::identify_disc(
fs::path(std::string(m.disc_path)), expected_serial,
expected_crc, has_expected_crc, /*compute_crc=*/has_expected_crc);
if (!id.opened) {
m.verdict_title = "Disc not found";
m.verdict_detail = "Could not open the image or its CUE-referenced BIN.";
m.verdict_state = "bad";
return;
}
if (!id.has_header) {
m.verdict_title = "Not a PlayStation disc";
m.verdict_detail = "No ISO9660 header at the expected sectors.";
m.verdict_state = "bad";
return;
}
m.v_header = true;
if (!id.detected_serial.empty()) m.disc_serial = id.detected_serial;
else if (!expected_serial.empty()) m.disc_serial = expected_serial;
if (!id.region.empty()) m.disc_region = region_long(id.region);
const bool serial_ok = !id.expected_serial_given || id.serial_matches;
// Middle check: exact CRC match if we have one to compare, otherwise the
// serial-based identity match stands in for the hash.
if (id.expected_crc_given && id.crc_computed)
m.v_crc = id.crc_matches;
else
m.v_crc = serial_ok && id.expected_serial_given;
m.v_verified = m.v_header && serial_ok &&
(!id.expected_crc_given || id.crc_matches);
const std::string nm = game_name.empty() ? std::string("Disc") : game_name;
if (m.v_verified) {
m.verdict_title = nm + " disc verified";
m.verdict_detail = "Correct disc image loaded. Ready to launch.";
m.verdict_state = "ok";
} else if (!serial_ok) {
m.verdict_title = "Wrong disc?";
m.verdict_detail = "Serial does not match this build (expected " + expected_serial + ").";
m.verdict_state = "bad";
} else if (id.expected_crc_given && id.crc_computed && !id.crc_matches) {
m.verdict_title = "Disc image differs";
m.verdict_detail = "Right game, but the image hash does not match the expected dump.";
m.verdict_state = "warn";
} else {
// Header ok, nothing authoritative to compare against.
m.verdict_title = "PlayStation disc";
m.verdict_detail = "Recognised PlayStation disc. No reference hash configured.";
m.verdict_state = "ok";
m.v_verified = true;
}
}
#if defined(_WIN32)
// Native open-file dialog. Returns "" if cancelled.
std::string win_pick_file(SDL_Window* parent, const char* title, const char* filter) {
char buf[MAX_PATH] = {0};
OPENFILENAMEA ofn = {0};
ofn.lStructSize = sizeof(ofn);
ofn.lpstrFilter = filter; // e.g. "BIOS (*.bin)\0*.bin\0All files\0*.*\0\0"
ofn.lpstrFile = buf;
ofn.nMaxFile = sizeof(buf);
ofn.lpstrTitle = title;
ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
(void)parent;
if (GetOpenFileNameA(&ofn)) return std::string(buf);
return std::string();
}
// Native save-file dialog (overwrite-prompts). Returns "" if cancelled.
std::string win_pick_save_file(SDL_Window* parent, const char* title,
const char* filter, const char* default_ext,
const std::string& initial) {
char buf[MAX_PATH] = {0};
if (!initial.empty()) {
std::snprintf(buf, sizeof(buf), "%s", initial.c_str());
for (char* p = buf; *p; ++p) if (*p == '/') *p = '\\'; // native sep
}
OPENFILENAMEA ofn = {0};
ofn.lStructSize = sizeof(ofn);
ofn.lpstrFilter = filter;
ofn.lpstrFile = buf;
ofn.nMaxFile = sizeof(buf);
ofn.lpstrTitle = title;
ofn.lpstrDefExt = default_ext; // appended if the user types no extension
ofn.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
(void)parent;
if (GetSaveFileNameA(&ofn)) return std::string(buf);
return std::string();
}
#else
std::string win_pick_file(SDL_Window*, const char*, const char*) { return std::string(); }
std::string win_pick_save_file(SDL_Window*, const char*, const char*, const char*,
const std::string&) { return std::string(); }
#endif
// Load at least one font face so RmlUi can render text. Tries bundled fonts in
// assets_dir, then a couple of platform fallbacks. Returns true if any loaded.
bool load_fonts(const fs::path& assets_dir) {
const char* bundled[] = {
"fonts/LatoLatin-Regular.ttf",
"fonts/LatoLatin-Bold.ttf",
"LatoLatin-Regular.ttf",
};
bool any = false;
for (const char* rel : bundled) {
const fs::path p = assets_dir / rel;
std::error_code ec;
if (fs::exists(p, ec) && Rml::LoadFontFace(p.generic_string())) any = true;
}
if (any) return true;
#if defined(_WIN32)
const char* sys[] = { "C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf" };
for (const char* p : sys)
if (Rml::LoadFontFace(p)) any = true;
#endif
return any;
}
} // namespace
namespace psx_launcher {
Result run(SDL_Window* window, void* gl_context,
PSXRecompV4::UserSettings& io,
const GameInfo& game, const char* assets_dir)
{
(void)gl_context; // already created + current; we only need the window.
const std::string expected_serial = game.expected_serial ? game.expected_serial : "";
const uint32_t expected_crc = game.expected_crc;
const bool has_expected_crc = game.has_expected_crc;
if (!RmlGL3::Initialize()) {
std::fprintf(stderr, "launcher: RmlGL3::Initialize failed\n");
return Result::Unavailable;
}
LauncherSystemInterface system_interface;
system_interface.SetWindow(window);
LauncherRenderInterface render_interface;
if (!render_interface) {
std::fprintf(stderr, "launcher: GL3 render interface init failed\n");
RmlGL3::Shutdown();
return Result::Unavailable;
}
Rml::SetSystemInterface(&system_interface);
Rml::SetRenderInterface(&render_interface);
if (!Rml::Initialise()) {
std::fprintf(stderr, "launcher: Rml::Initialise failed\n");
RmlGL3::Shutdown();
return Result::Unavailable;
}
const fs::path assets = assets_dir ? fs::path(assets_dir) : fs::current_path();
if (!load_fonts(assets))
std::fprintf(stderr, "launcher: warning — no font face loaded; text will not render\n");
int win_w = 0, win_h = 0;
SDL_GL_GetDrawableSize(window, &win_w, &win_h);
if (win_w <= 0 || win_h <= 0) { win_w = 1280; win_h = 800; }
render_interface.SetViewport(win_w, win_h);
Rml::Context* context = Rml::CreateContext("launcher", Rml::Vector2i(win_w, win_h));
if (!context) {
std::fprintf(stderr, "launcher: CreateContext failed\n");
Rml::Shutdown();
RmlGL3::Shutdown();
return Result::Unavailable;
}
// ---- Seed the model from the effective settings ----
LauncherModel m;
m.renderer = io.renderer;
m.supersampling = io.supersampling;
m.antialiasing = io.antialiasing;
m.texture_filter = io.texture_filter;
m.crt = io.screen_kind;
m.spu_hq = io.spu_hq;
m.aspect_index = io.has_aspect_ratio ? aspect_index_for(io.aspect_num, io.aspect_den) : 0;
m.window_width = kWinWidths[winsize_index(io.has_window_width ? io.window_width : 1280)];
m.bios_path = io.has_bios_path ? io.bios_path.generic_string() : Rml::String();
m.disc_path = io.has_disc_path ? io.disc_path.generic_string() : Rml::String();
refresh_labels(m);
const std::string game_name_s = game.name ? game.name : "";
refresh_disc_status(m, game_name_s, expected_serial, expected_crc, has_expected_crc);
// ---- Seed the memory-card slots from the effective settings ----
if (io.has_memcard1_enabled) m.mc1_enabled = io.memcard1_enabled;
if (io.has_memcard2_enabled) m.mc2_enabled = io.memcard2_enabled;
m.mc1_path = memcard_slot_path(io, 0);
m.mc2_path = memcard_slot_path(io, 1);
refresh_memcard(m, 0);
refresh_memcard(m, 1);
// ---- Seed the controller slots: enumerate devices, resolve selections ----
std::vector dev_opts = enumerate_devices();
m.p1_analog = io.has_p1_analog ? io.p1_analog : false;
m.p2_analog = io.has_p2_analog ? io.p2_analog : false;
if (io.has_p1_device) {
m.p1_dev_index = find_or_add_device_index(dev_opts, io.p1_device);
} else {
// Zero-config default: first plugged-in controller, else keyboard.
m.p1_dev_index = (dev_opts.size() > 2) ? 2 : 1;
}
m.p2_dev_index = io.has_p2_device ? find_or_add_device_index(dev_opts, io.p2_device) : 0;
refresh_player(m, 0, dev_opts);
refresh_player(m, 1, dev_opts);
// ---- Data model: bind fields + action callbacks ----
Rml::DataModelConstructor c = context->CreateDataModel("settings");
if (!c) {
Rml::Shutdown();
RmlGL3::Shutdown();
return Result::Unavailable;
}
Rml::String title = game.name ? Rml::String(game.name) : Rml::String("PSX");
c.BindFunc("game_name", [title](Rml::Variant& out) { out = title; });
c.Bind("supersampling", &m.supersampling);
c.Bind("antialiasing", &m.antialiasing);
c.Bind("spu_hq", &m.spu_hq);
c.Bind("renderer_label", &m.renderer_label);
c.Bind("crt_label", &m.crt_label);
c.Bind("aspect_label", &m.aspect_label);
c.Bind("winsize_label", &m.winsize_label);
c.Bind("texfilter_label",&m.texfilter_label);
c.Bind("bios_path", &m.bios_path);
c.Bind("disc_path", &m.disc_path);
c.Bind("disc_file", &m.disc_file);
c.Bind("disc_region", &m.disc_region);
c.Bind("disc_serial", &m.disc_serial);
c.Bind("v_header", &m.v_header);
c.Bind("v_crc", &m.v_crc);
c.Bind("v_verified", &m.v_verified);
c.Bind("verdict_title", &m.verdict_title);
c.Bind("verdict_detail", &m.verdict_detail);
c.Bind("verdict_state", &m.verdict_state);
c.Bind("view", &m.view);
c.Bind("p1_analog", &m.p1_analog);
c.Bind("p2_analog", &m.p2_analog);
c.Bind("p1_dev_label", &m.p1_dev_label);
c.Bind("p2_dev_label", &m.p2_dev_label);
c.Bind("p1_status", &m.p1_status);
c.Bind("p2_status", &m.p2_status);
c.Bind("p1_dot", &m.p1_dot);
c.Bind("p2_dot", &m.p2_dot);
c.Bind("p1_options", &m.p1_options);
c.Bind("p2_options", &m.p2_options);
c.Bind("dd_open", &m.dd_open);
c.Bind("mc1_enabled", &m.mc1_enabled);
c.Bind("mc2_enabled", &m.mc2_enabled);
c.Bind("mc1_name", &m.mc1_name);
c.Bind("mc2_name", &m.mc2_name);
c.Bind("mc1_size", &m.mc1_size);
c.Bind("mc2_size", &m.mc2_size);
c.Bind("mc1_used", &m.mc1_used);
c.Bind("mc2_used", &m.mc2_used);
c.Bind("mc1_foot", &m.mc1_foot);
c.Bind("mc2_foot", &m.mc2_foot);
c.Bind("mc1_grid", &m.mc1_grid);
c.Bind("mc2_grid", &m.mc2_grid);
Rml::DataModelHandle handle = c.GetModelHandle();
c.BindEventCallback("cycle_renderer",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.renderer ^= 1; refresh_labels(m);
handle.DirtyVariable("renderer_label");
});
c.BindEventCallback("cycle_ss",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.supersampling = (m.supersampling % 4) + 1;
handle.DirtyVariable("supersampling");
});
c.BindEventCallback("toggle_aa",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.antialiasing = !m.antialiasing;
handle.DirtyVariable("antialiasing");
});
c.BindEventCallback("cycle_texfilter",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.texture_filter ^= 1; refresh_labels(m);
handle.DirtyVariable("texfilter_label");
});
c.BindEventCallback("cycle_crt",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.crt = (m.crt + 1) % 4; refresh_labels(m);
handle.DirtyVariable("crt_label");
});
c.BindEventCallback("cycle_aspect",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.aspect_index = (m.aspect_index + 1) % kNumAspects; refresh_labels(m);
handle.DirtyVariable("aspect_label");
handle.DirtyVariable("winsize_label"); /* height follows aspect */
});
c.BindEventCallback("cycle_winsize",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
int i = (winsize_index(m.window_width) + 1) % kNumWinWidths;
m.window_width = kWinWidths[i]; refresh_labels(m);
handle.DirtyVariable("winsize_label");
});
c.BindEventCallback("toggle_spu",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.spu_hq = !m.spu_hq;
handle.DirtyVariable("spu_hq");
});
c.BindEventCallback("browse_bios",
[&m, window, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
std::string p = win_pick_file(window, "Select PlayStation BIOS",
"BIOS image (*.bin;*.rom)\0*.bin;*.rom\0All files (*.*)\0*.*\0\0");
if (!p.empty()) {
m.bios_path = fs::path(p).generic_string();
handle.DirtyVariable("bios_path");
}
});
auto do_browse_disc =
[&m, window, handle, game_name_s, expected_serial, expected_crc, has_expected_crc]
(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
std::string p = win_pick_file(window, "Select disc image",
"Disc image (*.cue;*.bin;*.iso)\0*.cue;*.bin;*.iso\0All files (*.*)\0*.*\0\0");
if (!p.empty()) {
m.disc_path = fs::path(p).generic_string();
refresh_disc_status(m, game_name_s, expected_serial, expected_crc, has_expected_crc);
for (const char* v : {"disc_path", "disc_file", "disc_region", "disc_serial",
"v_header", "v_crc", "v_verified",
"verdict_title", "verdict_detail", "verdict_state"})
handle.DirtyVariable(v);
}
};
c.BindEventCallback("browse_disc", do_browse_disc);
c.BindEventCallback("change_iso", do_browse_disc);
c.BindEventCallback("show_settings",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.view = "settings"; handle.DirtyVariable("view");
});
c.BindEventCallback("show_dashboard",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.view = "dashboard"; handle.DirtyVariable("view");
});
// ---- controller: device dropdown + DualShock toggle ----
auto dirty_player = [handle](int player) mutable {
const char* v0[] = {"p1_dev_label","p1_status","p1_dot","p1_options","p1_analog"};
const char* v1[] = {"p2_dev_label","p2_status","p2_dot","p2_options","p2_analog"};
for (const char* v : (player == 0 ? v0 : v1)) handle.DirtyVariable(v);
};
// dev_opts is captured by value: the device list is fixed for the launcher
// session (a hot-plug here would require a re-enumerate, deferred).
c.BindEventCallback("open_dd",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& args) mutable {
const int player = args.empty() ? 0 : (int)args[0].Get();
const char* key = player == 0 ? "p1" : "p2";
m.dd_open = (m.dd_open == key) ? Rml::String() : Rml::String(key);
handle.DirtyVariable("dd_open");
});
c.BindEventCallback("close_dd",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.dd_open = Rml::String(); handle.DirtyVariable("dd_open");
});
c.BindEventCallback("pick_device",
[&m, handle, dev_opts, dirty_player](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& args) mutable {
if (args.size() < 2) return;
const int player = (int)args[0].Get();
const int idx = (int)args[1].Get();
(player == 0 ? m.p1_dev_index : m.p2_dev_index) = idx;
refresh_player(m, player, dev_opts);
m.dd_open = Rml::String();
dirty_player(player);
handle.DirtyVariable("dd_open");
});
c.BindEventCallback("toggle_ds_p1",
[&m, handle, dev_opts, dirty_player](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.p1_analog = !m.p1_analog; refresh_player(m, 0, dev_opts); dirty_player(0);
});
c.BindEventCallback("toggle_ds_p2",
[&m, handle, dev_opts, dirty_player](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.p2_analog = !m.p2_analog; refresh_player(m, 1, dev_opts); dirty_player(1);
});
c.BindEventCallback("toggle_mc1",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.mc1_enabled = !m.mc1_enabled; handle.DirtyVariable("mc1_enabled");
});
c.BindEventCallback("toggle_mc2",
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
m.mc2_enabled = !m.mc2_enabled; handle.DirtyVariable("mc2_enabled");
});
auto dirty_mc = [handle](int slot) mutable {
const char* v0[] = {"mc1_name","mc1_size","mc1_used","mc1_foot","mc1_grid"};
const char* v1[] = {"mc2_name","mc2_size","mc2_used","mc2_foot","mc2_grid"};
for (const char* v : (slot == 0 ? v0 : v1)) handle.DirtyVariable(v);
};
auto browse_mc = [&m, window, dirty_mc](int slot) mutable {
std::string p = win_pick_file(window, "Select memory-card image",
"Memory card (*.mcd;*.mc;*.mcr)\0*.mcd;*.mc;*.mcr\0All files (*.*)\0*.*\0\0");
if (p.empty()) return;
(slot == 0 ? m.mc1_path : m.mc2_path) = fs::path(p).generic_string();
refresh_memcard(m, slot);
dirty_mc(slot);
};
auto new_mc = [&m, window, dirty_mc](int slot) mutable {
Rml::String& cur = (slot == 0 ? m.mc1_path : m.mc2_path);
std::string p = win_pick_save_file(window, "Create new memory card",
"Memory card (*.mcd)\0*.mcd\0All files (*.*)\0*.*\0\0", "mcd",
std::string(cur));
if (p.empty()) return;
if (memcard_format_file(p.c_str()) != 0) return; // I/O failure: leave as-is
cur = fs::path(p).generic_string();
refresh_memcard(m, slot);
dirty_mc(slot);
};
c.BindEventCallback("browse_mc1",
[browse_mc](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable { browse_mc(0); });
c.BindEventCallback("browse_mc2",
[browse_mc](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable { browse_mc(1); });
c.BindEventCallback("new_mc1",
[new_mc](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable { new_mc(0); });
c.BindEventCallback("new_mc2",
[new_mc](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable { new_mc(1); });
c.BindEventCallback("launch",
[&m](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) { m.launch_requested = true; });
c.BindEventCallback("quit",
[&m](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) { m.quit_requested = true; });
// ---- Load the document ----
const fs::path rml = assets / "launcher.rml";
Rml::ElementDocument* doc = context->LoadDocument(rml.generic_string());
if (!doc) {
std::fprintf(stderr, "launcher: failed to load %s — booting without launcher\n",
rml.generic_string().c_str());
Rml::Shutdown();
RmlGL3::Shutdown();
return Result::Unavailable;
}
doc->Show();
// ---- Main loop ----
Result result = Result::Quit;
bool running = true;
while (running) {
SDL_Event ev;
while (SDL_PollEvent(&ev)) {
switch (ev.type) {
case SDL_QUIT:
m.quit_requested = true;
break;
case SDL_WINDOWEVENT:
if (ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
SDL_GL_GetDrawableSize(window, &win_w, &win_h);
render_interface.SetViewport(win_w, win_h);
context->SetDimensions(Rml::Vector2i(win_w, win_h));
}
RmlSDL::InputEventHandler(context, ev);
break;
default:
RmlSDL::InputEventHandler(context, ev);
break;
}
}
if (m.launch_requested) { result = Result::Launch; running = false; }
if (m.quit_requested) { result = Result::Quit; running = false; }
context->Update();
render_interface.Clear();
render_interface.BeginFrame();
context->Render();
render_interface.EndFrame();
SDL_GL_SwapWindow(window);
}
// ---- Commit choices on launch ----
if (result == Result::Launch) {
io.renderer = m.renderer; io.has_renderer = true;
io.supersampling = m.supersampling; io.has_supersampling = true;
io.antialiasing = m.antialiasing; io.has_antialiasing = true;
io.texture_filter = m.texture_filter; io.has_texture_filter = true;
io.screen_kind = m.crt; io.has_screen_kind = true;
io.spu_hq = m.spu_hq; io.has_spu_hq = true;
io.aspect_num = kAspects[m.aspect_index][0];
io.aspect_den = kAspects[m.aspect_index][1];
io.has_aspect_ratio = true;
io.window_width = m.window_width; io.has_window_width = true;
if (!m.bios_path.empty()) { io.bios_path = fs::path(std::string(m.bios_path)); io.has_bios_path = true; }
if (!m.disc_path.empty()) { io.disc_path = fs::path(std::string(m.disc_path)); io.has_disc_path = true; }
io.memcard1_enabled = m.mc1_enabled; io.has_memcard1_enabled = true;
io.memcard2_enabled = m.mc2_enabled; io.has_memcard2_enabled = true;
if (!m.mc1_path.empty()) { io.memcard1_path = fs::path(std::string(m.mc1_path)); io.has_memcard1_path = true; }
if (!m.mc2_path.empty()) { io.memcard2_path = fs::path(std::string(m.mc2_path)); io.has_memcard2_path = true; }
const int i1 = (m.p1_dev_index >= 0 && m.p1_dev_index < (int)dev_opts.size()) ? m.p1_dev_index : 0;
const int i2 = (m.p2_dev_index >= 0 && m.p2_dev_index < (int)dev_opts.size()) ? m.p2_dev_index : 0;
io.p1_device = device_string(dev_opts[i1]); io.has_p1_device = true;
io.p2_device = device_string(dev_opts[i2]); io.has_p2_device = true;
io.p1_analog = m.p1_analog; io.has_p1_analog = true;
io.p2_analog = m.p2_analog; io.has_p2_analog = true;
}
Rml::Shutdown();
RmlGL3::Shutdown();
return result;
}
} // namespace psx_launcher