mirror of
https://github.com/ARMSX2/ARMSX1.git
synced 2026-08-24 16:53:35 -07:00
1594 lines
61 KiB
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" , ®ion , "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
|