Files
ARMSX1/frontend/config.c

1594 lines
61 KiB
C

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
#include <ctype.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#ifdef _WIN32
#include <direct.h>
#endif
#include "config.h"
#include "gpu_profile.h"
#include "pgo.h"
#include "../psx/fastboot.h"
#include "../psx/psx.h"
#include "../psx/dev/cdrom/cdrom.h"
#include "common.h"
#include "render.h"
/* Control surface only — this TU calls armsx_shader_set_source_scale() and nothing else,
and has no business pulling <vulkan/vulkan.h> in to do it. */
#define ARMSX_RENDER_SHADERS_NO_VK
#include "render_shaders.h"
#include "../psx/log.h"
#include "SDL.h"
static char* g_pref_path = NULL;
static char* g_settings_path = NULL;
static char* g_bios_path = NULL;
static const char* g_version_text =
#ifdef _WIN32
"armsx.exe (" STR(OS_INFO) ") " STR(REP_VERSION) "-" STR(REP_COMMIT_HASH) "\n"
#elif __linux__
"armsx (" STR(OS_INFO) ") " STR(REP_VERSION) "-" STR(REP_COMMIT_HASH) "\n"
#else
"armsx (" STR(OS_INFO) ") " STR(REP_VERSION) "-" STR(REP_COMMIT_HASH) "\n"
#endif
"\nARMSX - A simple, fast and portable Sony PlayStation emulator.\n\n"
"Copyright (C) 2023 Allkern (Lisandro Alarcon)\n"
"This is free software; see the source for copying conditions. There is NO\n"
"warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n";
static const char g_default_settings[] =
"# Settings file generated by ARMSX\n"
"\n"
"# Don't change please, reserved for future use\n"
"psxe_version = \"" STR(REP_VERSION) "\"\n"
"\n"
"# BIOS-related settings:\n"
"[bios]\n"
" search_path = \"bios\"\n"
" preferred_model = \"SCPH-1001\"\n"
" override_file = \"\"\n"
"\n"
"# Console settings\n"
"# CD-ROM drive\n"
"[cdrom]\n"
" read_speedup = 1 # 1 = authentic. >1 shortens loads; CAN break FMV/XA audio\n"
" seek_speedup = 1 # 1 = authentic. >1 shortens seeks; safer than read_speedup\n"
"\n"
"[console]\n"
" region = \"auto\"\n"
"\n"
"# CPU settings\n"
"[cpu]\n"
" execution_mode = \"cached\" # cached | interpreter\n"
" fast_boot = false # skip the BIOS startup animation and boot the game\n"
" overclock = 100 # 10..1000 %; >100 lifts games that dip. Can upset timing\n"
"\n"
"# Runtime settings\n"
"[runtime]\n"
" display_scale = 3\n"
" logging_enabled = false\n"
" log_level = 2\n"
" quiet = true\n"
" frame_limit = true # false runs unthrottled\n"
" speed_percent = 100 # 10..1000; % of the game's own frame rate\n"
" fps_limit = 0 # extra absolute cap in fps, 0 = off\n"
" fast_forward_speed = 2.0 # 1.0..16.0, or 0 for uncapped\n"
" frame_skip = 0 # drop PRESENTS, never emulation: 0 off, 1..5 fixed"
" (present 1 of every N+1), -1 adaptive\n"
" adpf_clock_hint = false # EXPERIMENTAL. Android 13+: report per-frame CPU work to the"
" performance-hint service\n"
" affinity_mode = 0 # EXPERIMENTAL. 0 scheduler decides, 1 pin emulation to the"
" performance cores, 2 all cores\n"
"\n"
"# Launch path defaults\n"
"[paths]\n"
" expansion_rom = \"\"\n"
" default_psx_exe = \"\"\n"
"\n"
"# Video settings\n"
"[video]\n"
" vsync = "
#if defined(UWP_TARGET)
"false"
#else
"true"
#endif
"\n"
#ifdef USE_HARDWARE
" gpu_backend = \"software\" # software | sdl-accelerated | opengl | angle | vulkan\n"
" gl_driver = \"system\" # GLES implementation for the opengl backend: system | angle\n"
" renderer = \"software\" # rasterizer: software | hardware | hardware-cpu | hardware-gl\n"
" internal_scale = 1 # 1..8; internal resolution, hardware rasterizer only\n"
" gpu_profile = \"auto\" # auto | adreno | mali | powervr | xclipse\n"
" force_fbfetch = \"auto\" # auto | on | off; overrule the GPU profile's fbfetch default\n"
#endif
" accurate_mask_bit = true # GP0(E6) mask bit, as real hardware. Free for games that never set E6\n"
" accurate_dither = true # dither only when the game enables it (GPUSTAT bit 9), as real hardware\n"
" accurate_prim_size = true # drop polygons bigger than 1023x511, as real hardware does\n"
" accurate_tex_modulate = true # truncate texture blending like hardware (fades feedback trails)\n"
" pgxp = false # PGXP: sub-pixel polygon precision captured from the GTE\n"
" widescreen_hack = false # scale the GTE's X projection so 3D geometry fills 16:9\n"
" texture_filter = \"nearest\" # nearest | bilinear | xbr; hardware (GLES) rasterizer only\n"
" downsample = 0 # 0 = off, 2..8: render high, then box-average by this factor\n"
" deinterlace = \"weave\" # weave | bob | adaptive; only affects 480-line (interlaced) modes\n"
" overscan_crop = \"none\" # none | small | full; trim the blanking border games leave\n"
" display_rotation = 0 # 0 | 90 | 180 | 270 degrees, clockwise\n"
" texture_dump = false # write every sampled texture to <texture_dir>/dump as PNG\n"
" texture_replacements = false # load PNG replacements from <texture_dir>/replacements\n"
" texture_dir = \"\" # base folder for both; empty = <prefs>/textures\n"
" line_detect = \"disabled\" # disabled | quads | basic; widen 1px polygons used as lines\n"
" texture_scale_mode = false\n"
" debug_panel = false\n"
" stretch_mode = false\n"
" display_aspect = \"classic\" # classic | square | wide16x9 | custom\n"
" display_aspect_custom = 1.7777 # width/height, used when display_aspect = \"custom\"\n"
" integer_scaling = false # snap output to a whole multiple of the source resolution\n"
" wide_upscale = \"480p\" # 480p | 720p | 1080p | 1440p | 2160p\n"
"\n"
"# Controller settings\n"
"[input]\n"
" # Mode the emulated DualShock reports at boot. Real hardware boots digital and the\n"
" # player presses the pad's ANALOG button; that button has no standard keycode on a\n"
" # phone/handheld, so booting analog is the sane default. The ANALOG button still\n"
" # toggles at runtime either way.\n"
" analog_mode_default = false\n"
" # Multitap in port 1: four players share one controller port. OFF by default and\n"
" # deliberately so — a tap answers the pad poll with its own ID (80h), and a game\n"
" # that does not understand multitaps reads that as NO controller at all.\n"
" multitap = false\n"
"\n"
"# Rewind and runahead. Both OFF by default; see psx/rewind.h.\n"
"[emulation]\n"
" # Rewind keeps a ring of save states in RAM. seconds x frequency IS a memory\n"
" # figure: a snapshot is ~3.5 MiB, so 10 s at 2/s is ~70 MiB and 60 s at 4/s is\n"
" # ~840 MiB. Nothing is allocated while rewind = false.\n"
" rewind = false\n"
" rewind_seconds = 10 # 1..60\n"
" rewind_frequency = 2 # 1..8 snapshots per second\n"
" # Runahead hides input latency by re-simulating N frames every frame. Costs a\n"
" # state save + load AND N extra emulated frames per frame — brutal on mobile.\n"
" runahead = 0 # 0 = off, 1..5 frames\n"
"\n"
"# Audio settings\n"
"[audio]\n"
" volume = 100 # 0..200 %\n"
" fast_forward_volume = 100 # 0..200 %, used while fast-forwarding\n"
" muted = false\n"
" mute_fast_forward = false\n"
" swap_channels = false\n"
" skip_reverb = false # bypass the SPU reverb network (saves CPU, loses echo)\n"
" buffer_ms = 20 # 20..100; one PAL frame keeps Android audio callbacks cadence-safe\n"
" driver = \"opensles\" # Android only: opensles | aaudio | default\n"
" background_playback = false # keep running with the app off-screen / the screen off\n"
"\n"
"# Library folders used by ARMSX\n"
"[library]\n"
" folders = []\n"
" recursive_folders = []\n";
static const char* g_models_text =
"Available console models:\n"
"\"scph1000\" (SCPH-1000) [NTSC-J]\n"
"\"scph1001\" (SCPH-1001) [NTSC-U/C]\n"
"\"scph1002\" (SCPH-1002) [PAL]\n"
"\"scph3000\" (SCPH-3000) [NTSC-J]\n"
"\"scph3500\" (SCPH-3500) [NTSC-J]\n"
"\"scph5000\" (SCPH-5000) [NTSC-U/C]\n"
"\"scph5501\" (SCPH-5501) [NTSC-U/C]\n"
"\"scph5500\" (SCPH-5500) [NTSC-J]\n"
"\"scph5502\" (SCPH-5502) [PAL]\n"
"\"scph5552\" (SCPH-5552) [PAL]\n"
"\"scph7000\" (SCPH-7000) [NTSC-J]\n"
"\"scph7001\" (SCPH-7001) [NTSC-U/C]\n"
"\"scph7002\" (SCPH-7002) [PAL]\n"
"\"scph7003\" (SCPH-7003) [NTSC-J]\n"
"\"scph7501\" (SCPH-7501) [NTSC]\n"
"\"scph7502\" (SCPH-7502) [PAL]\n"
"\"scph9002\" (SCPH-9002) [PAL]\n"
"\"scph100\" (SCPH-100) [NTSC-J]\n"
"\"scph101\" (SCPH-101) [NTSC-U/C]\n"
"\"scph102a\" (SCPH-102A) [PAL]\n"
"\"scph102b\" (SCPH-102B) [PAL]\n"
"\"scph102c\" (SCPH-102C) [?]\n";
static const char* g_regions_text =
"Available region options: \"ntsc\", \"pal\", \"auto\"\n";
static const char* g_desc_text =
"\nPlease report any bugs to <https://github.com/allkern/psxe/issues>\n";
static int psxe_file_exists(const char* path) {
if (!path || !path[0])
return 0;
FILE* file = fopen(path, "rb");
if (!file)
return 0;
fclose(file);
return 1;
}
static int psxe_is_path_separator(char c) {
return (c == '/') || (c == '\\');
}
static void psxe_normalize_model_name(const char* src, char* dst, size_t dst_size) {
size_t out = 0;
if (!dst_size)
return;
if (!src) {
dst[0] = '\0';
return;
}
while (*src && (out + 1) < dst_size) {
unsigned char ch = (unsigned char)*src++;
if (!isalnum(ch))
continue;
dst[out++] = (char)tolower(ch);
}
dst[out] = '\0';
}
static const char* psxe_basename_no_ext(const char* path) {
const char* base = path ? path : "";
const char* scan = base;
while (*scan) {
if (psxe_is_path_separator(*scan))
base = scan + 1;
scan++;
}
return base;
}
static int psxe_extension_matches(const char* ext, const char* expected) {
if (!ext || !expected)
return 0;
return !strcasecmp(ext, expected);
}
static char* psxe_join_path(const char* dir, const char* leaf) {
size_t dir_len = dir ? strlen(dir) : 0;
size_t leaf_len = leaf ? strlen(leaf) : 0;
int needs_sep = dir_len && !psxe_is_path_separator(dir[dir_len - 1]);
char* path = (char*)malloc(dir_len + leaf_len + (needs_sep ? 2 : 1));
if (!path)
return NULL;
if (dir_len)
memcpy(path, dir, dir_len);
if (needs_sep)
path[dir_len++] = '/';
if (leaf_len)
memcpy(path + dir_len, leaf, leaf_len);
path[dir_len + leaf_len] = '\0';
return path;
}
static char* psxe_find_bios_in_dir(const char* dir_path, const char* model) {
char normalized_model[128];
char normalized_name[128];
char* fallback = NULL;
DIR* dir;
if (!dir_path || !dir_path[0])
return NULL;
dir = opendir(dir_path);
if (!dir)
return NULL;
psxe_normalize_model_name(model, normalized_model, sizeof(normalized_model));
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
const char* name = entry->d_name;
const char* ext;
char* full_path;
if (!name || !name[0] || !strcmp(name, ".") || !strcmp(name, ".."))
continue;
ext = strrchr(name, '.');
if (!ext)
continue;
if (!psxe_extension_matches(ext, ".bin") && !psxe_extension_matches(ext, ".rom"))
continue;
full_path = psxe_join_path(dir_path, name);
if (!full_path)
continue;
if (!psxe_file_exists(full_path)) {
free(full_path);
continue;
}
if (!fallback && !strcasecmp(name, "bios.bin")) {
fallback = full_path;
continue;
}
psxe_normalize_model_name(psxe_basename_no_ext(name), normalized_name, sizeof(normalized_name));
if (normalized_model[0] && !strcmp(normalized_model, normalized_name)) {
if (fallback)
free(fallback);
closedir(dir);
return full_path;
}
free(full_path);
}
closedir(dir);
return fallback;
}
static void psxe_ensure_dir(const char* dir_path) {
if (!dir_path || !dir_path[0])
return;
struct stat st;
if (stat(dir_path, &st) == 0)
return;
#ifdef _WIN32
_mkdir(dir_path);
#else
mkdir(dir_path, 0755);
#endif
}
static void psxe_ensure_parent_dir(const char* path) {
if (!path)
return;
#ifdef PSVITA_TARGET
const size_t path_len = strlen(path);
if (path_len == 0)
return;
char* tmp = malloc(path_len + 1);
if (!tmp)
return;
memcpy(tmp, path, path_len + 1);
#else
char tmp[PATH_MAX];
strncpy(tmp, path, sizeof(tmp) - 1);
tmp[sizeof(tmp) - 1] = '\0';
#endif
char* slash = strrchr(tmp, '/');
#ifdef _WIN32
char* bslash = strrchr(tmp, '\\');
if (!slash || (bslash && bslash > slash))
slash = bslash;
#endif
if (slash && slash != tmp) {
*slash = '\0';
psxe_ensure_dir(tmp);
}
#ifdef PSVITA_TARGET
free(tmp);
#endif
}
static const char* psxe_pref_path(void) {
if (g_pref_path)
return g_pref_path;
#ifdef PSVITA_TARGET
const char* pref_base = "ux0:data/armsx/";
size_t len = strlen(pref_base) + 1;
g_pref_path = (char*)malloc(len);
if (g_pref_path)
memcpy(g_pref_path, pref_base, len);
#else
char* pref_base = SDL_GetPrefPath("nanodata", "armsx");
if (pref_base) {
size_t len = strlen(pref_base) + 1;
g_pref_path = (char*)malloc(len);
if (g_pref_path)
memcpy(g_pref_path, pref_base, len);
SDL_free(pref_base);
}
#endif
return g_pref_path;
}
static const char* psxe_prefixed_path(const char* leaf, char** cache) {
if (!leaf)
return NULL;
const char* base = psxe_pref_path();
if (base && cache && !*cache) {
size_t len = strlen(base) + strlen(leaf) + 1;
*cache = (char*)malloc(len);
if (*cache)
snprintf(*cache, len, "%s%s", base, leaf);
}
if (cache && *cache)
return *cache;
return leaf;
}
psxe_config_t* psxe_cfg_create(void) {
return (psxe_config_t*)malloc(sizeof(psxe_config_t));
}
void psxe_cfg_init(psxe_config_t* cfg) {
memset(cfg, 0, sizeof(psxe_config_t));
}
void psxe_cfg_destroy(psxe_config_t* cfg) {
free(cfg);
}
void psxe_cfg_load_defaults(psxe_config_t* cfg) {
cfg->bios = "bios.bin";
cfg->bios_search = "bios";
cfg->exe = NULL;
cfg->help_model = 0;
cfg->help_region = 0;
cfg->model = "scph1001";
cfg->cpu_engine = 1;
cfg->scale = 3;
cfg->psxe_version = STR(REP_VERSION);
cfg->region = "ntsc";
cfg->settings_path = NULL;
cfg->use_args = 0;
cfg->version = 0;
cfg->log_level = LOG_INFO;
cfg->quiet = 1;
cfg->cd_path = NULL;
cfg->exp_path = NULL;
#if defined(UWP_TARGET)
cfg->vsync_enabled = 0;
#else
cfg->vsync_enabled = 1;
#endif
#ifdef USE_HARDWARE
/* 0 = SDL software presentation. GPU presentation is opt-in so the deterministic SDL path
remains the default on every platform, including Android surfaces that cannot be adopted
by SDL_CreateWindowFrom(). MUST match Ps1Settings.gpuBackend's default. */
cfg->gpu_backend = 0;
cfg->gl_driver = 0;
/* 0 = the native, accuracy-authoritative rasterizer. The scale-aware hardware rasterizer
is opt-in: its CPU fallback is materially slower on SDL-only Android surfaces, and a
default that silently selects it turns a BIOS boot into a frame-rate regression. */
cfg->renderer = 0;
cfg->internal_scale = 1;
#endif
/* Hardware truth, and FREE for games that do not use the features: mask check/set
additionally require the GPUSTAT bits a game must explicitly raise via GP0(E6), and
honouring the dither-enable bit is strictly less work than force-dithering everything.
Off was never an optimisation — it was just wrong for the games that care (the
canonical one being Silent Hill: box around the player light, corrupted composites). */
cfg->accurate_mask_bit = 1;
cfg->accurate_dither = 1;
cfg->accurate_prim_size = 1;
cfg->accurate_tex_modulate = 1;
cfg->pgxp = 0;
/* Every video feature below is OFF/neutral by default and MUST stay that way when the key
is absent: an existing settings.toml has none of them, and a default that disagrees with
the Kotlin mirror is how a native gate turns into dead code. */
cfg->widescreen_hack = 0;
cfg->texture_filter = 0; /* nearest */
cfg->texture_dump = 0;
cfg->texture_replacements = 0;
cfg->texture_dir = "";
cfg->downsample = 0; /* off */
cfg->deinterlace = 0; /* weave */
cfg->overscan_crop = 0; /* none */
cfg->display_rotation = 0; /* 0 degrees */
cfg->line_detect = 0; /* disabled */
cfg->texture_scale_mode = 0;
cfg->debug_panel = 0;
cfg->stretch_mode = 0;
cfg->display_aspect = 0;
cfg->display_aspect_custom = 16.0f / 9.0f;
cfg->integer_scaling = 0;
cfg->upscale_height = 480;
cfg->frame_limit = 1;
cfg->speed_percent = 100;
cfg->fps_limit = 0;
cfg->fast_forward_speed = 2.0;
cfg->frame_skip = 0;
/* Host CPU scheduling levers, both EXPERIMENTAL and both OFF. An on-device settings.toml
written before these keys existed has neither, and absent MUST read as off — the Android
front-end owns the live value and pushes it through JNI before the core parses anything. */
cfg->adpf_clock_hint = 0;
cfg->affinity_mode = 0;
cfg->audio_volume = 100;
cfg->audio_ff_volume = 100;
cfg->audio_muted = 0;
cfg->audio_mute_fast_forward = 0;
cfg->audio_swap_channels = 0;
cfg->audio_skip_reverb = 0;
cfg->audio_buffer_ms = 20;
cfg->audio_driver = 1; /* openslES: the only Android backend that needs no SDLActivity */
/* Off: with the app off-screen the VM parks and the platform audio stream is stopped, not
just SDL-paused. Leaving it running is the "quiet, distorted audio out of a sleeping
device" bug, so it is strictly opt-in. */
cfg->audio_background_playback = 0;
cfg->analog_mode_default = 0;
/* Multitap, rewind and runahead all default OFF, and every parse site below seeds
its local from cfg — so a settings.toml written before these keys existed (which
is every file already on a device) resolves to exactly these values. Getting that
wrong is what silently broke fast_boot and pgxp. */
cfg->multitap = 0;
cfg->rewind = 0;
cfg->rewind_seconds = 10;
cfg->rewind_frequency = 2;
cfg->runahead = 0;
}
void psxe_cfg_load(psxe_config_t* cfg, int argc, const char* argv[]) {
log_set_level(LOG_INFO);
// macOS .app bundles can inject a "-psn_*" argument; drop it before parsing
const char** argv_filtered = (const char**)malloc(sizeof(char*) * argc);
int argc_filtered = 0;
for (int i = 0; i < argc; i++) {
if (strncmp(argv[i], "-psn_", 5) == 0)
continue;
argv_filtered[argc_filtered++] = argv[i];
}
int use_args = 0;
int version = 0;
int help_model = 0;
int help_region = 0;
int log_level = 0;
int quiet = cfg->quiet;
int console_source = 0;
int cpu_engine = cfg->cpu_engine;
int scale = 0;
int vsync_enabled = cfg->vsync_enabled;
#ifdef USE_HARDWARE
int gpu_backend = cfg->gpu_backend;
int gl_driver = cfg->gl_driver;
int renderer = cfg->renderer;
int internal_scale = cfg->internal_scale;
#endif
int accurate_mask_bit = cfg->accurate_mask_bit;
int accurate_dither = cfg->accurate_dither;
int accurate_prim_size = cfg->accurate_prim_size;
int accurate_tex_modulate = cfg->accurate_tex_modulate;
int pgxp = cfg->pgxp;
/* Seeded from cfg, never from a literal: an on-device settings.toml written before these
keys existed has none of them, and the absent-key value must equal the documented
default. fast_boot and pgxp were both bitten by exactly this. */
int widescreen_hack = cfg->widescreen_hack;
int texture_filter = cfg->texture_filter;
int texture_dump = cfg->texture_dump;
int texture_replacements = cfg->texture_replacements;
const char* texture_dir = cfg->texture_dir;
int downsample = cfg->downsample;
int deinterlace = cfg->deinterlace;
int overscan_crop = cfg->overscan_crop;
int display_rotation = cfg->display_rotation;
int line_detect = cfg->line_detect;
int texture_scale_mode = 0;
int debug_panel = 0;
int stretch_mode = 0;
int display_aspect = 0;
double display_aspect_custom = cfg->display_aspect_custom;
int integer_scaling = cfg->integer_scaling;
int upscale_height = 480;
/* Seeded from cfg (not a bare literal) so the default survives a settings.toml with no
[input] section — every file written before this key existed. */
int analog_mode_default = cfg->analog_mode_default;
int multitap = cfg->multitap;
int rewind = cfg->rewind;
int rewind_seconds = cfg->rewind_seconds;
int rewind_frequency = cfg->rewind_frequency;
int runahead = cfg->runahead;
/* Same reasoning for [runtime].fast_forward_speed and the whole [audio] table: a
settings.toml written before those keys existed must keep the defaults, not zeros. */
int frame_limit = cfg->frame_limit;
/* Distinct from the `log_level` CLI local above: that one is 1-based with 0 meaning
"-L not passed", this one holds a raw LOG_* level read from the file. */
int file_log_level = cfg->log_level;
int speed_percent = cfg->speed_percent;
int fps_limit = cfg->fps_limit;
double fast_forward_speed = cfg->fast_forward_speed;
int frame_skip = cfg->frame_skip;
int adpf_clock_hint = cfg->adpf_clock_hint;
int affinity_mode = cfg->affinity_mode;
int audio_volume = cfg->audio_volume;
int audio_ff_volume = cfg->audio_ff_volume;
int audio_muted = cfg->audio_muted;
int audio_mute_fast_forward = cfg->audio_mute_fast_forward;
int audio_swap_channels = cfg->audio_swap_channels;
int audio_skip_reverb = cfg->audio_skip_reverb;
int audio_buffer_ms = cfg->audio_buffer_ms;
int audio_driver = cfg->audio_driver;
int audio_background_playback = cfg->audio_background_playback;
const char* settings_path = NULL;
const char* bios = NULL;
int bios_from_cli = 0;
const char* bios_search = NULL;
const char* model = NULL;
const char* exe = NULL;
const char* region = NULL;
const char* psxe_version = NULL;
const char* cd_path = NULL;
const char* exp_path = NULL;
const char* cpu_engine_name = NULL;
static const char *const usages[] = {
"psxe [options] path-to-cdrom",
NULL,
};
struct argparse_option options[] = {
OPT_BOOLEAN ('h', "help" , NULL , "Display this information", argparse_help_cb, 0, 0),
OPT_BOOLEAN (0 , "help-model" , &help_model , "Display available console models", NULL, 0, 0),
OPT_BOOLEAN (0 , "help-region" , &help_region , "Display available region options", NULL, 0, 0),
OPT_BOOLEAN ('v', "version" , &version , "Display version and build information", NULL, 0, 0),
OPT_GROUP("Basic options"),
OPT_BOOLEAN ('a', "use-args" , &use_args , "Ignore settings file, use CLI args instead", NULL, 0, 0),
OPT_STRING ('b', "bios" , &bios , "Specify a BIOS file (ignores -B, -M)", NULL, 0, 0),
OPT_BOOLEAN ('B', "bios-folder" , &bios_search , "Specify a BIOS search folder", NULL, 0, 0),
OPT_STRING ('c', "console-source", &console_source, "Select console source (auto, null, kernel, atcons)"),
OPT_STRING (0 , "cpu-engine" , &cpu_engine_name, "Select CPU engine (cached or interpreter)", NULL, 0, 0),
OPT_STRING ('e', "exp-rom" , &exp_path , "Specify an expansion ROM file"),
OPT_INTEGER ('L', "log-level" , &log_level , "Set log level"),
OPT_STRING ('M', "model" , &model , "Specify console model (SPCH-XXXX)", NULL, 0, 0),
OPT_STRING ('r', "region" , &region , "Specify console region"),
OPT_INTEGER ('s', "scale" , &scale , "Display scaling factor", NULL, 0, 0),
OPT_STRING ('S', "settings-file" , &settings_path , "Specify settings file path", NULL, 0, 0),
OPT_BOOLEAN ('q', "quiet" , &quiet , "Silence all logs (ignores -L)"),
OPT_STRING ('x', "exe" , &exe , "Launch a PS-X EXE file"),
OPT_STRING (0 , "cdrom" , &cd_path , "Specify a CDROM image"),
OPT_END()
};
struct argparse argparse;
argparse_init(&argparse, options, usages, 0);
argparse_describe(&argparse, NULL, g_desc_text);
argc = argparse_parse(&argparse, argc_filtered, argv_filtered);
if (bios)
bios_from_cli = 1;
if (help_model) {
printf("%s\n", g_models_text);
exit(0);
}
if (help_region) {
printf("%s\n", g_regions_text);
exit(0);
}
if (version) {
printf("%s\n", g_version_text);
exit(0);
}
log_set_quiet(quiet);
if (!use_args) {
if (!settings_path) {
settings_path = psxe_prefixed_path("settings.toml", &g_settings_path);
}
if (!settings_path)
settings_path = "settings.toml";
psxe_ensure_parent_dir(settings_path);
FILE* settings = fopen(settings_path, "rb");
char error[0x100];
if (!settings) {
log_info("Settings file not found; attempting to create one at %s", settings_path ? settings_path : "settings.toml");
if (settings_path)
settings = fopen(settings_path, "w+b");
else
settings = fopen("settings.toml", "w+b");
if (!settings) {
log_error("Couldn't create settings file %s, loading default settings", settings_path ? settings_path : "settings.toml");
psxe_cfg_load_defaults(cfg);
return;
}
fwrite(g_default_settings, 1, sizeof(g_default_settings) - 1, settings);
fseek(settings, 0, 0);
}
/* The PATH, not just "a settings file". There is more than one plausible location on
a device (app-private vs a user-visible folder), and a value that came from a file
nobody was looking at reads as an emulator bug — which has already cost a round of
this investigation, over [cdrom] read_speedup. */
log_info("Parsing settings file: %s", settings_path ? settings_path : "settings.toml");
toml_table_t* conf = toml_parse_file(settings, error, sizeof(error));
if (!conf) {
log_error("Couldn't parse settings file");
exit(1);
}
toml_datum_t s_version = toml_string_in(conf, "psxe_version");
if (!s_version.ok) {
log_error("Settings file lacking version number");
exit(1);
}
toml_table_t* s_bios_table = toml_table_in(conf, "bios");
if (s_bios_table) {
toml_datum_t s_bios_search_path = toml_string_in(s_bios_table, "search_path");
if (s_bios_search_path.ok)
bios_search = s_bios_search_path.u.s;
toml_datum_t s_bios_preferred_model = toml_string_in(s_bios_table, "preferred_model");
if (s_bios_preferred_model.ok)
model = s_bios_preferred_model.u.s;
toml_datum_t s_bios_override_file = toml_string_in(s_bios_table, "override_file");
if (!bios_from_cli && s_bios_override_file.ok && s_bios_override_file.u.s[0])
bios = s_bios_override_file.u.s;
}
toml_table_t* s_console_table = toml_table_in(conf, "console");
if (s_console_table) {
toml_datum_t s_console_region = toml_string_in(s_console_table, "region");
if (s_console_region.ok)
region = s_console_region.u.s;
}
toml_table_t* s_cpu_table = toml_table_in(conf, "cpu");
if (s_cpu_table && !cpu_engine_name) {
toml_datum_t s_cpu_engine = toml_string_in(s_cpu_table, "execution_mode");
if (s_cpu_engine.ok && s_cpu_engine.u.s) {
cpu_engine = !strcasecmp(s_cpu_engine.u.s, "interpreter") ? 0 : 1;
free(s_cpu_engine.u.s);
}
}
/* Skip BIOS. Read outside the !cpu_engine_name guard above — that guard exists so a
command-line engine choice wins over the file, and has nothing to do with fast boot. */
if (s_cpu_table) {
toml_datum_t s_fast_boot = toml_bool_in(s_cpu_table, "fast_boot");
if (s_fast_boot.ok)
psx_fastboot_set_enabled(s_fast_boot.u.b);
}
if (s_cpu_table) {
toml_datum_t s_overclock = toml_int_in(s_cpu_table, "overclock");
if (s_overclock.ok)
psx_set_cpu_overclock((unsigned)s_overclock.u.i);
}
toml_table_t* s_cdrom_table = toml_table_in(conf, "cdrom");
if (s_cdrom_table) {
toml_datum_t s_read_speedup = toml_int_in(s_cdrom_table, "read_speedup");
toml_datum_t s_seek_speedup = toml_int_in(s_cdrom_table, "seek_speedup");
if (s_read_speedup.ok)
psx_cdrom_set_read_speedup((unsigned)s_read_speedup.u.i);
if (s_seek_speedup.ok)
psx_cdrom_set_seek_speedup((unsigned)s_seek_speedup.u.i);
/* Say so out loud. This is the one lever psx/dev/cdrom/cdrom.h singles out as
able to break FMV and streamed XA audio, it has no UI, and it is only reachable
by hand-editing a file — so a device running with it set looks identical to a
device with an emulator bug unless the log names it. */
if (psx_cdrom_get_read_speedup() != 1 || psx_cdrom_get_seek_speedup() != 1) {
log_warn("CD-ROM speedup active: read=%ux seek=%ux (1 = authentic timing). "
"Read speedup delivers sectors faster than the drive can and is "
"known to break FMV and streamed XA audio.",
psx_cdrom_get_read_speedup(), psx_cdrom_get_seek_speedup());
}
}
toml_table_t* s_runtime_table = toml_table_in(conf, "runtime");
if (s_runtime_table) {
toml_datum_t s_frame_limit = toml_bool_in(s_runtime_table, "frame_limit");
if (s_frame_limit.ok)
frame_limit = s_frame_limit.u.b;
toml_datum_t s_speed_percent = toml_int_in(s_runtime_table, "speed_percent");
if (s_speed_percent.ok)
speed_percent = (int)s_speed_percent.u.i;
toml_datum_t s_fps_limit = toml_int_in(s_runtime_table, "fps_limit");
if (s_fps_limit.ok)
fps_limit = (int)s_fps_limit.u.i;
/* Accepted as a float (2.5) or a bare integer (2) — the UI writes one, a
hand-edited file can hold either, and toml_double_in only matches floats. */
toml_datum_t s_ff_speed = toml_double_in(s_runtime_table, "fast_forward_speed");
if (s_ff_speed.ok) {
fast_forward_speed = s_ff_speed.u.d;
} else {
toml_datum_t s_ff_speed_int = toml_int_in(s_runtime_table, "fast_forward_speed");
if (s_ff_speed_int.ok)
fast_forward_speed = (double)s_ff_speed_int.u.i;
}
toml_datum_t s_frame_skip = toml_int_in(s_runtime_table, "frame_skip");
if (s_frame_skip.ok)
frame_skip = (int)s_frame_skip.u.i;
/* ADPF CPU clock hint and emulation-thread affinity (frontend/perf_hint.c). Both
are host levers, not emulation ones, and both are a boot DEFAULT here: an
embedded front-end that has already pushed a value keeps it. */
toml_datum_t s_adpf = toml_bool_in(s_runtime_table, "adpf_clock_hint");
if (s_adpf.ok)
adpf_clock_hint = s_adpf.u.b;
toml_datum_t s_affinity_mode = toml_int_in(s_runtime_table, "affinity_mode");
if (s_affinity_mode.ok)
affinity_mode = (int)s_affinity_mode.u.i;
toml_datum_t s_log_level = toml_int_in(s_runtime_table, "log_level");
if (s_log_level.ok)
file_log_level = (int)s_log_level.u.i;
}
toml_table_t* s_audio_table = toml_table_in(conf, "audio");
if (s_audio_table) {
toml_datum_t s_volume = toml_int_in(s_audio_table, "volume");
if (s_volume.ok)
audio_volume = (int)s_volume.u.i;
toml_datum_t s_ff_volume = toml_int_in(s_audio_table, "fast_forward_volume");
if (s_ff_volume.ok)
audio_ff_volume = (int)s_ff_volume.u.i;
toml_datum_t s_muted = toml_bool_in(s_audio_table, "muted");
if (s_muted.ok)
audio_muted = s_muted.u.b;
toml_datum_t s_mute_ff = toml_bool_in(s_audio_table, "mute_fast_forward");
if (s_mute_ff.ok)
audio_mute_fast_forward = s_mute_ff.u.b;
toml_datum_t s_swap = toml_bool_in(s_audio_table, "swap_channels");
if (s_swap.ok)
audio_swap_channels = s_swap.u.b;
toml_datum_t s_skip_reverb = toml_bool_in(s_audio_table, "skip_reverb");
if (s_skip_reverb.ok)
audio_skip_reverb = s_skip_reverb.u.b;
toml_datum_t s_buffer_ms = toml_int_in(s_audio_table, "buffer_ms");
if (s_buffer_ms.ok)
audio_buffer_ms = (int)s_buffer_ms.u.i;
/* Android-only; ignored everywhere else. "default" means "leave SDL's probe
order alone", which on Android is NOT safe in-process — see the note on
EnsureSdlVideo() in frontend/android_jni.cpp — so it is an explicit opt-in. */
toml_datum_t s_driver = toml_string_in(s_audio_table, "driver");
if (s_driver.ok && s_driver.u.s) {
if (!strcasecmp(s_driver.u.s, "aaudio")) {
audio_driver = 2;
} else if (!strcasecmp(s_driver.u.s, "default") || !strcasecmp(s_driver.u.s, "auto")) {
audio_driver = 0;
} else {
audio_driver = 1;
}
free(s_driver.u.s);
}
toml_datum_t s_background = toml_bool_in(s_audio_table, "background_playback");
if (s_background.ok)
audio_background_playback = s_background.u.b;
}
toml_table_t* s_video_table = toml_table_in(conf, "video");
if (s_video_table) {
toml_datum_t s_vsync = toml_bool_in(s_video_table, "vsync");
if (s_vsync.ok)
vsync_enabled = s_vsync.u.b;
#ifdef USE_HARDWARE
toml_datum_t s_gpu_backend = toml_string_in(s_video_table, "gpu_backend");
if (s_gpu_backend.ok && s_gpu_backend.u.s) {
const char* backend = s_gpu_backend.u.s;
/* Keep these numeric values in lockstep with GpuBackend / armsx_render_backend_t:
0 = software, 1 = sdl-accelerated, 2 = opengl, 3 = vulkan. Anything
unrecognised falls back to software so an old or hand-edited settings.toml
can never leave the frontend without a renderer. */
if (!strcasecmp(backend, "sdl-accelerated") || !strcasecmp(backend, "hardware") || !strcasecmp(backend, "hardware (experimental)") || !strcasecmp(backend, "hw") || !strcasecmp(backend, "hw-renderer")) {
gpu_backend = 1;
} else if (!strcasecmp(backend, "opengl") || !strcasecmp(backend, "gl") || !strcasecmp(backend, "gles") || !strcasecmp(backend, "gles3") || !strcasecmp(backend, "opengl-es")) {
gpu_backend = 2;
} else if (!strcasecmp(backend, "angle")) {
/* ANGLE is an implementation of the OpenGL backend, not a backend of its
own, so it selects opengl and pins the GLES provider. One token keeps
the UI's renderer list a single flat choice. */
gpu_backend = 2;
gl_driver = 1;
} else if (!strcasecmp(backend, "vulkan") || !strcasecmp(backend, "vk")) {
gpu_backend = 3;
} else {
gpu_backend = 0;
}
free(s_gpu_backend.u.s);
}
/* Explicit override, for "opengl" + a separately-chosen provider. Read after
gpu_backend so it wins over the "angle" shorthand above. */
toml_datum_t s_gl_driver = toml_string_in(s_video_table, "gl_driver");
if (s_gl_driver.ok && s_gl_driver.u.s) {
if (!strcasecmp(s_gl_driver.u.s, "angle")) {
gl_driver = 1;
} else {
gl_driver = 0;
}
free(s_gl_driver.u.s);
}
/* The RASTERIZER, deliberately not folded into gpu_backend above, which
selects the *presentation* path. the backend. */
toml_datum_t s_renderer = toml_string_in(s_video_table, "renderer");
if (s_renderer.ok && s_renderer.u.s) {
const char* name = s_renderer.u.s;
/* "hardware" stays the token the UI writes and means AUTO: the GLES
rasterizer when a GL context exists, the CPU one otherwise. The two
explicit tokens exist so the same device can A/B them from
settings.toml without a UI change. */
if (!strcasecmp(name, "hardware-gl") || !strcasecmp(name, "opengl") ||
!strcasecmp(name, "gl")) {
renderer = 3;
} else if (!strcasecmp(name, "hardware-cpu") || !strcasecmp(name, "cpu")) {
renderer = 2;
} else if (!strcasecmp(name, "hardware") || !strcasecmp(name, "hw") ||
!strcasecmp(name, "internal") || !strcasecmp(name, "upscale")) {
renderer = 1;
} else {
renderer = 0;
}
free(s_renderer.u.s);
}
/* GPU profile override: "auto" (default), "adreno", "mali", "powervr", "xclipse".
This exists because some device families cannot be identified reliably — Samsung
Xclipse in particular is recognised only by a vendor ID no Xclipse device has yet
confirmed for us. Without a way to force the profile, a tester on such a device
can neither reach the code path nor tell us whether it helps, and a workaround
nobody can trigger is a workaround nobody can validate. */
toml_datum_t s_gpu_profile = toml_string_in(s_video_table, "gpu_profile");
if (s_gpu_profile.ok && s_gpu_profile.u.s) {
armsx_gpu_profile_override(s_gpu_profile.u.s);
free(s_gpu_profile.u.s);
}
/* Overrules the profile's fbfetch conclusion. Those defaults are conservative, not
measured per device, so this is how an owner A/Bs their own hardware. */
toml_datum_t s_force_fbfetch = toml_string_in(s_video_table, "force_fbfetch");
if (s_force_fbfetch.ok && s_force_fbfetch.u.s) {
armsx_gpu_profile_force_fbfetch(s_force_fbfetch.u.s);
free(s_force_fbfetch.u.s);
}
/* Accepted as a bare integer (internal_scale = 2) or as a string ("2x"), so a
UI layer can write either without silently landing back on 1x. */
toml_datum_t s_internal_scale = toml_int_in(s_video_table, "internal_scale");
if (s_internal_scale.ok) {
internal_scale = (int)s_internal_scale.u.i;
} else {
toml_datum_t s_internal_scale_str = toml_string_in(s_video_table, "internal_scale");
if (s_internal_scale_str.ok && s_internal_scale_str.u.s) {
internal_scale = atoi(s_internal_scale_str.u.s);
free(s_internal_scale_str.u.s);
}
}
if (internal_scale < 1)
internal_scale = 1;
if (internal_scale > 8)
internal_scale = 8;
#endif
/* Accuracy fixes. Both default ON — see psxe_cfg_load_defaults() for why (hardware
truth, and free for games that never raise the GPUSTAT bits). They do change what
games look like relative to the releases before they existed, which is the risk
the backend ranks. */
toml_datum_t s_mask_bit = toml_bool_in(s_video_table, "accurate_mask_bit");
if (s_mask_bit.ok)
accurate_mask_bit = s_mask_bit.u.b;
toml_datum_t s_acc_dither = toml_bool_in(s_video_table, "accurate_dither");
if (s_acc_dither.ok)
accurate_dither = s_acc_dither.u.b;
/* Hardware drops any polygon whose vertices are more than 1023 apart
horizontally or 511 vertically (psx-spx). This core rejected at 2048x1024,
so primitives hardware throws away were drawn as huge stretched triangles.
See psx_gpu_prim_oversize() and the backend. Off restores
the historical 2048x1024 for an A/B. */
toml_datum_t s_prim_size = toml_bool_in(s_video_table, "accurate_prim_size");
if (s_prim_size.ok)
accurate_prim_size = s_prim_size.u.b;
/* Texture blending divides by 128 with integer truncation on hardware; this core
rounded. Invisible on ordinary art (<=1 level, 4.9% of inputs) but it makes a
frame-feedback trail permanent instead of fading — Silent Hill's loading
screen. psx_gpu_modulate_channel(), the backend. */
toml_datum_t s_tex_mod = toml_bool_in(s_video_table, "accurate_tex_modulate");
if (s_tex_mod.ok)
accurate_tex_modulate = s_tex_mod.u.b;
/* PGXP (psx/pgxp.c): sub-pixel vertex precision captured at the GTE and
re-attached at GP0 submission. Default off — with it off the pipeline is
byte-identical to before the feature existed. */
toml_datum_t s_pgxp = toml_bool_in(s_video_table, "pgxp");
if (s_pgxp.ok)
pgxp = s_pgxp.u.b;
/* ---- display / video feature set (the backend) --------------
Every one of these is default off/neutral and every parse below only ever
ASSIGNS on a successful read, so a file without the key keeps the seeded
default. An unrecognised string token also keeps the default rather than
falling through to some other mode. */
/* Widescreen hack (psx/cpu.c, GTE RTPS/RTPT): scales the projected X so 3D
geometry fills 16:9 instead of being stretched into it. 2D/UI still stretches. */
toml_datum_t s_widescreen = toml_bool_in(s_video_table, "widescreen_hack");
if (s_widescreen.ok)
widescreen_hack = s_widescreen.u.b;
/* Texture filtering, GLES rasterizer only. Operates on the TEXTURE SAMPLE, never
on the coverage decision (). */
toml_datum_t s_texture_filter = toml_string_in(s_video_table, "texture_filter");
if (s_texture_filter.ok && s_texture_filter.u.s) {
if (!strcasecmp(s_texture_filter.u.s, "bilinear"))
texture_filter = 1;
else if (!strcasecmp(s_texture_filter.u.s, "xbr"))
texture_filter = 2;
else
texture_filter = 0;
free(s_texture_filter.u.s);
}
/* Texture dumping / replacement (psx/texrep.h). Both bools default false, and an
absent key must leave them false — every settings.toml written before this
feature existed has none of the three. */
toml_datum_t s_tex_dump = toml_bool_in(s_video_table, "texture_dump");
if (s_tex_dump.ok)
texture_dump = s_tex_dump.u.b;
toml_datum_t s_tex_repl = toml_bool_in(s_video_table, "texture_replacements");
if (s_tex_repl.ok)
texture_replacements = s_tex_repl.u.b;
toml_datum_t s_tex_dir = toml_string_in(s_video_table, "texture_dir");
if (s_tex_dir.ok && s_tex_dir.u.s)
texture_dir = s_tex_dir.u.s; /* kept, exactly as cfg->bios is */
/* Downsampling: render at the internal scale, then box-average NxN back down.
0 and 1 both mean off; the ceiling matches internal_scale's. */
toml_datum_t s_downsample = toml_int_in(s_video_table, "downsample");
if (s_downsample.ok) {
downsample = (int)s_downsample.u.i;
if (downsample < 2)
downsample = 0;
if (downsample > 8)
downsample = 8;
}
toml_datum_t s_deinterlace = toml_string_in(s_video_table, "deinterlace");
if (s_deinterlace.ok && s_deinterlace.u.s) {
if (!strcasecmp(s_deinterlace.u.s, "bob"))
deinterlace = 1;
else if (!strcasecmp(s_deinterlace.u.s, "adaptive"))
deinterlace = 2;
else
deinterlace = 0;
free(s_deinterlace.u.s);
}
toml_datum_t s_overscan = toml_string_in(s_video_table, "overscan_crop");
if (s_overscan.ok && s_overscan.u.s) {
if (!strcasecmp(s_overscan.u.s, "small"))
overscan_crop = 1;
else if (!strcasecmp(s_overscan.u.s, "full"))
overscan_crop = 2;
else
overscan_crop = 0;
free(s_overscan.u.s);
}
/* Stored in the file as DEGREES (0/90/180/270) because that is what a user editing
it by hand expects; kept internally as quarter turns. Anything else keeps 0
rather than snapping to a rotation nobody asked for. */
toml_datum_t s_rotation = toml_int_in(s_video_table, "display_rotation");
if (s_rotation.ok) {
switch ((int)s_rotation.u.i) {
case 90: display_rotation = 1; break;
case 180: display_rotation = 2; break;
case 270: display_rotation = 3; break;
default: display_rotation = 0; break;
}
}
toml_datum_t s_line_detect = toml_string_in(s_video_table, "line_detect");
if (s_line_detect.ok && s_line_detect.u.s) {
if (!strcasecmp(s_line_detect.u.s, "quads"))
line_detect = 1;
else if (!strcasecmp(s_line_detect.u.s, "basic"))
line_detect = 2;
else
line_detect = 0;
free(s_line_detect.u.s);
}
toml_datum_t s_texture_scale_mode = toml_bool_in(s_video_table, "texture_scale_mode");
if (s_texture_scale_mode.ok)
texture_scale_mode = s_texture_scale_mode.u.b;
toml_datum_t s_debug_panel = toml_bool_in(s_video_table, "debug_panel");
if (s_debug_panel.ok)
debug_panel = s_debug_panel.u.b;
toml_datum_t s_stretch_mode = toml_bool_in(s_video_table, "stretch_mode");
if (s_stretch_mode.ok)
stretch_mode = s_stretch_mode.u.b;
{
toml_datum_t s_integer_scaling = toml_bool_in(s_video_table, "integer_scaling");
if (s_integer_scaling.ok)
integer_scaling = s_integer_scaling.u.b;
}
{
toml_datum_t s_aspect_custom = toml_double_in(s_video_table, "display_aspect_custom");
/* Out of range is IGNORED, not clamped: silently presenting a ratio the user
did not ask for is how "custom does nothing" gets reported. */
if (s_aspect_custom.ok && s_aspect_custom.u.d >= 0.5 && s_aspect_custom.u.d <= 4.0)
display_aspect_custom = s_aspect_custom.u.d;
}
toml_datum_t s_display_aspect = toml_string_in(s_video_table, "display_aspect");
if (s_display_aspect.ok) {
if (!strcasecmp(s_display_aspect.u.s, "square")) {
display_aspect = 1;
} else if (!strcasecmp(s_display_aspect.u.s, "wide16x9")) {
display_aspect = 2;
} else if (!strcasecmp(s_display_aspect.u.s, "custom")) {
display_aspect = 3;
} else {
display_aspect = 0;
}
}
toml_datum_t s_wide_upscale = toml_string_in(s_video_table, "wide_upscale");
if (s_wide_upscale.ok) {
if (!strcasecmp(s_wide_upscale.u.s, "720p")) upscale_height = 720;
else if (!strcasecmp(s_wide_upscale.u.s, "1080p")) upscale_height = 1080;
else if (!strcasecmp(s_wide_upscale.u.s, "1440p")) upscale_height = 1440;
else if (!strcasecmp(s_wide_upscale.u.s, "2160p")) upscale_height = 2160;
else upscale_height = 480;
}
}
toml_table_t* s_input_table = toml_table_in(conf, "input");
if (s_input_table) {
toml_datum_t s_analog_mode_default = toml_bool_in(s_input_table, "analog_mode_default");
if (s_analog_mode_default.ok)
analog_mode_default = s_analog_mode_default.u.b;
/* Multitap in port 1 (psx/input/multitap.c). An ABSENT key keeps the default
(off), which is what every settings.toml written before this existed has. */
toml_datum_t s_multitap = toml_bool_in(s_input_table, "multitap");
if (s_multitap.ok)
multitap = s_multitap.u.b;
}
/* [emulation]: rewind + runahead. A missing table leaves all four at their
defaults, so an existing file behaves exactly as it did before. */
toml_table_t* s_emulation_table = toml_table_in(conf, "emulation");
if (s_emulation_table) {
toml_datum_t s_rewind = toml_bool_in(s_emulation_table, "rewind");
if (s_rewind.ok)
rewind = s_rewind.u.b;
toml_datum_t s_rewind_seconds = toml_int_in(s_emulation_table, "rewind_seconds");
if (s_rewind_seconds.ok)
rewind_seconds = (int)s_rewind_seconds.u.i;
toml_datum_t s_rewind_frequency = toml_int_in(s_emulation_table, "rewind_frequency");
if (s_rewind_frequency.ok)
rewind_frequency = (int)s_rewind_frequency.u.i;
toml_datum_t s_runahead = toml_int_in(s_emulation_table, "runahead");
if (s_runahead.ok)
runahead = (int)s_runahead.u.i;
}
psxe_version = s_version.u.s;
log_info("Settings file parsed. ARMSX version: %s", psxe_version);
fclose(settings);
}
if (argc) {
if (argc > 1) {
log_error("Unrecognized parameter \'%s\'", argv[1]);
exit(1);
}
cd_path = argv[0];
}
if (cd_path)
cfg->cd_path = cd_path;
if (settings_path)
cfg->settings_path = settings_path;
/* -L wins over [runtime].log_level. The flag is 1-based (0 = "not passed") so it is
decoded here; the file value is already a raw LOG_* level. Clamped for both paths —
a hand-edited log_level = 99 would otherwise index level_strings[] out of bounds in
log_level_string(). */
if (log_level)
cfg->log_level = log_level - 1;
else
cfg->log_level = file_log_level;
if (cfg->log_level < LOG_TRACE)
cfg->log_level = LOG_TRACE;
else if (cfg->log_level > LOG_FATAL)
cfg->log_level = LOG_FATAL;
if (bios)
cfg->bios = bios;
if (!cfg->bios) {
const char* bios_default = psxe_prefixed_path("bios.bin", &g_bios_path);
#ifdef IOS_TARGET
if (!g_pref_path && !g_bios_path) {
char* base_path = SDL_GetBasePath();
if (base_path) {
size_t len = strlen(base_path) + strlen("bios.bin") + 1;
g_bios_path = (char*)malloc(len);
if (g_bios_path)
snprintf(g_bios_path, len, "%sbios.bin", base_path);
SDL_free(base_path);
bios_default = g_bios_path ? g_bios_path : bios_default;
}
}
#endif
cfg->bios = bios_default ? bios_default : "bios.bin";
}
if (bios_search)
cfg->bios_search = bios_search;
if (model)
cfg->model = model;
if (exe)
cfg->exe = exe;
if (region)
cfg->region = region;
if (psxe_version)
cfg->psxe_version = psxe_version;
if (exp_path)
cfg->exp_path = exp_path;
if (cpu_engine_name)
cpu_engine = !strcasecmp(cpu_engine_name, "interpreter") ? 0 : 1;
if (scale)
cfg->scale = scale;
cfg->vsync_enabled = vsync_enabled;
cfg->cpu_engine = cpu_engine;
#ifdef USE_HARDWARE
cfg->gpu_backend = gpu_backend;
cfg->gl_driver = gl_driver;
/* The FILE is the default; a selection the host pushed through JNI before the VM thread
started is the override and survives this. */
armsx_render_set_gl_driver_default(gl_driver == 1 ? ARMSX_RENDER_GL_DRIVER_ANGLE
: ARMSX_RENDER_GL_DRIVER_SYSTEM);
cfg->renderer = renderer;
cfg->internal_scale = internal_scale;
cfg->accurate_mask_bit = accurate_mask_bit;
cfg->accurate_dither = accurate_dither;
cfg->accurate_prim_size = accurate_prim_size;
cfg->accurate_tex_modulate = accurate_tex_modulate;
cfg->pgxp = pgxp;
if (renderer) {
static const char* kRendererName[4] = {
"software", "hardware (auto)", "hardware (CPU)", "hardware (GLES)"
};
log_info("Rasterizer: %s, internal resolution %dx",
kRendererName[renderer & 3], internal_scale > 1 ? internal_scale : 1);
}
#endif
/* ---- [video] display/video feature set ----------------------------------------------
ALL SEVEN must be assigned here. Parsing a key and never storing it is the failure this
project keeps shipping: the setting appears in settings.toml, the parser reads it, and
the core still runs the default because the local never made it back onto cfg. Four of
these were exactly that until a call-site grep caught it.
Outside the USE_HARDWARE guard on purpose. Three of them only mean anything to the GLES
rasterizer, but psxe_config_t has the fields unconditionally and the frontend copies
them unconditionally, so a guard here would only create a build where the struct field
silently keeps its default. */
cfg->widescreen_hack = widescreen_hack ? 1 : 0;
cfg->texture_filter = (texture_filter < 0 || texture_filter > 2) ? 0 : texture_filter;
cfg->texture_dump = texture_dump ? 1 : 0;
cfg->texture_replacements = texture_replacements ? 1 : 0;
cfg->texture_dir = texture_dir ? texture_dir : "";
/* 0 and 1 are both "off"; the useful range starts at 2. Clamped rather than ignored
because a downsample factor is a magnitude, not a mode token. */
cfg->downsample = (downsample < 2) ? 0 : (downsample > 8 ? 8 : downsample);
cfg->deinterlace = (deinterlace < 0 || deinterlace > 2) ? 0 : deinterlace;
cfg->overscan_crop = (overscan_crop < 0 || overscan_crop > 2) ? 0 : overscan_crop;
cfg->display_rotation = display_rotation & 3;
cfg->line_detect = (line_detect < 0 || line_detect > 2) ? 0 : line_detect;
cfg->texture_scale_mode = texture_scale_mode;
cfg->debug_panel = debug_panel;
cfg->stretch_mode = stretch_mode;
cfg->display_aspect = display_aspect;
cfg->display_aspect_custom = (float)display_aspect_custom;
cfg->integer_scaling = integer_scaling;
cfg->upscale_height = upscale_height;
cfg->analog_mode_default = analog_mode_default;
cfg->multitap = multitap;
cfg->rewind = rewind;
/* Clamped to the ranges psx/rewind.h documents. The core clamps again, so this is
about the numbers a front-end reads back, not about protecting the ring. */
cfg->rewind_seconds = rewind_seconds < 1 ? 1 : (rewind_seconds > 60 ? 60 : rewind_seconds);
cfg->rewind_frequency = rewind_frequency < 1 ? 1 : (rewind_frequency > 8 ? 8 : rewind_frequency);
cfg->runahead = runahead < 0 ? 0 : (runahead > 5 ? 5 : runahead);
cfg->frame_limit = frame_limit;
cfg->speed_percent = speed_percent < 10 ? 10 : (speed_percent > 1000 ? 1000 : speed_percent);
cfg->fps_limit = fps_limit < 0 ? 0 : (fps_limit > 1000 ? 1000 : fps_limit);
/* 0 stays 0 (uncapped); anything else is clamped into a range the frame pacer can
actually express, so a hand-edited 0.01 can't stall the emulation thread. */
cfg->fast_forward_speed = (fast_forward_speed <= 0.0)
? 0.0
: (fast_forward_speed < 1.0 ? 1.0 : (fast_forward_speed > 16.0 ? 16.0 : fast_forward_speed));
/* -1 is the adaptive sentinel, so the floor is -1 rather than 0. Anything below that is a
typo, not a request for more skipping. */
cfg->frame_skip = frame_skip < -1 ? -1 : (frame_skip > 5 ? 5 : frame_skip);
cfg->adpf_clock_hint = adpf_clock_hint ? 1 : 0;
/* Not clamped to 0..2: 7 is a live alias for "performance cores" and perf_hint.c is the one
place that decides what an unknown mode means (it means off). Clamping here would turn a
future mode into a silently different one. */
cfg->affinity_mode = affinity_mode;
cfg->audio_volume = audio_volume < 0 ? 0 : (audio_volume > 200 ? 200 : audio_volume);
cfg->audio_ff_volume = audio_ff_volume < 0 ? 0 : (audio_ff_volume > 200 ? 200 : audio_ff_volume);
cfg->audio_muted = audio_muted;
cfg->audio_mute_fast_forward = audio_mute_fast_forward;
cfg->audio_swap_channels = audio_swap_channels;
cfg->audio_skip_reverb = audio_skip_reverb;
cfg->audio_buffer_ms = audio_buffer_ms < 20 ? 20 : (audio_buffer_ms > 100 ? 100 : audio_buffer_ms);
cfg->audio_driver = (audio_driver < 0 || audio_driver > 2) ? 1 : audio_driver;
cfg->audio_background_playback = audio_background_playback ? 1 : 0;
cfg->quiet = quiet;
cfg->use_args = use_args;
/* Tell the shader layer the internal-resolution scale, so a CRT chain can be fed the
NATIVE-resolution image rather than the upscaled one — a scanline shader run on a 3x
frame draws 720 scanlines instead of 240 and moires. Only meaningful when the
hardware rasterizer is actually on; `renderer == 0` leaves the frame at 1x whatever
internal_scale says. This is a hint, not a contract: render_shaders.cpp walks it down
until it divides the real frame exactly, so a rasterizer that failed to install
degrades to 1x instead of corrupting the geometry. */
#ifdef USE_HARDWARE
armsx_shader_set_source_scale(cfg->renderer ? cfg->internal_scale : 1);
#else
armsx_shader_set_source_scale(1);
#endif
log_info("Using BIOS path: %s", cfg->bios ? cfg->bios : "(none)");
free(argv_filtered);
}
char* psxe_cfg_get_bios_path(psxe_config_t* cfg) {
char* bios_path = NULL;
if (!cfg)
return NULL;
if (cfg->bios && psxe_file_exists(cfg->bios))
return strdup(cfg->bios);
if (cfg->bios_search && cfg->bios_search[0])
bios_path = psxe_find_bios_in_dir(cfg->bios_search, cfg->model);
if (bios_path)
return bios_path;
if (cfg->bios)
return strdup(cfg->bios);
return NULL;
}
const char* psxe_cfg_get_pref_path(void) {
return psxe_pref_path();
}
void psxe_cfg_set_pref_path(const char* path) {
if (!path || !*path || g_pref_path)
return;
size_t len = strlen(path);
int needs_slash = path[len - 1] != '/';
g_pref_path = (char*)malloc(len + (needs_slash ? 1 : 0) + 1);
if (!g_pref_path)
return;
memcpy(g_pref_path, path, len);
if (needs_slash)
g_pref_path[len++] = '/';
g_pref_path[len] = '\0';
/* An instrumented (PGO=generate) build writes its counters under this directory. Hooked here
rather than in the Android JNI host because BOTH entry paths that resolve the app's private
files dir — EnsureAchievementsReady() and runVMThread() — already funnel it through this
one function, and it is a no-op after the first call, so this inherits that exactly-once
property for free. Compiles to an empty call in a normal build (see frontend/pgo.c). */
armsx_pgo_set_output_dir(g_pref_path);
}
#undef STR1
#undef STR