Files

803 lines
22 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 "common.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"
"ARMSX modifications Copyright (C) 2026 Moon. All rights reserved.\n"
"Original psxe Copyright (C) 2023 Allkern (Lisandro Alarcon).\n"
"Moon-owned material is proprietary and requires Moon's prior written\n"
"permission. Other components retain their existing licenses. See LICENSE.\n"
"There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A\n"
"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"
"[console]\n"
" region = \"auto\"\n"
"\n"
"# CPU settings\n"
"[cpu]\n"
" execution_mode = \"cached\" # cached | interpreter\n"
"\n"
"# Runtime settings\n"
"[runtime]\n"
" display_scale = 3\n"
" logging_enabled = false\n"
" log_level = 2\n"
" quiet = true\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\n"
#endif
" texture_scale_mode = false\n"
" presentation_upscale = 1 # 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8\n"
" debug_panel = false\n"
" stretch_mode = false\n"
" display_aspect = \"classic\" # classic | square | wide16x9\n"
" wide_upscale = \"480p\" # 480p | 720p | 1080p | 1440p | 2160p\n"
"\n"
"[input]\n"
#if defined(__ANDROID__) || defined(IOS_TARGET)
" mobile_controls = true\n"
#else
" mobile_controls = false\n"
#endif
"\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
cfg->gpu_backend = 0;
#endif
cfg->texture_scale_mode = 0;
cfg->debug_panel = 0;
cfg->stretch_mode = 0;
cfg->display_aspect = 0;
cfg->upscale_height = 480;
}
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;
#endif
int texture_scale_mode = 0;
int debug_panel = 0;
int stretch_mode = 0;
int display_aspect = 0;
int upscale_height = 480;
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);
}
log_info("Parsing settings file...");
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);
}
}
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;
if (!strcasecmp(backend, "sdl-accelerated") || !strcasecmp(backend, "hardware") || !strcasecmp(backend, "hardware (experimental)") || !strcasecmp(backend, "hw") || !strcasecmp(backend, "hw-renderer")) {
gpu_backend = 1;
} else {
gpu_backend = 0;
}
free(s_gpu_backend.u.s);
}
#endif
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_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 {
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;
}
}
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;
if (log_level)
cfg->log_level = log_level - 1;
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;
#endif
cfg->texture_scale_mode = texture_scale_mode;
cfg->debug_panel = debug_panel;
cfg->stretch_mode = stretch_mode;
cfg->display_aspect = display_aspect;
cfg->upscale_height = upscale_height;
cfg->quiet = quiet;
cfg->use_args = use_args;
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();
}
#undef STR1
#undef STR