mirror of
https://github.com/jessicanataliagta/PSPRecomp
synced 2026-09-26 08:41:08 -04:00
826 lines
36 KiB
C++
826 lines
36 KiB
C++
#include "vcs_config.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cerrno>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <limits>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
#include <string_view>
|
|
|
|
#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 <windows.h>
|
|
#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<char>(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<std::uint32_t>(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<std::uint64_t>(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 == "marchsteps") {
|
|
if (!parse_u32(value, 4u, 64u, config.volumetric_clouds.march_steps))
|
|
warning(config, line_number, "ProperShaders.ini: MarchSteps must be between 4 and 64");
|
|
} else if (key == "coverage") {
|
|
if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.coverage)) bad_float("Coverage");
|
|
} 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, 1.0f, config.volumetric_clouds.speed)) bad_float("Speed");
|
|
} 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::uint32_t>(std::max(320, GetSystemMetrics(SM_CXSCREEN))),
|
|
static_cast<std::uint32_t>(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<std::int32_t>(client_width),
|
|
static_cast<std::int32_t>(client_height)};
|
|
}
|
|
const double horizontal = static_cast<double>(client_width) /
|
|
static_cast<double>(source_width);
|
|
const double vertical = static_cast<double>(client_height) /
|
|
static_cast<double>(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<std::uint32_t>(
|
|
static_cast<std::uint32_t>(std::llround(static_cast<double>(source_width) * scale)),
|
|
1u, client_width);
|
|
const std::uint32_t height = std::clamp<std::uint32_t>(
|
|
static_cast<std::uint32_t>(std::llround(static_cast<double>(source_height) * scale)),
|
|
1u, client_height);
|
|
return PresentationRectangle{
|
|
static_cast<std::int32_t>((client_width - width) / 2u),
|
|
static_cast<std::int32_t>((client_height - height) / 2u),
|
|
static_cast<std::int32_t>(width), static_cast<std::int32_t>(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<std::uint32_t>(
|
|
480u, static_cast<std::uint32_t>(
|
|
(static_cast<std::uint64_t>(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<std::uint32_t>(
|
|
static_cast<std::uint32_t>(
|
|
(static_cast<std::uint64_t>(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::uint32_t>(std::max(480, GetSystemMetrics(SM_CXSCREEN))),
|
|
static_cast<std::uint32_t>(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<unsigned char>(raw_line[0]) == 0xEFu &&
|
|
static_cast<unsigned char>(raw_line[1]) == 0xBBu &&
|
|
static_cast<unsigned char>(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<std::mutex> 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<float>(configuration.widescreen.aspect_x) /
|
|
static_cast<float>(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<float>(surface_width) / static_cast<float>(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
|