diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index eb6e0bc8..221ce190 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -485,6 +485,10 @@ UserSettings load_user_settings(const fs::path& path) { const auto n = toml::find(v, "supersampling"); if (n >= 1 && n <= 4) { s.supersampling = (int)n; s.has_supersampling = true; } }); + if (v.contains("window_width")) try_get([&]{ + const auto n = toml::find(v, "window_width"); + if (n >= 640 && n <= 3840) { s.window_width = (int)n; s.has_window_width = true; } + }); if (v.contains("antialiasing")) try_get([&]{ s.antialiasing = toml::find(v, "antialiasing"); s.has_antialiasing = true; }); @@ -584,6 +588,8 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) { f << "renderer = \"" << (s.renderer ? "opengl" : "software") << "\"\n"; if (s.has_supersampling) f << "supersampling = " << s.supersampling << "\n"; + if (s.has_window_width) + f << "window_width = " << s.window_width << "\n"; if (s.has_antialiasing) f << "antialiasing = " << (s.antialiasing ? "true" : "false") << "\n"; if (s.has_texture_filter) diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index b766a492..b6da41a0 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -211,6 +211,9 @@ struct UserSettings { // [video] bool has_renderer = false; int renderer = 0; // 0=software,1=opengl bool has_supersampling = false; int supersampling = 1; // 1..4 + // Window size: width in px; height is always width*3/4 (PSX 4:3). Applies to + // both the launcher and the emulator window so they boot at the same size. + bool has_window_width = false; int window_width = 1280; // -> 1280x960 bool has_antialiasing = false; bool antialiasing = true; bool has_texture_filter = false; int texture_filter = 0; // 0=nearest,1=bilinear bool has_screen_kind = false; int screen_kind = 0; // 0..3 (ScreenKind) diff --git a/runtime/launcher/assets/launcher.rml b/runtime/launcher/assets/launcher.rml index e78329ed..a7b49b63 100644 --- a/runtime/launcher/assets/launcher.rml +++ b/runtime/launcher/assets/launcher.rml @@ -146,6 +146,7 @@ button:hover { background-color: #232d3c; } .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; } .path { flex: 1 1 auto; min-width: 0dp; margin-left: 12dp; color: #7d8590; white-space: nowrap; overflow: hidden; } @@ -300,6 +301,8 @@ button:hover { background-color: #232d3c; }
VIDEO
+
Window size (4:3) +
Renderer
Supersampling diff --git a/runtime/launcher/launcher.cpp b/runtime/launcher/launcher.cpp index 787d540f..7d049273 100644 --- a/runtime/launcher/launcher.cpp +++ b/runtime/launcher/launcher.cpp @@ -99,6 +99,7 @@ struct LauncherModel { int texture_filter = 0; // 0=nearest, 1=bilinear int crt = 0; // 0=raw,1=crt,2=composite,3=trinitron bool spu_hq = false; + int window_width = 1280; // window size (4:3; height = w*3/4) Rml::String bios_path; Rml::String disc_path; @@ -107,6 +108,7 @@ struct LauncherModel { Rml::String renderer_label; Rml::String crt_label; Rml::String texfilter_label; + Rml::String winsize_label; // Disc verification (recomputed whenever disc_path changes). Rml::String disc_file; // file name only, e.g. "tomba.cue" @@ -332,10 +334,29 @@ const char* crt_name(int v) { } } +// Offered 4:3 window widths (height = w*3/4). 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) { + return std::to_string(width) + " \xC3\x97 " + std::to_string(width * 3 / 4); // "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.winsize_label = winsize_label_for(m.window_width); } std::string region_long(const std::string& r) { @@ -546,6 +567,7 @@ Result run(SDL_Window* window, void* gl_context, m.texture_filter = io.texture_filter; m.crt = io.screen_kind; m.spu_hq = io.spu_hq; + 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); @@ -588,6 +610,7 @@ Result run(SDL_Window* window, void* gl_context, c.Bind("spu_hq", &m.spu_hq); c.Bind("renderer_label", &m.renderer_label); c.Bind("crt_label", &m.crt_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); @@ -652,6 +675,12 @@ Result run(SDL_Window* window, void* gl_context, m.crt = (m.crt + 1) % 4; refresh_labels(m); handle.DirtyVariable("crt_label"); }); + 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; @@ -831,6 +860,7 @@ Result run(SDL_Window* window, void* gl_context, 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.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; } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index f783e9f0..fedede8e 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -118,7 +118,23 @@ static bool g_video_aa = true; /* linear present filtering */ static int g_video_texfilter = 0; /* 0=nearest, 1=bilinear */ static int g_video_renderer = 0; /* 0=software, 1=opengl (requested) */ static int g_video_screen = 0; /* 0=raw,1=crt,2=composite,3=trinitron */ +static int g_video_win_w = 1280; /* window width (height = w*3/4, 4:3) */ static bool g_audio_spu_hq = false; /* SPU float-shadow (env overrides) */ + +/* Clamp a requested 4:3 window width to the primary display's usable area so an + * oversized choice (e.g. 1920 on a 1080p panel) still fits on screen. Keeps the + * 4:3 aspect: height = width*3/4. */ +static void clamp_window_4x3(int* w, int* h) { + int width = *w; + if (width < 640) width = 640; + SDL_Rect bounds; + if (SDL_GetDisplayUsableBounds(0, &bounds) == 0 && bounds.w > 0 && bounds.h > 0) { + if (width > bounds.w) width = bounds.w; + if (width * 3 / 4 > bounds.h) width = bounds.h * 4 / 3; + } + *w = width; + *h = width * 3 / 4; +} static bool g_gl_active = false; /* GL context live -> GL present path */ /* Present straight from the FBO (fast, no readback). Set PSX_GL_FORCE_CPU_PRESENT=1 * to force the software readout path instead — a diagnostic/fallback that also @@ -1408,6 +1424,7 @@ int main(int argc, char** argv) { PSXRecompV4::load_user_settings(settings_path); if (us.has_renderer) 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; if (us.has_texture_filter) g_video_texfilter = us.texture_filter; if (us.has_screen_kind) g_video_screen = us.screen_kind; @@ -1460,17 +1477,25 @@ int main(int argc, char** argv) { seed.p2_device = p2_device; seed.has_p2_device = true; seed.p1_analog = p1_analog; seed.has_p1_analog = true; seed.p2_analog = p2_analog; seed.has_p2_analog = true; + seed.window_width = g_video_win_w; seed.has_window_width = true; SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + /* 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_4x3(&lwin_w, &lwin_h); 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, - 1280, 924, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); + 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); @@ -1513,6 +1538,7 @@ int main(int argc, char** argv) { if (seed.has_memcard2_path) memcard2_path = seed.memcard2_path; p1_device = seed.p1_device; p2_device = seed.p2_device; p1_analog = seed.p1_analog; p2_analog = seed.p2_analog; + g_video_win_w = seed.window_width; /* Persist the user's choices next to the exe. */ PSXRecompV4::save_user_settings( exe_dir_from_argv(argv[0]) / "settings.toml", seed); @@ -1662,10 +1688,16 @@ int main(int argc, char** argv) { Uint32 win_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE; if (g_video_renderer == 1) win_flags |= SDL_WINDOW_OPENGL; + /* Open at the user-chosen 4:3 window size (default 1280x960) instead of the + * old hardcoded 640x480, so the game doesn't boot into a tiny window. The + * present path keeps a 640x480 logical space, so the image scales to fill + * the larger window with no aspect distortion. */ + int game_w = g_video_win_w, game_h = 0; + clamp_window_4x3(&game_w, &game_h); sdl_window = SDL_CreateWindow( window_title.c_str(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, - 640, 480, + game_w, game_h, win_flags ); if (!sdl_window) {