#include "vcs_config.hpp" #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif // windows.h defines min/max as macros, which turns std::max(...) into a syntax // error at the GetSystemMetrics call below. Only MSVC builds hit this, so a // Linux-only validation never sees it. #ifndef NOMINMAX #define NOMINMAX #endif #include #endif namespace vcs { namespace { std::string trim_copy(std::string value) { const auto not_space = [](unsigned char ch) { return std::isspace(ch) == 0; }; value.erase(value.begin(), std::find_if(value.begin(), value.end(), not_space)); value.erase(std::find_if(value.rbegin(), value.rend(), not_space).base(), value.end()); return value; } std::string lowercase_copy(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); return value; } std::string strip_inline_comment(std::string value) { bool quoted = false; char quote = '\0'; for (std::size_t index = 0; index < value.size(); ++index) { const char ch = value[index]; if ((ch == '\'' || ch == '"')) { if (!quoted) { quoted = true; quote = ch; } else if (quote == ch) { quoted = false; } continue; } if (!quoted && (ch == ';' || ch == '#')) { value.resize(index); break; } } return trim_copy(std::move(value)); } bool parse_bool(std::string value, bool &out) { value = lowercase_copy(trim_copy(std::move(value))); if (value == "1" || value == "true" || value == "yes" || value == "on") { out = true; return true; } if (value == "0" || value == "false" || value == "no" || value == "off") { out = false; return true; } return false; } bool parse_u32(std::string value, std::uint32_t minimum, std::uint32_t maximum, std::uint32_t &out) { value = trim_copy(std::move(value)); if (value.empty()) return false; errno = 0; char *end = nullptr; const unsigned long parsed = std::strtoul(value.c_str(), &end, 10); if (errno == ERANGE || end == value.c_str() || *end != '\0' || parsed < minimum || parsed > maximum) { return false; } out = static_cast(parsed); return true; } bool parse_u64(std::string value, std::uint64_t minimum, std::uint64_t maximum, std::uint64_t &out) { value = trim_copy(std::move(value)); if (value.empty()) return false; errno = 0; char *end = nullptr; const unsigned long long parsed = std::strtoull(value.c_str(), &end, 10); if (errno == ERANGE || end == value.c_str() || *end != '\0' || parsed < minimum || parsed > maximum) { return false; } out = static_cast(parsed); return true; } bool parse_float(std::string value, float minimum, float maximum, float &out) { value = trim_copy(std::move(value)); if (value.empty()) return false; errno = 0; char *end = nullptr; const float parsed = std::strtof(value.c_str(), &end); if (errno == ERANGE || end == value.c_str() || *end != '\0' || !std::isfinite(parsed) || parsed < minimum || parsed > maximum) { return false; } out = parsed; return true; } void warning(VcsConfiguration &config, std::size_t line, const std::string &message) { std::ostringstream stream; stream << "line " << line << ": " << message; config.warnings.push_back(stream.str()); } void load_proper_shaders_configuration(VcsConfiguration &config, const std::filesystem::path &path) { std::ifstream input(path); if (!input) return; std::string section; std::string raw_line; std::size_t line_number = 0u; while (std::getline(input, raw_line)) { ++line_number; std::string line = trim_copy(raw_line); if (line.empty() || line[0] == ';' || line[0] == '#') continue; if (line.front() == '[' && line.back() == ']') { section = lowercase_copy(trim_copy(line.substr(1u, line.size() - 2u))); continue; } if (section != "volumetricclouds" && section != "volumetric clouds") continue; const std::size_t separator = line.find('='); if (separator == std::string::npos) { warning(config, line_number, "ProperShaders.ini: expected key=value"); continue; } const std::string key = lowercase_copy(trim_copy(line.substr(0u, separator))); const std::string value = strip_inline_comment(line.substr(separator + 1u)); auto bad_float = [&](const char *name) { warning(config, line_number, std::string("ProperShaders.ini: invalid ") + name); }; if (key == "enabled") { if (!parse_bool(value, config.volumetric_clouds.enabled)) warning(config, line_number, "ProperShaders.ini: Enabled expects true/false"); } else if (key == "downscalediv") { if (!parse_u32(value, 1u, 8u, config.volumetric_clouds.downscale_div)) warning(config, line_number, "ProperShaders.ini: DownscaleDiv must be between 1 and 8"); } else if (key == "layers") { if (!parse_u32(value, 1u, 3u, config.volumetric_clouds.layers)) warning(config, line_number, "ProperShaders.ini: Layers must be between 1 and 3"); } else if (key == "shadowsteps") { if (!parse_u32(value, 2u, 8u, config.volumetric_clouds.shadow_steps)) warning(config, line_number, "ProperShaders.ini: ShadowSteps must be between 2 and 8"); } else if (key == "coveragelow") { if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.coverage_low)) bad_float("CoverageLow"); } else if (key == "coveragemid") { if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.coverage_mid)) bad_float("CoverageMid"); } else if (key == "coveragehigh") { if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.coverage_high)) bad_float("CoverageHigh"); } else if (key == "opacity") { if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.opacity)) bad_float("Opacity"); } else if (key == "speed") { if (!parse_float(value, 0.0f, 1000.0f, config.volumetric_clouds.speed)) bad_float("Speed"); } else if (key == "brightness") { if (!parse_float(value, 0.0f, 8.0f, config.volumetric_clouds.brightness)) bad_float("Brightness"); } else if (key == "randomseed") { if (!parse_float(value, 0.0f, 6.2831855f, config.volumetric_clouds.random_seed)) bad_float("RandomSeed"); } else if (key == "sundirectionx") { if (!parse_float(value, -1.0f, 1.0f, config.volumetric_clouds.sun_direction_x)) bad_float("SunDirectionX"); } else if (key == "sundirectiony") { if (!parse_float(value, -1.0f, 1.0f, config.volumetric_clouds.sun_direction_y)) bad_float("SunDirectionY"); } else if (key == "sundirectionz") { if (!parse_float(value, -1.0f, 1.0f, config.volumetric_clouds.sun_direction_z)) bad_float("SunDirectionZ"); } else if (key == "suncolorr") { if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.sun_color_r)) bad_float("SunColorR"); } else if (key == "suncolorg") { if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.sun_color_g)) bad_float("SunColorG"); } else if (key == "suncolorb") { if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.sun_color_b)) bad_float("SunColorB"); } else if (key == "cloudbasecolorr") { if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.cloud_base_color_r)) bad_float("CloudBaseColorR"); } else if (key == "cloudbasecolorg") { if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.cloud_base_color_g)) bad_float("CloudBaseColorG"); } else if (key == "cloudbasecolorb") { if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.cloud_base_color_b)) bad_float("CloudBaseColorB"); } else if (key == "atmospheredensity") { if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.atmosphere_density)) bad_float("AtmosphereDensity"); } else if (key == "mist") { if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.mist)) bad_float("Mist"); } else if (key == "fogcolorr") { if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.fog_color_r)) bad_float("FogColorR"); } else if (key == "fogcolorg") { if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.fog_color_g)) bad_float("FogColorG"); } else if (key == "fogcolorb") { if (!parse_float(value, 0.0f, 4.0f, config.volumetric_clouds.fog_color_b)) bad_float("FogColorB"); } else if (key == "fogstart") { if (!parse_float(value, 1.0f, 100000.0f, config.volumetric_clouds.fog_start)) bad_float("FogStart"); } else if (key == "dayprogression") { if (!parse_float(value, -1.0f, 1.0f, config.volumetric_clouds.day_progression)) bad_float("DayProgression"); } else if (key == "temporalblend") { if (!parse_float(value, 0.0f, 0.95f, config.volumetric_clouds.temporal_blend)) bad_float("TemporalBlend"); } else if (key == "temporaldenoise") { if (!parse_float(value, 0.0f, 16.0f, config.volumetric_clouds.temporal_denoise)) bad_float("TemporalDenoise"); } else if (key == "temporalclamp") { if (!parse_float(value, 0.0f, 16.0f, config.volumetric_clouds.temporal_clamp)) bad_float("TemporalClamp"); } else { warning(config, line_number, "ProperShaders.ini: unknown [VolumetricClouds] key '" + key + "'"); } } } void apply_display_key(VcsConfiguration &config, const std::string &key, const std::string &value, std::size_t line) { if (key == "enabled") { if (!parse_bool(value, config.display.enabled)) warning(config, line, "Display.Enabled expects true/false"); return; } if (key == "resolutionmode" || key == "resolution") { const std::string mode = lowercase_copy(trim_copy(value)); if (mode == "psp" || mode == "pspnative" || mode == "nativepsp" || mode == "480x272") config.display.resolution_mode = DisplayResolutionMode::PspNative; else if (mode == "custom") config.display.resolution_mode = DisplayResolutionMode::Custom; else if (mode == "desktop" || mode == "native" || mode == "monitor") config.display.resolution_mode = DisplayResolutionMode::Desktop; else warning(config, line, "Display.ResolutionMode expects PSP, Custom or Desktop"); return; } if (key == "width") { if (!parse_u32(value, 320u, 16384u, config.display.custom_width)) warning(config, line, "Display.Width must be between 320 and 16384"); return; } if (key == "height") { if (!parse_u32(value, 180u, 16384u, config.display.custom_height)) warning(config, line, "Display.Height must be between 180 and 16384"); return; } if (key == "fullscreen" || key == "borderlessfullscreen") { if (!parse_bool(value, config.display.fullscreen)) warning(config, line, "Display.Fullscreen expects true/false"); return; } if (key == "aspectratio" || key == "aspectmode") { const std::string aspect = lowercase_copy(trim_copy(value)); if (aspect == "preserve" || aspect == "keep" || aspect == "letterbox") config.display.aspect_mode = DisplayAspectMode::Preserve; else if (aspect == "stretch" || aspect == "fill") config.display.aspect_mode = DisplayAspectMode::Stretch; else warning(config, line, "Display.AspectRatio expects Preserve or Stretch"); return; } if (key == "upscalefilter" || key == "filter") { const std::string filter = lowercase_copy(trim_copy(value)); if (filter == "nearest" || filter == "point" || filter == "pixel") config.display.upscale_filter = DisplayUpscaleFilter::Nearest; else if (filter == "bilinear" || filter == "linear" || filter == "smooth") config.display.upscale_filter = DisplayUpscaleFilter::Bilinear; else warning(config, line, "Display.UpscaleFilter expects Nearest or Bilinear"); return; } if (key == "integerscale") { if (!parse_bool(value, config.display.integer_scale)) warning(config, line, "Display.IntegerScale expects true/false"); return; } if (key == "showfps" || key == "fpscounter") { if (!parse_bool(value, config.display.show_fps)) warning(config, line, "Display.ShowFPS expects true/false"); return; } warning(config, line, "unknown [Display] key '" + key + "'"); } void apply_rendering_key(VcsConfiguration &config, const std::string &key, const std::string &value, std::size_t line) { if (key == "backend" || key == "renderingbackend") { const std::string backend = lowercase_copy(trim_copy(value)); if (backend == "software" || backend == "cpu") config.rendering.backend = RenderingBackend::Software; else if (backend == "directx12" || backend == "dx12" || backend == "d3d12" || backend == "gpu") config.rendering.backend = RenderingBackend::DirectX12; else if (backend == "vulkan") { // Stage 44 removes Vulkan from the Windows runtime. Keep the old // spelling as a migration alias so an existing INI does not silently // fall back to the CPU renderer. config.rendering.backend = RenderingBackend::DirectX12; warning(config, line, "Rendering.Backend=Vulkan is deprecated; using DirectX12"); } else warning(config, line, "Rendering.Backend expects Software or DirectX12"); return; } if (key == "internalresolutionmode" || key == "internalmode") { const std::string mode = lowercase_copy(trim_copy(value)); if (mode == "psp" || mode == "pspnative" || mode == "480x272") config.rendering.internal_resolution_mode = InternalResolutionMode::PspNative; else if (mode == "scale" || mode == "scaled") config.rendering.internal_resolution_mode = InternalResolutionMode::Scale; else if (mode == "custom") config.rendering.internal_resolution_mode = InternalResolutionMode::Custom; else if (mode == "desktop" || mode == "native" || mode == "monitor") config.rendering.internal_resolution_mode = InternalResolutionMode::Desktop; else warning(config, line, "Rendering.InternalResolutionMode expects PSP, Scale, Custom or Desktop"); return; } if (key == "internalscale" || key == "scale") { if (!parse_u32(value, 1u, 8u, config.rendering.internal_scale)) warning(config, line, "Rendering.InternalScale must be between 1 and 8"); return; } if (key == "msaa" || key == "multisampling") { std::uint32_t samples = 0u; if (!parse_u32(value, 1u, 16u, samples)) { warning(config, line, "Rendering.MSAA expects 1, 2, 4, 8 or 16"); return; } if (samples != 1u && samples != 2u && samples != 4u && samples != 8u && samples != 16u) { warning(config, line, "Rendering.MSAA expects 1, 2, 4, 8 or 16"); return; } config.rendering.msaa = samples; return; } if (key == "hardwaretransform") { if (!parse_bool(value, config.rendering.hardware_transform)) warning(config, line, "Rendering.HardwareTransform expects true/false"); return; } if (key == "dx12gecolor" || key == "directx12gecolor" || key == "nativege") { if (!parse_bool(value, config.rendering.dx12_ge_color)) warning(config, line, "Rendering.DX12GEColor expects true/false"); return; } if (key == "texturecacheentries" || key == "texturecachelimit") { if (!parse_u32(value, 256u, 32768u, config.rendering.texture_cache_entries)) warning(config, line, "Rendering.TextureCacheEntries must be between 256 and 32768"); return; } if (key == "texturecachemb" || key == "texturecachememorymb") { if (!parse_u32(value, 32u, 2048u, config.rendering.texture_cache_mb)) warning(config, line, "Rendering.TextureCacheMB must be between 32 and 2048"); return; } if (key == "depthprecision" || key == "depthbits") { std::uint32_t bits = 0u; if (!parse_u32(value, 16u, 32u, bits) || (bits != 16u && bits != 24u && bits != 32u)) { warning(config, line, "Rendering.DepthPrecision expects 16, 24 or 32"); return; } config.rendering.depth_precision = bits; return; } if (key == "smaa" || key == "antialiasing") { if (!parse_bool(value, config.rendering.smaa)) warning(config, line, "Rendering.SMAA expects true/false"); return; } if (key == "anisotropicfiltering" || key == "anisotropy") { if (!parse_u32(value, 1u, 16u, config.rendering.anisotropic_filtering)) warning(config, line, "Rendering.AnisotropicFiltering must be between 1 and 16"); return; } if (key == "internalwidth") { if (!parse_u32(value, 480u, 16384u, config.rendering.internal_width)) warning(config, line, "Rendering.InternalWidth must be between 480 and 16384"); return; } if (key == "internalheight") { if (!parse_u32(value, 272u, 16384u, config.rendering.internal_height)) warning(config, line, "Rendering.InternalHeight must be between 272 and 16384"); return; } if (key == "experimentalgpucolorpreview" || key == "gpucolorpreview") { if (!parse_bool(value, config.rendering.experimental_gpu_color_preview)) warning(config, line, "Rendering.ExperimentalGpuColorPreview expects true/false"); return; } if (key == "geometrydebugcolors" || key == "gpugeometrydebugcolors") { if (!parse_bool(value, config.rendering.gpu_geometry_debug_colors)) warning(config, line, "Rendering.GeometryDebugColors expects true/false"); return; } if (key == "dumpgpuframevblank" || key == "dumpinternalframevblank") { if (!parse_u64(value, 0u, 1000000000u, config.rendering.dump_gpu_frame_vblank)) warning(config, line, "Rendering.DumpGpuFrameVblank must be between 0 and 1000000000"); return; } warning(config, line, "unknown [Rendering] key '" + key + "'"); } void apply_audio_key(VcsConfiguration &config, const std::string &key, const std::string &value, std::size_t line) { if (key == "enabled") { if (!parse_bool(value, config.audio.enabled)) warning(config, line, "Audio.Enabled expects true/false"); return; } if (key == "volume") { if (!parse_u32(value, 0u, 400u, config.audio.volume)) warning(config, line, "Audio.Volume must be between 0 and 400 (percent)"); return; } if (key == "diagnostics" || key == "performancelog") { if (!parse_bool(value, config.audio.diagnostics)) warning(config, line, "Audio.Diagnostics expects true/false"); return; } if (key == "prebufferblocks") { if (!parse_u32(value, 2u, 22u, config.audio.prebuffer_blocks)) warning(config, line, "Audio.PrebufferBlocks must be between 2 and 22"); return; } if (key == "recoveryprebufferblocks") { if (!parse_u32(value, 2u, 22u, config.audio.recovery_prebuffer_blocks)) warning(config, line, "Audio.RecoveryPrebufferBlocks must be between 2 and 22"); return; } warning(config, line, "unknown [Audio] key '" + key + "'"); } // Accepts "auto" or "x:y" (e.g. 21:9), matching the ForceAspectRatio key of // ThirteenAG's plugin so a ratio copied from his INI works here unchanged. bool parse_aspect_ratio(const std::string &value, std::uint32_t &x, std::uint32_t &y) { if (lowercase_copy(trim_copy(value)) == "auto") { x = 0u; y = 0u; return true; } const std::size_t separator = value.find(':'); if (separator == std::string::npos) return false; std::uint32_t parsed_x = 0u; std::uint32_t parsed_y = 0u; if (!parse_u32(value.substr(0u, separator), 1u, 1000u, parsed_x)) return false; if (!parse_u32(value.substr(separator + 1u), 1u, 1000u, parsed_y)) return false; x = parsed_x; y = parsed_y; return true; } void apply_controls_key(VcsConfiguration &config, const std::string &key, const std::string &value, std::size_t line) { if (key == "camerastick" || key == "mousecamera") { if (!parse_bool(value, config.controls.camera_stick)) warning(config, line, "Controls.CameraStick expects true/false"); return; } if (key == "mousesensitivity") { if (!parse_u32(value, 1u, 100u, config.controls.mouse_sensitivity)) warning(config, line, "Controls.MouseSensitivity must be between 1 and 100"); return; } if (key == "invertcameray") { if (!parse_bool(value, config.controls.invert_camera_y)) warning(config, line, "Controls.InvertCameraY expects true/false"); return; } if (key == "pedcamerauplimitdegrees" || key == "pedcamerauplimit") { if (!parse_u32(value, 10u, 85u, config.controls.ped_camera_up_limit_degrees)) warning(config, line, "Controls.PedCameraUpLimitDegrees must be between 10 and 85"); return; } if (key == "moderncontrolscheme") { if (!parse_bool(value, config.controls.modern_control_scheme)) warning(config, line, "Controls.ModernControlScheme expects true/false"); return; } warning(config, line, "unknown [Controls] key '" + key + "'"); } void apply_widescreen_key(VcsConfiguration &config, const std::string &key, const std::string &value, std::size_t line) { if (key == "enabled") { if (!parse_bool(value, config.widescreen.enabled)) warning(config, line, "Widescreen.Enabled expects true/false"); return; } if (key == "aspectratio" || key == "forceaspectratio") { if (!parse_aspect_ratio(value, config.widescreen.aspect_x, config.widescreen.aspect_y)) warning(config, line, "Widescreen.AspectRatio expects 'auto' or 'x:y' (e.g. 21:9)"); return; } warning(config, line, "unknown [Widescreen] key '" + key + "'"); } void apply_timing_key(VcsConfiguration &config, const std::string &key, const std::string &value, std::size_t line) { if (key == "framerate" || key == "fps" || key == "targetfps") { std::uint32_t frame_rate = 0u; if (!parse_u32(value, 30u, 240u, frame_rate) || (frame_rate != 30u && frame_rate != 60u && frame_rate != 120u && frame_rate != 200u && frame_rate != 240u)) { warning(config, line, "Timing.FrameRate expects 30, 60, 120, 200 or 240"); return; } config.timing.frame_rate = frame_rate; return; } if (key == "realtimespeeddiagnostics" || key == "showspeeddiagnostics") { if (!parse_bool(value, config.timing.realtime_speed_diagnostics)) warning(config, line, "Timing.RealtimeSpeedDiagnostics expects true/false"); return; } if (key == "realtimespeedintervalvblanks" || key == "speedintervalvblanks") { if (!parse_u64(value, 1u, 36000u, config.timing.realtime_speed_interval_vblanks)) warning(config, line, "Timing.RealtimeSpeedIntervalVblanks must be between 1 and 36000"); return; } warning(config, line, "unknown [Timing] key '" + key + "'"); } void apply_diagnostics_key(VcsConfiguration &config, const std::string &key, const std::string &value, std::size_t line) { if (key == "logtofile" || key == "enablelog" || key == "enabled") { if (!parse_bool(value, config.diagnostics.log_to_file)) warning(config, line, "Diagnostics.LogToFile expects true/false"); return; } if (key == "logfile" || key == "filename") { const std::string trimmed = trim_copy(value); if (trimmed.empty()) warning(config, line, "Diagnostics.LogFile must not be empty"); else config.diagnostics.log_file = trimmed; return; } if (key == "flusheveryline" || key == "autoflush") { if (!parse_bool(value, config.diagnostics.flush_every_line)) warning(config, line, "Diagnostics.FlushEveryLine expects true/false"); return; } warning(config, line, "unknown [Diagnostics] key '" + key + "'"); } VcsConfiguration &global_configuration() { static VcsConfiguration config; return config; } std::mutex &global_configuration_mutex() { static std::mutex mutex; return mutex; } void set_environment_value(const char *name, const std::string &value) { #if defined(_WIN32) _putenv_s(name, value.c_str()); #else setenv(name, value.c_str(), 1); #endif } } // namespace DisplaySurfaceDimensions resolve_display_surface_dimensions( const DisplayConfiguration &configuration) noexcept { switch (configuration.resolution_mode) { case DisplayResolutionMode::PspNative: return {480u, 272u}; case DisplayResolutionMode::Custom: return {std::clamp(configuration.custom_width, 320u, 16384u), std::clamp(configuration.custom_height, 180u, 16384u)}; case DisplayResolutionMode::Desktop: #if defined(_WIN32) return {static_cast(std::max(320, GetSystemMetrics(SM_CXSCREEN))), static_cast(std::max(180, GetSystemMetrics(SM_CYSCREEN)))}; #else return {std::clamp(configuration.custom_width, 320u, 16384u), std::clamp(configuration.custom_height, 180u, 16384u)}; #endif } return {480u, 272u}; } PresentationRectangle calculate_presentation_rectangle( std::uint32_t client_width, std::uint32_t client_height, std::uint32_t source_width, std::uint32_t source_height, DisplayAspectMode aspect_mode, bool integer_scale) noexcept { if (client_width == 0u || client_height == 0u) return {}; if (aspect_mode == DisplayAspectMode::Stretch || source_width == 0u || source_height == 0u) { return PresentationRectangle{0, 0, static_cast(client_width), static_cast(client_height)}; } const double horizontal = static_cast(client_width) / static_cast(source_width); const double vertical = static_cast(client_height) / static_cast(source_height); double scale = std::min(horizontal, vertical); if (integer_scale && scale >= 1.0) scale = std::max(1.0, std::floor(scale)); const std::uint32_t width = std::clamp( static_cast(std::llround(static_cast(source_width) * scale)), 1u, client_width); const std::uint32_t height = std::clamp( static_cast(std::llround(static_cast(source_height) * scale)), 1u, client_height); return PresentationRectangle{ static_cast((client_width - width) / 2u), static_cast((client_height - height) / 2u), static_cast(width), static_cast(height)}; } // Largest 480:272 target that fits inside the monitor. Everything the GE // produces -- world geometry and every through-mode HUD rectangle alike -- is // normalized by the 480x272 reference before it reaches the native GPU backend, so the internal // target *is* the PSP screen. Handing it the raw ultrawide desktop stretched // that screen to 21:9: the radar became an ellipse and the elements anchored to // the right/bottom edges were pushed past the visible area. Keeping the PSP // aspect and letting the presentation layer letterbox scales the HUD with the // user's resolution instead of distorting it. [[nodiscard]] static InternalResolutionDimensions fit_psp_aspect( std::uint32_t monitor_width, std::uint32_t monitor_height) noexcept { const std::uint32_t width_from_height = std::max( 480u, static_cast( (static_cast(monitor_height) * 480ull + 136ull) / 272ull)); const std::uint32_t width = std::clamp( std::min(std::max(monitor_width, 480u), width_from_height), 480u, 16384u); const std::uint32_t height = std::clamp( static_cast( (static_cast(width) * 272ull + 240ull) / 480ull), 272u, 16384u); return {width, height}; } InternalResolutionDimensions resolve_internal_resolution( const RenderingConfiguration &configuration) noexcept { switch (configuration.internal_resolution_mode) { case InternalResolutionMode::PspNative: return {480u, 272u}; case InternalResolutionMode::Scale: { const std::uint32_t scale = std::clamp(configuration.internal_scale, 1u, 8u); return {480u * scale, 272u * scale}; } case InternalResolutionMode::Custom: return {std::clamp(configuration.internal_width, 480u, 16384u), std::clamp(configuration.internal_height, 272u, 16384u)}; case InternalResolutionMode::Desktop: #if defined(_WIN32) // The monitor's real pixel count, ultrawide included. Fitting the PSP // 480:272 aspect here would render a pillarboxed image on a 21:9 panel, // which is not what "desktop resolution" is asked for. return {static_cast(std::max(480, GetSystemMetrics(SM_CXSCREEN))), static_cast(std::max(272, GetSystemMetrics(SM_CYSCREEN)))}; #else // Headless/non-Windows builds cannot query a monitor here. Custom // values provide a deterministic fallback for CI and proof tooling. return {std::clamp(configuration.internal_width, 480u, 16384u), std::clamp(configuration.internal_height, 272u, 16384u)}; #endif } return {480u, 272u}; } VcsConfiguration load_vcs_configuration(const std::filesystem::path &path) { VcsConfiguration config; config.source_path = path; std::ifstream input(path); if (!input) return config; config.loaded_from_file = true; std::string section; std::string raw_line; std::size_t line_number = 0u; while (std::getline(input, raw_line)) { ++line_number; if (line_number == 1u && raw_line.size() >= 3u && static_cast(raw_line[0]) == 0xEFu && static_cast(raw_line[1]) == 0xBBu && static_cast(raw_line[2]) == 0xBFu) { raw_line.erase(0u, 3u); } std::string line = trim_copy(raw_line); if (line.empty() || line[0] == ';' || line[0] == '#') continue; if (line.front() == '[' && line.back() == ']') { section = lowercase_copy(trim_copy(line.substr(1u, line.size() - 2u))); continue; } const std::size_t separator = line.find('='); if (separator == std::string::npos) { warning(config, line_number, "expected key=value"); continue; } const std::string key = lowercase_copy(trim_copy(line.substr(0u, separator))); const std::string value = strip_inline_comment(line.substr(separator + 1u)); if (section == "display") apply_display_key(config, key, value, line_number); else if (section == "rendering") apply_rendering_key(config, key, value, line_number); else if (section == "audio") apply_audio_key(config, key, value, line_number); else if (section == "timing") apply_timing_key(config, key, value, line_number); else if (section == "diagnostics" || section == "logging") apply_diagnostics_key(config, key, value, line_number); else if (section == "widescreen") apply_widescreen_key(config, key, value, line_number); else if (section == "controls") apply_controls_key(config, key, value, line_number); // These sections belong to the optional Project2DFX module, which // deliberately owns its parser so it can be compiled independently of // the core display/input configuration. They are nevertheless valid // VCSNative.ini sections and must not be reported as unknown here. else if (section == "project2dfx" || section == "project 2dfx" || section == "lodlights" || section == "lod lights" || section == "trafficlights" || section == "traffic lights" || section == "blinkinglights" || section == "blinking lights" || section == "skygfx" || section == "heliheight" || section == "heli height" || section == "drawdistance" || section == "draw distance" || section == "simulatehdr" || section == "simulate hdr") { // Parsed by install_project2dfx() or hdr_post_configure(). } else if (section.empty()) warning(config, line_number, "key outside a section"); else warning(config, line_number, "unknown section [" + section + "]"); } return config; } void initialize_vcs_configuration(const std::filesystem::path &executable_directory) { std::filesystem::path path; if (const char *override_path = std::getenv("PSPRECOMP_CONFIG"); override_path != nullptr && *override_path != '\0') { path = override_path; } else { path = executable_directory / "VCSNative.ini"; } VcsConfiguration loaded = load_vcs_configuration(path); load_proper_shaders_configuration(loaded, executable_directory / "ProperShaders.ini"); loaded.initialized = true; loaded.executable_directory = executable_directory; { std::lock_guard guard(global_configuration_mutex()); global_configuration() = std::move(loaded); } // Preserve the existing diagnostic implementation while making it // configurable from the same user-facing INI. Explicit environment values // remain authoritative for scripted runs. const VcsConfiguration &config = global_configuration(); if (config.timing.realtime_speed_diagnostics && std::getenv("PSPRECOMP_REALTIME_SPEED_DIAG") == nullptr) { set_environment_value("PSPRECOMP_REALTIME_SPEED_DIAG", "1"); } if (std::getenv("PSPRECOMP_REALTIME_SPEED_INTERVAL") == nullptr) { set_environment_value("PSPRECOMP_REALTIME_SPEED_INTERVAL", std::to_string(config.timing.realtime_speed_interval_vblanks)); } if (std::getenv("PSPRECOMP_GE_BACKEND") == nullptr) { set_environment_value("PSPRECOMP_GE_BACKEND", config.rendering.backend == RenderingBackend::DirectX12 ? "directx12" : "software"); } const InternalResolutionDimensions internal = resolve_internal_resolution(config.rendering); if (std::getenv("PSPRECOMP_INTERNAL_WIDTH") == nullptr) set_environment_value("PSPRECOMP_INTERNAL_WIDTH", std::to_string(internal.width)); if (std::getenv("PSPRECOMP_INTERNAL_HEIGHT") == nullptr) set_environment_value("PSPRECOMP_INTERNAL_HEIGHT", std::to_string(internal.height)); if (std::getenv("PSPRECOMP_GE_GPU_TEXTURE_DECODE_LIMIT") == nullptr) set_environment_value("PSPRECOMP_GE_GPU_TEXTURE_DECODE_LIMIT", std::to_string(config.rendering.texture_cache_entries)); if (std::getenv("PSPRECOMP_GE_GPU_TEXTURE_CACHE_MB") == nullptr) set_environment_value("PSPRECOMP_GE_GPU_TEXTURE_CACHE_MB", std::to_string(config.rendering.texture_cache_mb)); if (config.rendering.experimental_gpu_color_preview && std::getenv("PSPRECOMP_GE_GPU_COLOR_PREVIEW") == nullptr) { set_environment_value("PSPRECOMP_GE_GPU_COLOR_PREVIEW", "1"); } if (config.rendering.gpu_geometry_debug_colors && std::getenv("PSPRECOMP_GE_GPU_GEOMETRY_DEBUG_COLORS") == nullptr) { set_environment_value("PSPRECOMP_GE_GPU_GEOMETRY_DEBUG_COLORS", "1"); } if (config.rendering.dump_gpu_frame_vblank != 0u && std::getenv("PSPRECOMP_GE_GPU_DUMP_VBLANK") == nullptr) { set_environment_value("PSPRECOMP_GE_GPU_DUMP_VBLANK", std::to_string(config.rendering.dump_gpu_frame_vblank)); } } const VcsConfiguration &vcs_configuration() { return global_configuration(); } const char *display_resolution_mode_name(DisplayResolutionMode mode) noexcept { switch (mode) { case DisplayResolutionMode::PspNative: return "PSP"; case DisplayResolutionMode::Custom: return "Custom"; case DisplayResolutionMode::Desktop: return "Desktop"; } return "Unknown"; } const char *display_aspect_mode_name(DisplayAspectMode mode) noexcept { switch (mode) { case DisplayAspectMode::Preserve: return "Preserve"; case DisplayAspectMode::Stretch: return "Stretch"; } return "Unknown"; } const char *display_upscale_filter_name(DisplayUpscaleFilter filter) noexcept { switch (filter) { case DisplayUpscaleFilter::Nearest: return "Nearest"; case DisplayUpscaleFilter::Bilinear: return "Bilinear"; } return "Unknown"; } const char *internal_resolution_mode_name(InternalResolutionMode mode) noexcept { switch (mode) { case InternalResolutionMode::PspNative: return "PSP"; case InternalResolutionMode::Scale: return "Scale"; case InternalResolutionMode::Custom: return "Custom"; case InternalResolutionMode::Desktop: return "Desktop"; } return "Unknown"; } const char *rendering_backend_name(RenderingBackend backend) noexcept { switch (backend) { case RenderingBackend::Software: return "Software"; case RenderingBackend::DirectX12: return "DirectX 12"; } return "Unknown"; } float resolve_widescreen_aspect_ratio(const VcsConfiguration &configuration, std::uint32_t surface_width, std::uint32_t surface_height) noexcept { if (!configuration.widescreen.enabled) return 0.0f; if (configuration.widescreen.aspect_x != 0u && configuration.widescreen.aspect_y != 0u) { return static_cast(configuration.widescreen.aspect_x) / static_cast(configuration.widescreen.aspect_y); } // "auto": the ratio of the surface the game is presented on. ThirteenAG's // plugin asks the emulator for this through a devctl; here the host owns // the surface, so it is known directly. if (surface_width == 0u || surface_height == 0u) return 0.0f; return static_cast(surface_width) / static_cast(surface_height); } float widescreen_stretch_factor(const VcsConfiguration &configuration, std::uint32_t surface_width, std::uint32_t surface_height) noexcept { const float aspect = resolve_widescreen_aspect_ratio(configuration, surface_width, surface_height); if (!(aspect > 0.0f)) return 1.0f; const float factor = aspect / kGameNativeAspectRatio; if (!std::isfinite(factor) || factor <= 0.0f) return 1.0f; // A ratio far from the game's own is a configuration mistake, not a wish. // Clamping keeps a typo in ForceAspectRatio from collapsing the interface // into a sliver or pushing it entirely off screen. return std::clamp(factor, 0.5f, 4.0f); } } // namespace vcs