mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
The release commit for 0.10.1 had turned both off by default (replay_workers = 0, tail_early = 0). They stay as 0.10.0 has them: replay_workers = auto, half the processor's physical cores less one (3 on 8 cores), and tail_early = 1. A player's session with all four Testing settings at 0 filled the video memory as before (bug capture 7: 15.5 GiB in use, 20.7 frames a second); what filled it were the images of moved render targets, which the two commits before this one fix. runtime/cfg.c, runtime/ui/menu.cpp, runtime/vk_exec.c and nx2navi.ini.example are back as they were at 0.10.0. The notes of 0.10.1 lose the entry about the defaults and take the fix's entry from the draft for 0.10.2, which goes. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
1254 lines
81 KiB
C
1254 lines
81 KiB
C
// nx2navi.ini: file-backed defaults for the NX2NAVI_* environment variables (docs/controls.md, nx2navi.ini.example).
|
|
// Format: "key = value" lines, '#' or ';' at the start of a line begins a comment, [sections] only group. A key is a
|
|
// variable name without "NX2NAVI_" (an optional NX2NAVI_ prefix is accepted), case-insensitive. A value is the rest of the
|
|
// line, trimmed, optionally in double quotes; an empty value counts as unset. The environment wins over the file.
|
|
//
|
|
// Live settings: cfg_set changes a value in memory (readers see the new pointer; the old string is kept, so a pointer
|
|
// cfg_get handed out stays valid) and cfg_save writes only the changed keys back, keeping the rest of the file.
|
|
// Readers take no lock: an entry is complete before n_entries counts it, and value pointers are swapped atomically.
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "bugcap.h"
|
|
#include "cfg.h"
|
|
#include "cfg_private.h"
|
|
#include "discord_app.h"
|
|
#include "host.h"
|
|
#include "platform/platform.h"
|
|
|
|
#define PREFIX "NX2NAVI_"
|
|
#define PREFIX_LEN (sizeof PREFIX - 1)
|
|
#define CFG_MAX_KEYS 512
|
|
|
|
typedef struct { char *key, *value; int dirty, removed; } CfgEntry; // key upper-case, without the prefix; value NULL after cfg_reset
|
|
|
|
static CfgEntry entries[CFG_MAX_KEYS];
|
|
static int n_entries;
|
|
static unsigned version;
|
|
static PlatMutex set_lock = PLAT_MUTEX_INIT; // cfg_set, cfg_reset, cfg_save
|
|
static uint64_t kept_n, kept_bytes; // set_lock: values cfg_set replaced and kept for readers (cfg_sizes)
|
|
static int save_ok = 1; // the last cfg_save_to wrote (or had nothing to write)
|
|
|
|
// Every name the runtime reads (and the patch sets in generated/), without the prefix. A file key outside this list is
|
|
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
|
|
static const char *const known[] = {
|
|
"CORES", "TM_DISPATCH", "NVN2_CS_TABLE", "SKIP_SHADERS", "MOVIE_DEBAND", "MOVIE_FPS", "MOVIE_SKIP", "MOVIE_OVERLAY", "MOVIES", "NPLN", "NVN2_PROGRAMS", "NVN_TRAP", "OCARINA_REALTIME", "ONESHOT_BUDGET", "ONESHOT_WAIT_MS", "OCARINA_STEP", "PHYSICS_STEPS", "PHYSICS_STEPS_FULL", "PICKUP_COUNT", "WATER_STEPS", "SOUND_STEPS", "FLOOR_PROBE", "FLOOR_SCAN", "FLOOR_WATCH", "FLOOR_CARRY", "MOVIE_PTS", "STEP_STEADY", "VI_HZ", "VI_VRR", "TIME_DUMP", "SHADER_DUMP", "SHOT_AFTER", "SHOT_DIR", "SHOT_EVERY", // Switch 2 port: movie skip (romfs.c), NVN2 proc traps (nvn_procs.c)
|
|
"AA", "AA_LOG", "AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CATCHUP", "AUDIO_CHANNELS", "AUDIO_DEVICE", "AUDIO_GAME_PRIO", "AUDIO_OFF", "AUDIO_PREFILL_MS", "AUDIO_RAMP", "AUDIO_STATS",
|
|
"BARRIER_GAME", "MESH_AHEAD", "MESH_FALLBACK", "MESH_FALLBACK_MB", "MESH_FALLBACK_WORKERS", "PROBE_MESHFB_DIR",
|
|
"CAPTURE_AT", "CAPTURE_DIR", "CAPTURE_REPLAY", "CAPTURE_REPLAY_CAMERA", // bug captures and their replays (bugcap.c)
|
|
"BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT",
|
|
"BIND_LSTICK_RIGHT", "BIND_LSTICK_UP", "BIND_MINUS",
|
|
"BIND_PLUS", "BIND_R", "BIND_RCLICK", "BIND_RIGHT", "BIND_RSTICK_DOWN", "BIND_RSTICK_LEFT", "BIND_RSTICK_RIGHT", "BIND_RSTICK_UP",
|
|
"BIND_SHAKE",
|
|
"BIND_SKIP", "BIND_STATS", "BIND_UP", "BIND_X", "BIND_Y",
|
|
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CACHE_DIE", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COMPUTE_KEEP", "COLOR_CONTRAST", "COLOR_GAMMA",
|
|
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
|
|
"DMABUF", "DMABUF_MB", "DRAW_CRUMBS", "GPA_LATE", "DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_CAM_KEEP", "DLAA_CUT", "DLSS_IDLE_FREE", "DLAA_EDGE_FILL", "DLAA_AT_UPSCALER", "FFX_DEBUG", "FFX_KEYS", "FSR3_SHARPEN", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TARGET_KEEP", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_PREBLOOM", "DLSS_RANGE", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
|
|
"DUMP_LOG", "DUMP_RAW", "DUMP_SHADERS", "DUMP_FILE", "DS3", "LOG_CS_BINDS", "LOG_TEX_FLAGS", "LOG_MESH", "MESH", "POOL", "NATIVE_TENSOR", "LOG_RCOND", "LOG_RT_SIZES", "DUMP_WHITE", "DUMP_AFTER", "DUMP_MOVIE", "SUBMIT_FINISH", "ACQUIRE_WAIT", "CRASH_FILTER_GUARD", "CS_FALLBACK", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES", "GPU_TIMELINE",
|
|
"FFX_PIPE_CACHE", "FLIGHT_SPIKE_MS", "STUTTER_MAP", "PAR_JOBS", "PIPE_RELEASE", "FLIGHT_THUMBS", "FLIGHT_EVERY", "FLIGHT_FLASH", "FLIGHT_FLASH_DIFF", "FLIGHT_FLASH_CELLS", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOG_CULL", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FG_MULT", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREEZE_ASYNC", "FREEZE_BUDGET_US", "FREE_CORES",
|
|
"FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "GPA_TWIN_MAX_KB", "MIRROR_CAPTURE_KB", "WB_SKIP_MB", "GPL", "GPL_DRAW_BUILD", "GPL_FEEDBACK", "GPL_MESH", "PIPE_AHEAD", "PIPE_FAST_WORKERS", "PIPE_FRAME_WAIT_MS", "PIPE_LINK_DRAW_MS", "PIPE_SKIP_STEADY", "DRAW_WRITERS_WAIT", "PROG_LEAD", "GPU", "GPU_INDEX", "GPU_PASSES", "GPU_NAME", "HDR",
|
|
"HDR_GAME_NITS", "HDR_LIVE", "HEAP_LAZY", "HDR_PEAK_NITS", "HDR_UI_NITS",
|
|
// retired: the HDR look's settings of earlier versions, now fixed (vk_hdr.c, shaders/hdr_tonemap.frag); read by
|
|
// nothing, listed so that a file which still sets them logs no unknown key
|
|
"HDR_BLOWOUT", "HDR_CONTRAST", "HDR_EXPOSURE", "HDR_FLARE", "HDR_GAMMA", "HDR_GRADE", "HDR_HIGHLIGHT_SAT",
|
|
"HDR_HIGHLIGHTS", "HDR_HUE", "HDR_HUE_CLIP", "HDR_MATCH_SLOPE", "HDR_MIDGRAY", "HDR_SATURATION", "HDR_SHADOWS",
|
|
"HDR_TONEMAP", "HDR_TONE_SLOPE", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
|
|
"BIG_BUILD_FREE_MB", "BIG_BUILD_MAX_MB", "BUILD_FREE_MB", "BUILD_RESERVE_MB", "LIB_PREBUILD_BIG", "LIB_PREBUILD_BIG_BG", "COMPUTE_WARM_REUSE", "LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LIB_WARM", "LINE_WIDTH", "POOL_SHOWN_FIRST", "POOL_DATA_FIRST", "TEX_REUSE_NAMED", "TEX_OVERWRITTEN_USED", "PIPE_LINK_MS", "FRAME_HOLD_MS", "PACE_SMOOTH", "OQ_BATCH", "OQ_HOLD", "QUAD_INDEX", "CLEAR_SMALL_CPU", "GATE_WAIT_MS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MEM_CENSUS", "MEM_NEED_MB", "MENU_LANG", "MENU_SOUND_VOLUME", "MENU_TEST",
|
|
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_CLICK", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MICROPHONE", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MONITOR", "MOTION", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOUSE_TRACE", "MOVIE_HOST", "MOVIE_PACE", "MOVIE_CLOCK", "TEST_HITCH", "SPRINT_BUTTON", "SPRINT_MODE", "SPRINT_BUTTON_PATCH", "TEXT_SPEED", "TEXT_SPEED_PATCH",
|
|
"MOTION_GUARD_DEGREES", "MOTION_POLICY_TRACE", "SELECTIVE_MOTION", "SELECTIVE_MOTION_RAW", // motion_owner.c, vk_fg_pose.inc
|
|
"SKIN_MOTION", // vk_skin_motion.inc
|
|
"MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK",
|
|
"MV_SURVEY",
|
|
"PIPE_SEED_BOOT", "ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PACE_HZ", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_RES_FIXED", "PATCH_SCENE", "PATCH_WIDE_ASPECT", "PATCH_WIDE_HUD", "PATCH_WIDE_MOVIE", "PATCH_WIDE_OUT", "PATCH_FREECAM", "PATCH_OCCLUSION_OFF", "PATCH_WIDE_SAVE", "PATCH_WIDE_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPELINE_CACHE_MAX_MB", "PIPELINE_CACHE_PLAY", "PIPELINE_CACHE_PLAY_MB", "PIPE_BINARIES", "PIPE_BINARIES_MAX_MB", "PIPE_FEEDBACK", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SEED", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT", "POWER_FULL", "TEX_SAME", "TEX_SAME_BIG", "TEX_FLAT3D", "TEX_KEEP_TAKEN", "TEX_STALE_KEPT", "PRESENT_HELD", "HELD_SYNC",
|
|
"PREPARE_PIPELINES", "PREWARM_FULL", "PREWARM_FULL_S", "PREWARM_WAIT_MS", "PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES", "PROFILE_REPLAY", "PROFILE_SLOW",
|
|
"POOL_FAST", "POOL_GROUP", "POOL_GROUP_CHECK", "POOL_PRESNAP", "POOL_PRESNAP_RUNS",
|
|
"SUBMIT_FINISH_GPU", "SVC_WAITS", "SNAP_WIDEN", "SUBMIT_FINISH_RESULTS", "SUBMIT_FINISH_KINDS", "FOLIAGE", "MIRROR_STAGE", "FLUSH_SKIP_CLEAN", "MIRROR_HOT", "MESH_EMULATION", "MESH_EMU_MB", "MESH_EMU_TASKS", "MESH_EMU_BATCH", "GUEST_OPT", "LOG_TEX_NEW", "DUMP_KEY", "VEC_INDEX_FIX", "PASS_TIMES",
|
|
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
|
|
"REFLEX", "LOW_LATENCY", "RENDER_SCALE", "REPLAY_ASYNC", "REPLAY_PIPE", "REPLAY_RUNS", "REPLAY_SMALL", "TAIL_EARLY", "REPLAY_WORKERS", "RTSS", "RTSS_ENUM", "SCENE_SHARE",
|
|
"PRESNAP_PLACE",
|
|
"REPLAY_CHECK", "RESOLVE_CHECK", "RESTART_WAIT", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
|
|
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RSTICK_DEADZONE", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_HD", "RUMBLE_HD_MS", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
|
|
"SHADER_CACHE_FLUSH", "SHADER_CARRY", "SHADER_COMPRESS", "SHADER_COMPRESS_IDLE_S", "SHADER_MEMO", "SHADOW_BULK_WRITE", "SHADOW_CHECK", "SHADOW_COMPARE", "SHADOW_PCF", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHADOW_TABLE_STORES", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
|
|
"SVC_SLOW_MS",
|
|
"SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_IDLE", "SPLIT_IN_PASS", "SPLIT_US", "SPV_MARK_TEST", "SPV_OVERRIDE", "STICK_DEADZONE", "STICK_SENS",
|
|
"SWKBD_ANSWER",
|
|
"SET_AT", "SWKBD_TEST", "TAA", "DBG_TEST", "DEBUG_MENU", "MENU_TEST_DEBUG", "FSR4", "FSR4_FG", "FG4_VERSION", "FG4_SWAPCHAIN", "TEST_PAD", "TEST_PAD_AT", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "DEPTH_INIT_INLINE", "DEPTH_ALIAS_LOG", "TEX_DISUSE", "TEX_DISUSE_EVICT", "TEX_GEN_UNDO", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TEX_RELEASED_MB", "TEX_RELEASED_MAX", "TEX_RELEASED_HOLD", "TEX_REUSE", "TEX_SPLIT_FILL", "TEX_COMMIT_FREE_MB", "TEX_PROC_COMMIT_MB", "TEX_EVICT", "TEX_COMMIT_EVICT", "MIRROR_EVICT", "VRAM_BUDGET_MB", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "EVO_TRACE", "EVENT_AT_SYNC", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NAN_PRESERVE", "VK_NOOPT", "VK_NO_FLOAT64",
|
|
"VK_PRESENT", "VK_RECONVERGE", "VK_NAN_BITS", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS", "WATER_PROJECTIVE",
|
|
"WAIT_WATCH_S", "WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WB_MERGE", "WB_NO_REUP", "WB_MERGE_MAX", "WB_PROBE", "WB_SPLIT", "WB_LAZY", "WB_LAZY_CHUNK", "WB_LAZY_TOUCH", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "XLATE_PROCS",
|
|
"UPLOAD_BATCH_MB", "VRAM_HEAP_LIMIT", "VRAM_RESERVE_MB", "TEX_CLOBBER", "LOW_END_GPU",
|
|
"ZSTD_CHECK", "ZSTD_NATIVE",
|
|
"BIND_REUSE", "DYN_DELTA", "STG_ARENA", "SUBMIT_BATCH", "SUBMIT_THREAD", "ASTC_GPU_DESWIZZLE", "ASTC_SCRATCH_KEEP", "ASTC_ARENA", "ASTC_BC1",
|
|
"TEXUP_LOG_MS", "DYN_RES", "DYN_RES_TARGET", "DYN_RES_STEP", "TEXTURE_DETAIL", "VRAM_USE", // vram_use: the removed Video memory use row, read by nothing
|
|
"RECORDER_CORE", "RECORDER_PRIO", "REPLAY_TIME", "LOCK_TIME", "POOL_CREATE_BUDGET", "POOL_CREATE_MS", "POOL_FINI_IDLE", "POOL_LOAD_BACKLOG", "POOL_LOAD_MS",
|
|
"VMA_BLOCK_MB", "SYNC_WAKE_ALL", "LOG_ASYNC", "DEVICE_LOST_AT", "STAGING_FAIL_AT", "VRAM_BALLAST_MB",
|
|
"MEM_BUDGET_TEST_VRAM_MB", "MEM_BUDGET_TEST_RAM_MB",
|
|
"TEX_COMPACT", "TEX_COMPACT_MIN_MB", "TEX_COMPACT_FRAME_MB", // texture compaction (vk_texture.c); no menu row
|
|
"STANDIN_FILTER", // the filter of the scene upscaler's stand-in (vk_tonemap.c); no menu row
|
|
// retired: the settings of the co-op of versions up to 0.9.2; read by nothing, listed so that a file which still
|
|
// sets them logs no unknown key
|
|
"COOP", "COOP_BETA", "COOP_NAME", "COOP_SERVER", "COOP_PORT", "COOP_ROOM", "COOP_AUTO", "COOP_DROP",
|
|
"COOP_PROGRESS", "COOP_ITEMS", "COOP_SONGS", "COOP_QUEST", "COOP_KEYS", "COOP_RUPEES", "COOP_AMMO",
|
|
"COOP_TEST_SET", "COOP_ACCEPT", "COOP_REMOVE", "COOP_RESET_POOLS",
|
|
// retired: the settings of the co-op through Steam that the main branch held for a while after 0.9.2 and no
|
|
// release shipped; listed like the ones above, for the file of someone who built from it then and turned it on
|
|
"COOP_STEAM", "COOP_SHARE", "COOP_NAMES", "COOP_CHAT",
|
|
};
|
|
|
|
// The nx2navi.ini section a key cfg_set may write goes to when the file lacks it (nx2navi.ini.example's sections).
|
|
static const struct { const char *key, *section; } sections[] = {
|
|
// the low-end GPU mode's rows in the menu's Game tab (ui/menu.cpp rows_low_end)
|
|
{"TEX_RELEASED_MAX", "performance"}, {"MIRROR_HOT", "performance"}, {"FREEZE_IDLE", "performance"}, {"SUBMIT_FINISH_KINDS", "performance"}, {"FOLIAGE", "performance"},
|
|
{"ZSTD_NATIVE", "performance"},
|
|
{"VRAM_HEAP_LIMIT", "performance"}, {"VRAM_RESERVE_MB", "performance"}, {"UPLOAD_BATCH_MB", "performance"}, {"MESH_EMULATION", "performance"},
|
|
{"MESH_EMU_MB", "performance"}, {"MESH_EMU_TASKS", "performance"}, {"MESH_EMU_BATCH", "performance"}, {"VEC_INDEX_FIX", "performance"},
|
|
{"GUEST_OPT", "performance"}, {"PASS_TIMES", "diagnostics"}, {"DUMP_KEY", "diagnostics"},
|
|
{"FLOOR_PROBE", "diagnostics"}, {"FLOOR_SCAN", "diagnostics"}, {"FLOOR_WATCH", "diagnostics"},
|
|
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"}, {"RTSS", "frame rate"},
|
|
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
|
|
{"PHYSICS_STEPS", "frame rate"}, {"PICKUP_COUNT", "frame rate"}, {"WATER_STEPS", "frame rate"},
|
|
{"SOUND_STEPS", "frame rate"}, {"FLOOR_CARRY", "frame rate"},
|
|
{"MOVIE_PTS", "frame rate"}, {"STEP_STEADY", "frame rate"},
|
|
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
|
|
{"GPU_INDEX", "display"}, {"MONITOR", "display"}, {"OUTPUT_RES", "display"}, {"GAME_ASPECT", "display"}, {"GAME_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
|
|
{"SHARPNESS", "display"}, {"STANDIN_FILTER", "display"}, {"SCENE_SHARE", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
|
|
{"TEXTURE_DETAIL", "display"},
|
|
{"COLOR_PRESET", "display"},
|
|
{"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"},
|
|
{"COLOR_GAMMA", "display"}, {"HDR", "display"}, {"HDR_LIVE", "display"}, {"HDR_PEAK_NITS", "display"}, {"HDR_GAME_NITS", "display"},
|
|
{"HDR_UI_NITS", "display"}, {"SHADOW_RES", "display"},
|
|
{"DLAA", "display"}, {"FSR4", "display"}, {"FSR4_FG", "display"}, {"DLSS", "display"}, {"DLSS_PRESET", "display"}, {"DLSS_IDLE_FREE", "display"}, {"DLSS_RANGE", "display"}, {"DLSS_PREBLOOM", "display"}, {"DLSS_CHAR_MASK", "display"}, {"FXAA", "display"}, {"AA", "display"},
|
|
{"FRAMEGEN", "display"}, {"FRAMEGEN_MULTIPLIER", "display"}, {"FG_MULT", "display"}, {"SKIN_MOTION", "display"},
|
|
{"LOW_LATENCY", "display"},
|
|
{"DEVICE_PROFILE", "display"}, {"ASSUME_FOCUS", "display"},
|
|
{"DISCORD_RPC", "discord"}, {"DISCORD_APP_ID", "discord"}, {"DISCORD_IMAGE", "discord"},
|
|
{"SHADOW_SCALE", "display"}, {"SHADOW_RANGE", "display"}, {"CONTACT_SHADOWS", "display"},
|
|
{"PCSS", "display"},
|
|
{"SHADOW_PCF", "display"},
|
|
{"MODS_DIR", "mods"}, {"MODS_ORDER", "mods"}, {"MODS_DISABLED", "mods"}, {"MODS_REBUILD", "mods"},
|
|
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"}, {"TEXT_SPEED", "language"},
|
|
{"VOLUME", "audio"}, {"AUDIO_CHANNELS", "audio"}, {"AUDIO_DEVICE", "audio"}, {"MICROPHONE", "audio"}, {"MENU_SOUND_VOLUME", "audio"},
|
|
{"AUDIO_RAMP", "audio"}, {"AUDIO_CATCHUP", "audio"},
|
|
{"MOTION", "input"}, {"SPRINT_BUTTON", "input"}, {"SPRINT_MODE", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"}, {"RUMBLE_HD", "input"}, {"DS3", "input"},
|
|
{"PAD_LAYOUT", "input"}, {"PROMPTS", "input"}, {"STICK_DEADZONE", "input"}, {"LSTICK_DEADZONE", "input"}, {"RSTICK_DEADZONE", "input"},
|
|
{"STICK_SENS", "input"}, {"DEBUG_MENU", "input"},
|
|
{"MENU_CURSOR", "input"}, {"MOUSE_MODE", "input"}, {"MOUSE_SENS", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
|
|
{"BIND_A", "input"}, {"BIND_B", "input"}, {"BIND_X", "input"}, {"BIND_Y", "input"}, {"BIND_LCLICK", "input"},
|
|
{"BIND_RCLICK", "input"}, {"BIND_L", "input"}, {"BIND_R", "input"}, {"BIND_ZL", "input"}, {"BIND_ZR", "input"},
|
|
{"BIND_PLUS", "input"}, {"BIND_MINUS", "input"}, {"BIND_LEFT", "input"}, {"BIND_UP", "input"},
|
|
{"BIND_RIGHT", "input"}, {"BIND_DOWN", "input"}, {"BIND_LSTICK_LEFT", "input"}, {"BIND_LSTICK_UP", "input"},
|
|
{"BIND_LSTICK_RIGHT", "input"}, {"BIND_LSTICK_DOWN", "input"}, {"BIND_RSTICK_LEFT", "input"},
|
|
{"BIND_RSTICK_UP", "input"}, {"BIND_RSTICK_RIGHT", "input"}, {"BIND_RSTICK_DOWN", "input"}, {"BIND_SHAKE", "input"},
|
|
{"ASYNC_PREEMPT", "performance"}, {"REPLAY_ASYNC", "performance"}, {"REPLAY_WORKERS", "performance"}, {"REPLAY_PIPE", "performance"}, {"TAIL_EARLY", "performance"}, {"REPLAY_SMALL", "performance"},
|
|
{"FREE_CORES", "performance"},
|
|
{"DYN_RES", "performance"}, {"DYN_RES_TARGET", "performance"}, {"DYN_RES_STEP", "performance"},
|
|
{"RECORDER_CORE", "performance"}, {"RECORDER_PRIO", "performance"}, {"BIND_REUSE", "performance"},
|
|
{"DYN_DELTA", "performance"}, {"STG_ARENA", "performance"}, {"SUBMIT_BATCH", "performance"}, {"SUBMIT_THREAD", "performance"},
|
|
{"BUILD_FREE_MB", "performance"}, {"BUILD_RESERVE_MB", "performance"}, {"MEM_NEED_MB", "diagnostics"},
|
|
{"ASTC_GPU_DESWIZZLE", "performance"}, {"ASTC_SCRATCH_KEEP", "performance"}, {"ASTC_ARENA", "performance"}, {"ASTC_BC1", "performance"}, {"POOL_CREATE_BUDGET", "performance"},
|
|
{"POOL_CREATE_MS", "performance"}, {"POOL_FINI_IDLE", "performance"}, {"FREEZE_BUDGET_US", "performance"}, {"POOL_LOAD_BACKLOG", "performance"}, {"POOL_LOAD_MS", "performance"},
|
|
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"}, {"ONESHOT_BUDGET", "performance"},
|
|
{"PIPE_FRAME_WAIT_MS", "performance"}, {"PIPE_SKIP_STEADY", "performance"}, {"DRAW_WRITERS_WAIT", "performance"},
|
|
{"CACHE_DIE", "performance"}, {"ONE_PER_CORE", "performance"},
|
|
{"PIPE_SEED_BOOT", "performance"}, {"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"LIB_WARM", "performance"}, {"PIPE_LINK_MS", "performance"}, {"PIPE_LINK_DRAW_MS", "performance"}, {"PIPE_AHEAD", "performance"}, {"LINE_WIDTH", "performance"}, {"POOL_SHOWN_FIRST", "performance"}, {"POOL_DATA_FIRST", "performance"}, {"TEX_REUSE_NAMED", "performance"}, {"TEX_OVERWRITTEN_USED", "performance"}, {"PIPE_FAST_WORKERS", "performance"}, {"GPL_FEEDBACK", "performance"}, {"GPL_MESH", "performance"}, {"FRAME_HOLD_MS", "performance"}, {"PACE_SMOOTH", "performance"}, {"OQ_BATCH", "performance"}, {"CLEAR_SMALL_CPU", "performance"}, {"PATCH_OCCLUSION_OFF", "performance"}, {"GATE_WAIT_MS", "performance"}, {"PIPE_PREBUILD", "performance"}, {"PIPE_SEED", "performance"},
|
|
{"OQ_HOLD", "performance"}, {"QUAD_INDEX", "performance"},
|
|
{"COMPUTE_KEEP", "performance"},
|
|
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
|
|
{"TEX_ALIAS", "performance"}, {"DEPTH_INIT_INLINE", "performance"}, {"DEPTH_ALIAS_LOG", "diagnostics"}, {"TEX_DISUSE", "performance"}, {"TEX_DISUSE_EVICT", "performance"}, {"TEX_GEN_UNDO", "performance"}, {"TEX_REUSE", "performance"}, {"TEX_SPLIT_FILL", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"TEX_RELEASED_HOLD", "performance"}, {"TEX_PROC_COMMIT_MB", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"}, {"WATER_PROJECTIVE", "performance"},
|
|
{"SHADER_COMPRESS", "performance"}, {"SHADER_COMPRESS_IDLE_S", "performance"},
|
|
{"BIND_SKIP", "performance"}, {"SHADER_CACHE_FLUSH", "performance"}, {"SHADER_CARRY", "performance"}, {"PIPELINE_CACHE_PLAY", "performance"}, {"PIPELINE_CACHE_PLAY_MB", "performance"}, {"FFX_PIPE_CACHE", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"DESC_TEMPLATE", "performance"},
|
|
{"TEX_CLOBBER", "performance"}, {"LOW_END_GPU", "performance"},
|
|
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_IDLE", "performance"}, {"SPLIT_IN_PASS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
|
|
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"}, {"VK_RECONVERGE", "performance"},
|
|
{"VK_NAN_PRESERVE", "performance"}, {"VK_NAN_BITS", "performance"},
|
|
{"DRAW_CRUMBS", "diagnostics"}, {"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"}, {"STUTTER_MAP", "diagnostics"}, {"PAR_JOBS", "performance"}, {"PIPE_RELEASE", "performance"},
|
|
{"FLIGHT_FLASH", "diagnostics"}, {"FLIGHT_FLASH_DIFF", "diagnostics"}, {"FLIGHT_FLASH_CELLS", "diagnostics"},
|
|
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_MERGE", "diagnostics"}, {"WB_EXACT", "diagnostics"}, {"MEM_CENSUS", "diagnostics"}, {"WB_NO_REUP", "diagnostics"}, {"WB_PROBE", "diagnostics"}, {"WB_SPLIT", "performance"}, {"SHADOW_BULK_WRITE", "performance"}, {"WB_LAZY", "performance"}, {"HEAP_LAZY", "performance"}, {"BARRIER_GAME", "performance"}, {"MESH_AHEAD", "performance"}, {"MESH_FALLBACK", "performance"}, {"MESH_FALLBACK_MB", "performance"}, {"MESH_FALLBACK_WORKERS", "performance"}, {"WB_LAZY_TOUCH", "diagnostics"}, {"WB_LAZY_CHUNK", "performance"}, {"FLUSH_UPLOAD", "diagnostics"}, {"GPA_TWIN_MAX_KB", "diagnostics"}, {"MIRROR_CAPTURE_KB", "diagnostics"}, {"WB_SKIP_MB", "diagnostics"},
|
|
{"WB_MERGE_MAX", "diagnostics"}, {"MV_SURVEY", "diagnostics"}, {"TEXUP_LOG_MS", "diagnostics"},
|
|
{"WAIT_WATCH_S", "diagnostics"},
|
|
{"VMA_BLOCK_MB", "diagnostics"}, {"DEVICE_LOST_AT", "diagnostics"}, {"STAGING_FAIL_AT", "diagnostics"}, {"VRAM_BALLAST_MB", "diagnostics"},
|
|
{"MEM_BUDGET_TEST_VRAM_MB", "diagnostics"}, {"MEM_BUDGET_TEST_RAM_MB", "diagnostics"},
|
|
{"TEX_COMPACT", "performance"}, {"TEX_COMPACT_MIN_MB", "performance"}, {"TEX_COMPACT_FRAME_MB", "performance"},
|
|
};
|
|
|
|
// The settings the menu shows: the value in effect when the key is unset, and what the key accepts. The only place a
|
|
// default lives: consumers read cfg_value / cfg_int / cfg_double / cfg_bool. A row with an empty default has no fixed
|
|
// value (it depends on the machine); its consumer reads cfg_get and keeps an "unset" branch.
|
|
#define LIVE 1
|
|
#define RESTART 0
|
|
// The lazy write-back (memory.c "Lazy write-back") is on where it has run: its fetch inside the access-fault handler
|
|
// has only ever run on Windows. This is the platform's part of the default, which memory.c mem_init goes by to take
|
|
// the access faults; the back end then turns it off for a driver it is not trusted on while the key is unset
|
|
// (wb_lazy_choice.h: AMD's own), and with shadow storage whatever is set (vk_memory.c vk_mem_lazy_decide).
|
|
#ifdef _WIN32
|
|
#define WB_LAZY_DEFAULT "1"
|
|
#else
|
|
#define WB_LAZY_DEFAULT "0"
|
|
#endif
|
|
|
|
static const CfgMeta metas[] = {
|
|
// Screen
|
|
{"FULLSCREEN", "0", "0|1", LIVE}, {"WINDOW_SIZE", "1920x1080", NULL, LIVE},
|
|
{"VK_PRESENT", "mailbox", "mailbox|fifo|immediate", LIVE}, {"GPU_INDEX", "", NULL, RESTART},
|
|
// vk_lowlat.c level_now: the driver's low latency without frame generation (0 off, 1 on, 2 on with boost)
|
|
{"LOW_LATENCY", "0", "0|1|2", LIVE},
|
|
{"MONITOR", "auto", NULL, RESTART}, // auto|primary|<1-based index>|<part of a monitor name> (gfx_window_win.c, gfx_window_sdl.c chosen_monitor)
|
|
{"PERF_OVERLAY", "off", "off|fps|full", LIVE},
|
|
{"OUTPUT_RES", "1920x1080", NULL, RESTART}, {"RENDER_SCALE", "game", "game|50|67|75|100|125|150|200", RESTART},
|
|
{"UPSCALER", "fsr", "fsr|bilinear", LIVE}, {"SHARPNESS", "80", "0..100", LIVE},
|
|
{"STANDIN_FILTER", "easu", "easu|bilinear", LIVE}, // vk_tonemap.c vk_tonemap_record: how the scene is scaled to the picture
|
|
// widescreen.c plain_share: the scene's share of the picture without DLSS or FSR; 1 is the Upscaling row's Native,
|
|
// 0 the game's dynamic resolution. Live within the render-target heap the start made for it (heap_share).
|
|
{"SCENE_SHARE", "0.75", "0..1", LIVE},
|
|
{"DLAA", "0", "0|1", LIVE}, {"DLSS", "dlaa", "dlaa|quality|balanced|performance|ultra_performance", LIVE},
|
|
{"DLSS_PRESET", "default", "default|k|j", LIVE}, {"DLSS_IDLE_FREE", "1", "0|1|2", RESTART}, {"TAA", "dlss", "dlss|fsr3", LIVE}, // vk_dlaa.c fsr3
|
|
{"DLSS_RANGE", "0", "0|1", RESTART}, // vk_dlaa.c range_on: one DLSS feature for the render sizes of a mode
|
|
{"DLSS_PREBLOOM", "0", "0|1", LIVE}, // vk_dlaa.c prebloom_run: DLSS or FSR 3.1 before the bloom, not after the tonemap
|
|
{"FSR4", "0", "0|1", LIVE}, // vk_ffx.cpp vk_ffx_fsr4
|
|
{"FSR4_FG", "0", "0|1", LIVE}, // vk_fg.cpp use4
|
|
{"FRAMEGEN", "off", "off|fsr3|dlss", LIVE}, // vk_fg.cpp, vk_dlssg.c
|
|
{"FRAMEGEN_MULTIPLIER", "2", "2|3|4", LIVE}, // vk_dlssg.c: frames on screen per rendered frame with DLSS
|
|
// the same where DLSS's frame generation runs through NGX itself (gfx_present.c ng_multiplier): 2x to 6x or dynamic
|
|
{"FG_MULT", "2", "2|3|4|5|6|dynamic", LIVE},
|
|
{"DLSS_CHAR_MASK", "65", "0|35|65|100", LIVE}, {"FXAA", "1", "0|1", LIVE},
|
|
// vk_skin_motion.inc skin_enabled: motion vectors of their own for every skinned character, not the player alone
|
|
{"SKIN_MOTION", "1", "0|1", RESTART},
|
|
{"AA", "game", "game|off|fxaa|smaa|taa", LIVE}, // vk_aa.c: what runs in place of the game's SMAA
|
|
{"GAME_ASPECT", "16:9", "16:9|window|21:9|32:9", RESTART}, // widescreen.c
|
|
{"GAME_RES", "1440", "1080|1440|1800|2160|2880|window", RESTART}, // widescreen.c: the picture's height
|
|
{"ASPECT", "fit", "fit|stretch", LIVE}, {"FOV", "100", "70..140", LIVE},
|
|
{"ANISO", "16", "game|2|4|8|16", LIVE}, // vk_texture.c sampler_features, vk_sampler_aniso_live; 16: the game asks 1x or 2x
|
|
{"TEXTURE_DETAIL", "auto", "auto|full|reduced|low", RESTART}, // vk_texture.c "texture detail", read at the first texture
|
|
// colour adjustment (vk_color.c); the preset is the menu's, the four values are what the present uses
|
|
{"COLOR_PRESET", "default", "default|vivid|clarity|custom", LIVE}, {"COLOR_BRIGHTNESS", "0", "-30..30", LIVE},
|
|
{"COLOR_CONTRAST", "100", "70..150", LIVE}, {"COLOR_SATURATION", "100", "50..200", LIVE},
|
|
{"COLOR_GAMMA", "1", "0.8..1.2", LIVE},
|
|
// HDR output (vk_hdr.c): on and off apply at once (the HDR tonemap is spliced into the game's as shaders load
|
|
// whatever is set, vk_hdr_armed); native is the game's own and is asked for once, at start
|
|
// (vk_hdr_needs_restart). HDR_LIVE = 0 leaves the HDR path out of a run that starts with HDR off. The look is
|
|
// fixed: only the three whites are settings.
|
|
{"HDR", "0", "0|1|native", LIVE}, {"HDR_LIVE", "1", "0|1", RESTART},
|
|
{"HDR_PEAK_NITS", "1000", "400..10000", LIVE}, {"HDR_GAME_NITS", "203", "48..500", LIVE},
|
|
{"HDR_UI_NITS", "203", "48..500", LIVE},
|
|
{"DEVICE_PROFILE", "auto", "auto|desktop|deck", RESTART},
|
|
{"SHADOW_RES", "game", "game|256|512|768|1024|1536|2048", RESTART},
|
|
{"SHADOW_SCALE", "1", "1..4", RESTART}, {"SHADOW_COMPARE", "1", "0|1", RESTART}, // vk_texture.c
|
|
{"SHADOW_BULK_WRITE", "0", "0|1", RESTART}, // vk_memory.c shadow_write_back
|
|
// vk_memory.c "Pages a shader stores into through the device address table"
|
|
{"SHADOW_TABLE_STORES", "0", "0|1", RESTART}, {"SHADOW_CHECK", "0", "0|1", RESTART},
|
|
{"SVC_SLOW_MS", "0", "0..60000", RESTART}, // kernel.c kern_svc: log services slower than this
|
|
{"SHADOW_RANGE", "100", "50..400", RESTART}, {"CONTACT_SHADOWS", "0", "0..100", RESTART},
|
|
{"PCSS", "0", "0..200", RESTART},
|
|
// vk_texture.c shadow_pcf: every depth-compare sampler filters linearly (2x2 percentage-closer filtering)
|
|
{"SHADOW_PCF", "0", "0|1", RESTART},
|
|
// Game
|
|
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|165|240|display|unlimited", LIVE}, {"FPS60", "1", "0|1", LIVE},
|
|
{"RTSS", "1", "0|1", RESTART}, // rtss_limit.c: RivaTuner Statistics Server takes the limit where it runs
|
|
{"REALTIME", "1", "0|1", RESTART}, {"CLOTH_REALTIME", "1", "0|1", RESTART},
|
|
{"MODS_REBUILD", "auto", "auto|off", RESTART},
|
|
{"HIGHFPS_FIX", "1", "0|1", RESTART},
|
|
// the set owater of patches-v111.txt (frame_rate.c frame_rate_sim_host): water and floor surfaces' fixed steps
|
|
{"WATER_STEPS", "1", "0|1", RESTART},
|
|
// the set osound of patches-v111.txt (frame_rate.c frame_rate_sound_gate_host): the sound job at 60 Hz above 60 fps
|
|
{"SOUND_STEPS", "1", "0|1", RESTART},
|
|
// the set ofollow of patches-v111.txt (frame_rate.c floor_carry_store): a moving floor carries a character above
|
|
// 100 fps as at 60
|
|
{"FLOOR_CARRY", "1", "0|1", RESTART},
|
|
// frame_rate.c floor_probe: lines about Link on a moving floor (0 off, 1 = 4000 lines, or a count)
|
|
{"FLOOR_PROBE", "0", NULL, RESTART},
|
|
// frame_rate.c floor_scan: finds the moving floor under Link in guest memory (0 off, 1 = 20000 lines, or a count)
|
|
{"FLOOR_SCAN", "0", NULL, RESTART},
|
|
// floor_watch.c: hardware watchpoints on the floor the scan finds, at 60 fps and at 120 or more (0 off, 1 on)
|
|
{"FLOOR_WATCH", "0", "0|1", RESTART},
|
|
{"LANG", "", "ja|en-US|fr|de|it|es|zh-CN|ko|nl|pt|ru|zh-TW|en-GB|fr-CA|es-419|zh-Hans|zh-Hant|pt-BR|pl", RESTART},
|
|
{"OPERATION_MODE", "docked", "docked|handheld", LIVE},
|
|
{"TEXT_SPEED", "normal", "normal|fast|instant", LIVE}, // text_speed.c: the message window's typing
|
|
{"ASSUME_FOCUS", "1", "0|1", LIVE}, // read on every focus change (ov_system.c)
|
|
// Sound, Controls, Menu
|
|
{"VOLUME", "100", "0..100", LIVE}, {"MENU_SOUND_VOLUME", "80", "0..100", LIVE}, {"AUDIO_CHANNELS", "stereo", "auto|stereo|5.1", LIVE}, // audio_mixer.c audio_channels_live
|
|
{"AUDIO_DEVICE", "default", NULL, RESTART}, // default|<1-based index>|<part of a device name> (audio_mixer.c resolve_audio_device)
|
|
{"MICROPHONE", "1", "0|1", RESTART}, // ov_audio_input.c mic_on: 0 refuses the game's input streams
|
|
{"AUDIO_RAMP", "1", "0|1", RESTART}, // audio_mixer.c accumulate_ramp: a changed volume is reached across the frame
|
|
{"AUDIO_CATCHUP", "1", "0|1", RESTART}, // audren.c render_thread: frames owed after a stall are rendered and dropped
|
|
// sprint.c: the sprint button (b the game's B, l3 the left stick's click) and toggle or hold; the Controls tab's
|
|
// Sprint rows (1.1.1)
|
|
{"SPRINT_BUTTON", "off", "off|b|l3", LIVE}, {"SPRINT_MODE", "toggle", "toggle|hold", LIVE},
|
|
{"MOTION", "1", "0|1", LIVE}, {"RUMBLE_STRENGTH", "130", "0..300", LIVE}, {"RUMBLE_CURVE", "0.6", "0.2..1.5", LIVE},
|
|
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "nintendo", "auto|nintendo|xbox|playstation", LIVE},
|
|
{"DS3", "0", "0|1", LIVE}, // input_ds3_win.c ds3_read: read a DualShock 3 directly; off: no HID scan
|
|
// input.c hd_setting: a Switch Pro Controller or Joy-Con gets the game's vibration values as they are
|
|
{"RUMBLE_HD", "0", "0|1", LIVE},
|
|
{"PROMPTS", "nintendo", "nintendo|xbox|playstation|steamdeck|pc", RESTART},
|
|
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"STICK_SENS", "1", "0.1..3", LIVE}, // camera_speed.c cam_speed_tick
|
|
{"DEBUG_MENU", "0", "0|1", LIVE}, // ui/menu.cpp menu_debug_toggle
|
|
{"LSTICK_DEADZONE", "", "0..50", LIVE}, {"RSTICK_DEADZONE", "", "0..50", LIVE},
|
|
{"MOUSE_MODE", "direct", "stick|direct", LIVE},
|
|
{"MOUSE_SENS", "1", "0..10", LIVE}, {"MENU_CURSOR", "1", "0|1", LIVE},
|
|
{"WALK_KEY", "none", NULL, LIVE}, {"WALK_SCALE", "0.51", "0.2..0.9", LIVE},
|
|
{"MENU_LANG", "", "en|ru|pt-BR|zh-Hans", LIVE},
|
|
{"AMIIBO_DIR", "", NULL, LIVE},
|
|
// Discord Rich Presence (discord_rpc.c): on by default (it only talks to a Discord client running on this PC, and
|
|
// does nothing without one); the Game tab of the settings menu and the launcher's Settings switch it. The application's id and art key default to discord_app.h's NX2NAVI_DISCORD_*_DEFAULT.
|
|
{"DISCORD_RPC", "1", "0|1", LIVE},
|
|
{"DISCORD_APP_ID", NX2NAVI_DISCORD_APP_ID_DEFAULT, NULL, LIVE},
|
|
{"DISCORD_IMAGE", NX2NAVI_DISCORD_IMAGE_DEFAULT, NULL, LIVE},
|
|
// Advanced: scheduling
|
|
{"ASYNC_PREEMPT", "1", "0|1|2", RESTART}, {"REPLAY_ASYNC", "0", "0|1", RESTART},
|
|
{"REPLAY_PIPE", "0", "0..4", RESTART},
|
|
{"REPLAY_WORKERS", "auto", "auto|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16", RESTART}, // vk_exec.c rw_init, vk_exec_replay_workers_auto
|
|
// vk_exec.c "the early tail"; on by default since 0.10.0
|
|
{"TAIL_EARLY", "1", "0|1", RESTART},
|
|
{"REPLAY_SMALL", "0", "0..63", RESTART}, // vk_exec.c group_start
|
|
{"FREE_CORES", "0", "0|1", RESTART}, {"TIMER_1MS", "1", "0|1", RESTART},
|
|
// dynamic resolution (dynres.c): the scene follows the GPU time toward DYN_RES_TARGET milliseconds
|
|
{"DYN_RES", "0", "0|1", LIVE}, {"DYN_RES_TARGET", "16.7", "1..100", LIVE}, {"DYN_RES_STEP", "45", "8..600", LIVE},
|
|
// the recording thread: a core of its own (auto: with 12 or more logical processors) and the highest priority
|
|
// (vk_exec.c rec_claim)
|
|
{"RECORDER_CORE", "0", "auto|0|1", RESTART}, {"RECORDER_PRIO", "1", "0|1", RESTART}, {"REPLAY_TIME", "0", "0|1", RESTART}, {"LOCK_TIME", "0", "0|1", RESTART}, {"POOL_CREATE_BUDGET", "16", "1..4096", RESTART}, {"POOL_CREATE_MS", "6", "0..100", RESTART}, {"POOL_FINI_IDLE", "0", "0|1", RESTART}, {"FREEZE_BUDGET_US", "1000", "0..100000", RESTART}, {"POOL_LOAD_BACKLOG", "512", "0..65536", RESTART}, {"POOL_LOAD_MS", "50", "1..1000", RESTART},
|
|
// held submissions handed to the queue together (vk_sync.c, submit batching)
|
|
{"SUBMIT_BATCH", "4", "1..16", RESTART},
|
|
#ifdef _WIN32
|
|
{"SUBMIT_THREAD", "0", "0|1", RESTART}, // vk_device.c mesa_submit_thread: Mesa's submit thread (RADV)
|
|
#else
|
|
{"SUBMIT_THREAD", "1", "0|1", RESTART}, // vk_device.c mesa_submit_thread: Mesa's submit thread (RADV)
|
|
#endif
|
|
{"CACHE_DIE", "1", "0|1|ab", RESTART}, {"ONE_PER_CORE", "0", "0|1|ab", RESTART}, // perf.c cpu_placement
|
|
// shaders and pipelines
|
|
{"PIPE_WAIT", "default", "default|draw", RESTART}, {"PIPE_SKIP", "any", "strict|any|fullscreen", RESTART},
|
|
{"PIPE_LINK_MS", "6", "0..100", RESTART}, // vk_exec_draw.c link_budget
|
|
{"PIPE_LINK_DRAW_MS", "3", "1..50", RESTART}, // vk_exec_draw.c link_draw_ms
|
|
{"PIPE_FRAME_WAIT_MS", "50", "0..500", RESTART}, // vk_exec_draw.c frame_wait
|
|
{"PIPE_SKIP_STEADY", "1", "0|1", RESTART}, // vk_exec.c steady_on
|
|
{"DRAW_WRITERS_WAIT", "1", "0|1", RESTART}, // vk_exec_draw.c writers_wait_on
|
|
{"PREPARE_PIPELINES", "all", "0|mesh|all", RESTART}, // vk_pipeline.c seed_pipelines
|
|
// vk_device.c: task and mesh programs through compute passes (vk_exec_draw.c "mesh fallback"); auto: on a device
|
|
// without mesh shaders. The megabytes are the buffers the passes write (vk_mesh_fb.c).
|
|
{"MESH_FALLBACK", "auto", "auto|0|1", RESTART}, {"MESH_FALLBACK_MB", "128", "16..4096", RESTART},
|
|
{"MESH_FALLBACK_WORKERS", "auto", "auto|1|2|3|4", RESTART}, // vk_exec_draw.c mfb_worker_count
|
|
{"PIPE_AHEAD", "1", "0|1", RESTART}, // vk_exec.c vk_exec_ahead, vk_pipeline.c vk_pipeline_hint
|
|
{"LINE_WIDTH", "1", "0|1", RESTART}, // vk_pipeline.c key_lines
|
|
{"POOL_SHOWN_FIRST", "1", "0|1", RESTART}, // vk_texture.c shown_take
|
|
{"POOL_DATA_FIRST", "1", "0|1", RESTART}, // vk_texture.c data_take
|
|
{"TEX_REUSE_NAMED", "0", "0|1", RESTART}, // vk_texture.c slot_names
|
|
{"TEX_OVERWRITTEN_USED", "1", "0|1", RESTART}, // vk_texture.c drop_overwritten
|
|
{"GPL_FEEDBACK", "1", "0|1", RESTART}, // vk_pipeline.c gpl_feedback
|
|
{"GPL_MESH", "1", "0|1", RESTART}, // vk_pipeline.c mesh_lib_init
|
|
{"PIPE_FAST_WORKERS", "1", "0|1", RESTART}, // vk_pipeline.c lib_worker, fast_take
|
|
{"FRAME_HOLD_MS", "120", "0..500", RESTART}, // vk_backend.c frame_hold
|
|
{"PACE_SMOOTH", "1", "0|1", RESTART}, // vk_backend.c pace_smooth
|
|
{"OQ_BATCH", "1", "0|1", RESTART}, // vk_exec_xfer.c oq_take
|
|
{"CLEAR_SMALL_CPU", "1", "0|1", RESTART}, // vk_exec_xfer.c clear_small
|
|
// vk_exec_xfer.c oq_report_count: a samples-passed report missing draws the full query ring left uncounted
|
|
// repeats its record's last complete count where that is more than the part counted
|
|
{"OQ_HOLD", "0", "0|1", RESTART},
|
|
// vk_exec_draw.c draw_idx_rewritten: indexed quads drawn as two triangles each, indexed line loops closed
|
|
{"QUAD_INDEX", "0", "0|1", RESTART},
|
|
// the patch set occlusion_off (recomp/patches-v111.txt): the game makes no occlusion queries. The row is what
|
|
// gives the set its default (patches.c rt_patches_init).
|
|
{"PATCH_OCCLUSION_OFF", "0", "0|1", RESTART},
|
|
{"GATE_WAIT_MS", "10", "0..100", RESTART}, // vk_exec.c vk_exec_gate_wait
|
|
{"LIB_PREBUILD", "1", "0|1", RESTART}, {"LIB_PREBUILD_THREADS", "", "1..64", RESTART},
|
|
{"LIB_WARM", "1", "0|1|force", RESTART}, // vk_pipeline.c lw_start: the driver's cache of stage libraries, warmed when cold
|
|
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PIPE_SEED", "1", "0|1", RESTART}, {"PIPE_SEED_BOOT", "auto", "0|1|auto", RESTART}, // vk_pipeline.c keys_open_seed
|
|
{"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "", "0|1", RESTART}, // vk_caps.c vk_caps_gpl: by the driver, off with AMD's for RDNA 2 and older
|
|
{"LIB_PREBUILD_BIG_BG", "1", "0|1", RESTART}, {"PIPE_FEEDBACK", "1", "0|1|2", RESTART},
|
|
{"PIPELINE_CACHE_MAX_MB", "", "0..65536", RESTART}, // vk_pipeline.c cache_cap: unset, from the RAM (1024 to 4096)
|
|
{"FFX_PIPE_CACHE", "1", "0|1", RESTART}, // vk_ffx.cpp vk_ffx_proc_addr: FSR 3.1's pipelines through the pipeline caches
|
|
{"PIPELINE_CACHE_PLAY", "1", "0|1", RESTART}, {"PIPELINE_CACHE_PLAY_MB", "64", "1..65536", RESTART}, // vk_pipeline.c "the play cache": what a run compiles goes into a file of its own
|
|
{"PIPE_BINARIES", "0", "0|1", RESTART}, {"ONESHOT_WAIT_MS", "30000", "0..60000", RESTART}, {"ONESHOT_BUDGET", "1", "0|1", RESTART}, {"PIPE_BINARIES_MAX_MB", "1024", "0..65536", RESTART}, {"BIG_BUILD_FREE_MB", "8192", "0..262144", RESTART},
|
|
{"BIG_BUILD_MAX_MB", "4096", "0..262144", RESTART},
|
|
{"BUILD_FREE_MB", "4096", "0..1048576", RESTART}, {"BUILD_RESERVE_MB", "64", "0..65536", RESTART}, // vk_pipeline.c gate_enter
|
|
{"MEM_NEED_MB", "24576", "0..1048576", RESTART}, // memory.c mem_start_note; the default is mem_need.h MEM_NEED_DEFAULT_MB
|
|
// all|<count>: the compute pipelines the boot keeps, those dispatched most recently (vk_pipeline.c, compute-used.bin)
|
|
{"COMPUTE_KEEP", "512", NULL, RESTART},
|
|
{"SHADER_CARRY", "1", "0|1", RESTART}, // gfx_shader.c "Carry": a previous translator's translations until translated again
|
|
// gfx_shader.c "SPIR-V at rest": a shader's SPIR-V is kept zstd-compressed in memory once no pipeline was made of
|
|
// it for the idle seconds; 0 keeps every shader's SPIR-V plain
|
|
{"SHADER_COMPRESS", "1", "0|1", RESTART},
|
|
{"SHADER_COMPRESS_IDLE_S", "30", "1..3600", RESTART},
|
|
// the low-end GPU mode's rows in the menu's Game tab (ui/menu.cpp rows_low_end)
|
|
{"TEX_RELEASED_MAX", "", NULL, RESTART}, // vk_texture.c released_max: "" by the video memory budget, else 1..16384
|
|
{"MIRROR_HOT", "0", "0..1000", RESTART}, {"FREEZE_IDLE", "1800", "0..100000", RESTART},
|
|
{"SUBMIT_FINISH_KINDS", "31", "0..31", RESTART}, // vk_sync.c res_mask
|
|
{"FOLIAGE", "full", "full|reduced|off", RESTART}, // vk_exec_draw.c foliage_skip
|
|
{"VRAM_HEAP_LIMIT", "0", NULL, RESTART}, // vk_device.c vram_heap_limits: auto, 0 or MiB
|
|
{"VRAM_RESERVE_MB", "0", "0..4096", RESTART}, // vk_vma_budget.c: video memory kept free of VMA's allocations
|
|
// vk_memory.c vk_upload_end: MiB of staging before a batch goes early. 0: at 64 MiB, the staging pool's own limit
|
|
// (STG_FLUSH), which also holds for every value above it
|
|
{"UPLOAD_BATCH_MB", "0", NULL, RESTART},
|
|
{"MESH_EMULATION", "0", "0|1", RESTART}, {"MESH_EMU_MB", "256", "32..2048", RESTART},
|
|
{"MESH_EMU_TASKS", "16384", "256..262144", RESTART}, {"MESH_EMU_BATCH", "1", "0|1", RESTART}, {"VEC_INDEX_FIX", "0", "0|auto|1", RESTART},
|
|
{"GUEST_OPT", "1", "1|2", RESTART}, // tools/make_game.py: the next Rebuild the Game
|
|
{"PASS_TIMES", "0", "0|1", RESTART}, {"DUMP_KEY", "", NULL, RESTART}, // vk_crumbs.c, vk_dump.c
|
|
// textures and memory
|
|
{"ASTC", "auto", "auto|gpu|native|cpu", RESTART}, {"MEMORY", "auto", "auto|import|mirror|verify", RESTART},
|
|
{"TEX_ALIAS", "1", "0|1", RESTART}, {"DEPTH_INIT_INLINE", "1", "0|1", RESTART}, {"DEPTH_ALIAS_LOG", "0", "0|1", RESTART}, {"TEX_DISUSE", "1", "0|1|2", RESTART}, {"TEX_DISUSE_EVICT", "0", "0|1", RESTART}, {"TEX_GEN_UNDO", "1", "0|1", RESTART}, {"TEX_REUSE", "1", "0|1", RESTART}, {"TEX_SPLIT_FILL", "1", "0|1|2", RESTART}, {"TEX_VRAM_PRESSURE", "80", "0..100", RESTART},
|
|
// vk_texture.c drop_aged: seconds a released texture is kept unused (vk_tex_release.h)
|
|
{"TEX_RELEASED_HOLD", "180", "0..900", RESTART}, {"ASTC_BC", "", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
|
|
{"TEX_CLOBBER", "0", "0|1", RESTART},
|
|
{"LOW_END_GPU", "0", "0|1", RESTART}, // LOWEND below
|
|
{"STG_ARENA", "1", "0|1", RESTART}, {"ASTC_GPU_DESWIZZLE", "1", "0|1", RESTART},
|
|
{"ASTC_SCRATCH_KEEP", "1", "0|1", RESTART}, {"ASTC_ARENA", "1", "0|1", RESTART}, // vk_astc.c scratch_get, vk_texture.c astc_arena
|
|
// render fixes
|
|
{"LOD_GUARD", "1", "0|1", RESTART}, {"FIX_C0", "1", "0|1", RESTART}, {"FIX_ZERO", "1", "0|1", RESTART},
|
|
{"WATER_PROJECTIVE", "1", "0|1", RESTART}, // vk_pipeline.c water_projective_pair
|
|
{"BIND_SKIP", "1", "0|1", RESTART}, {"SHADER_CACHE_FLUSH", "0", "0|1", RESTART}, {"SHADER_MEMO", "1", "0|1", RESTART}, {"PIPE_MEMO", "1", "0|1", RESTART}, {"TEX_MEMO", "1", "0|1", RESTART},
|
|
{"BIND_REUSE", "1", "0|1", RESTART}, {"DYN_DELTA", "1", "0|1", RESTART},
|
|
{"DESC_TEMPLATE", "1", "0|1", RESTART},
|
|
{"PUSH_SKIP", "1", "0|1", RESTART}, {"SPLIT_DRAWS", "64", "0..1000", RESTART},
|
|
{"SPLIT_US", "1000", "0..20000", RESTART}, {"SPLIT_IDLE", "0", "0|1", RESTART}, {"SPLIT_IN_PASS", "1", "0|1", RESTART},
|
|
// Vulkan
|
|
{"VK_VALIDATION", "0", "0|1", RESTART}, {"VK_DYN_VERTEX", "", "0|1", RESTART},
|
|
{"VK_DYN_A2C", "", "0|1", RESTART}, {"VK_SUBGROUP_PIN", "1", "0|1", RESTART},
|
|
{"VK_RECONVERGE", "1", "0|1", RESTART}, // vk_pipeline.c spirv_reconverge
|
|
{"VK_NAN_PRESERVE", "", "0|1", RESTART}, // vk_pipeline.c spirv_nan_preserve; unset: every driver but NVIDIA's
|
|
{"VK_NAN_BITS", "", "0|1", RESTART}, // vk_pipeline.c spirv_nan_bits; unset: AMD's driver for RDNA 1 and 2
|
|
{"VK_NOOPT", "", "0|1|mesh", RESTART}, // vk_pipeline.c noopt_flag_for; unset: mesh on AMD's driver for RDNA 1 and 2
|
|
// diagnostics
|
|
{"TRACE", "", NULL, RESTART}, {"FLIGHT", "0", "0|1", RESTART}, {"FLIGHT_SPIKE_MS", "100", "0..60000", RESTART},
|
|
// stutter_map.c: hitches by area and cause, and the pipelines and shaders draws met missing (0 off, 1 on)
|
|
{"STUTTER_MAP", "0", "0|1", RESTART},
|
|
// par_jobs.c: the worker pool that splits a large texture's copy out of the game's memory (0: the thread that makes it alone)
|
|
{"PAR_JOBS", "1", "0|1", RESTART},
|
|
// vk_pipeline.c "pipelines no draw has used, given back while video memory is short" (0: every pipeline stays)
|
|
{"PIPE_RELEASE", "1", "0|1", RESTART},
|
|
// flight.c "The flash trigger": on or off, and flight_flash.h flash_test's two thresholds
|
|
{"FLIGHT_FLASH", "1", "0|1", RESTART}, {"FLIGHT_FLASH_DIFF", "24", "4..255", RESTART}, {"FLIGHT_FLASH_CELLS", "3", "1..144", RESTART},
|
|
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_MERGE", "1", "0|1", RESTART}, {"WB_EXACT", "1", "0|1", RESTART}, {"MEM_CENSUS", "0", "0|1", RESTART}, {"WB_NO_REUP", "1", "0|1", RESTART}, {"WB_PROBE", "0", "0|1", RESTART}, {"WB_SPLIT", "1", "0|1|2|3", RESTART}, {"WB_LAZY", WB_LAZY_DEFAULT, "0|1", RESTART}, {"HEAP_LAZY", "0", "0|1", RESTART}, {"WB_LAZY_TOUCH", "0", "0..16", RESTART}, {"WB_LAZY_CHUNK", "0", "0|1", RESTART}, {"FLUSH_UPLOAD", "0", "0|1", RESTART}, {"GPA_TWIN_MAX_KB", "0", "0..1048576", RESTART},
|
|
{"MIRROR_CAPTURE_KB", "0", "0..1048576", RESTART}, {"WB_SKIP_MB", "0", "0..4096", RESTART}, // vk_memory.c capture_cap, wb_skip
|
|
{"WB_MERGE_MAX", "32", "1..256", RESTART}, {"TEXUP_LOG_MS", "0", "0..60000", RESTART}, // vk_texture.c vk_texstat_frame
|
|
{"WAIT_WATCH_S", "0", "0..3600", RESTART}, // kernel.c wait_watch: a lock or thread-end wait this long is logged; 0 off
|
|
{"VMA_BLOCK_MB", "64", "16..256", RESTART}, // vk_headroom.c vk_vma_block_size
|
|
// vk_texture.c compact_on: idle textures move out of sparsely used blocks of video memory; the free space inside
|
|
// the blocks from which a pass starts, and the bytes moved per frame (also the largest texture that moves)
|
|
{"TEX_COMPACT", "0", "0|1", RESTART}, {"TEX_COMPACT_MIN_MB", "256", "64..65536", RESTART},
|
|
{"TEX_COMPACT_FRAME_MB", "8", "1..32", RESTART},
|
|
{"ASTC_BC1", "1", "0|1", RESTART}, // vk_astc.c bc1_on
|
|
{"PASS_CRUMBS", "", "0|1", RESTART}, // vk_crumbs.c pass marks; unset: VK_AMD_buffer_marker only
|
|
{"DRAW_CRUMBS", "0", "0|1", RESTART}, // vk_crumbs.c draw marks: a mark at each change of shaders inside a pass
|
|
{"STAGING_FAIL_AT", "0", "0..1000000000", RESTART}, // vk_memory.c staging_fail_now: a test of give_back
|
|
{"VRAM_BALLAST_MB", "0", "0..131072", RESTART}, // vk_memory.c vk_mem_ballast: a test of a smaller card
|
|
// mem_budget.h: a test that pretends the video memory or the system memory is that many MiB, for the levels the
|
|
// owners that give memory back go by; 0 as it is
|
|
{"MEM_BUDGET_TEST_VRAM_MB", "0", "0..1048576", RESTART},
|
|
{"MEM_BUDGET_TEST_RAM_MB", "0", "0..1048576", RESTART},
|
|
{"DEVICE_LOST_AT", "0", "0..1000000000", RESTART}, // vk_sync.c lost_at_due: a test of the lost device's end
|
|
{"INPUT_RECORD", "", NULL, RESTART},
|
|
};
|
|
|
|
static int is_known(const char *key) {
|
|
for (size_t i = 0; i < sizeof known / sizeof *known; i++)
|
|
if (!strcmp(known[i], key)) return 1;
|
|
return !strncmp(key, "CHEAT_", 6); // cheats.c and the cheat patch sets (tools/cheats.py)
|
|
}
|
|
|
|
// The keys of the file's lines that are not settings of this version (add), lower-case, ", " between them, each once;
|
|
// what did not fit is counted and left out. Written by cfg_load, before any other thread reads settings.
|
|
static char unknown_keys[384];
|
|
static int n_unknown, unknown_cut; // how many; whether the list left some out
|
|
|
|
static void note_unknown(const char *key) {
|
|
char low[64];
|
|
size_t n = 0;
|
|
for (; key[n] && n < sizeof low - 1; n++) {
|
|
// a key of other characters (a line of text with an '=' in it) is counted, not listed: the menu cuts the list
|
|
// by bytes to fit its row
|
|
if (!isalnum((unsigned char)key[n]) && key[n] != '_') break;
|
|
low[n] = (char)tolower((unsigned char)key[n]);
|
|
}
|
|
if (key[n]) {
|
|
n_unknown++, unknown_cut = 1;
|
|
return;
|
|
}
|
|
low[n] = 0;
|
|
size_t have = strlen(unknown_keys);
|
|
for (const char *p = unknown_keys; (p = strstr(p, low)) != NULL; p += n) // a repeated line counts once
|
|
if ((p == unknown_keys || p[-1] == ' ') && (p[n] == ',' || !p[n])) return;
|
|
n_unknown++;
|
|
if (have + n + 3 > sizeof unknown_keys) {
|
|
unknown_cut = 1;
|
|
return;
|
|
}
|
|
snprintf(unknown_keys + have, sizeof unknown_keys - have, "%s%s", have ? ", " : "", low);
|
|
}
|
|
|
|
const char *cfg_unknown_keys(int *count) {
|
|
if (count) *count = n_unknown;
|
|
return unknown_keys;
|
|
}
|
|
|
|
static void clear(void) {
|
|
for (int i = 0; i < n_entries; i++) free(entries[i].key), free(entries[i].value);
|
|
n_entries = 0;
|
|
unknown_keys[0] = 0, n_unknown = unknown_cut = 0;
|
|
}
|
|
|
|
static char *trim(char *s) {
|
|
while (*s == ' ' || *s == '\t') s++;
|
|
char *e = s + strlen(s);
|
|
while (e > s && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\r')) *--e = 0;
|
|
return s;
|
|
}
|
|
|
|
// The upper-case key without the prefix, in place.
|
|
static char *norm_key(char *key) {
|
|
for (char *p = key; *p; p++) *p = (char)toupper((unsigned char)*p);
|
|
return strncmp(key, PREFIX, PREFIX_LEN) ? key : key + PREFIX_LEN;
|
|
}
|
|
|
|
static void add(char *key, char *value, int line) {
|
|
key = norm_key(key);
|
|
size_t vl = strlen(value);
|
|
if (vl >= 2 && value[0] == '"' && value[vl - 1] == '"') value[vl - 1] = 0, value++;
|
|
if (!*key) return;
|
|
if (!is_known(key)) {
|
|
rt_log("cfg: nx2navi.ini line %d: unknown key '%s'", line, key);
|
|
note_unknown(key);
|
|
}
|
|
if (!*value) return; // "key =" leaves the setting at its default
|
|
for (int i = 0; i < n_entries; i++)
|
|
if (!strcmp(entries[i].key, key)) { // a repeated key: the later line wins
|
|
free(entries[i].value);
|
|
entries[i].value = strdup(value);
|
|
return;
|
|
}
|
|
if (n_entries == CFG_MAX_KEYS) return;
|
|
entries[n_entries++] = (CfgEntry){strdup(key), strdup(value), 0, 0};
|
|
}
|
|
|
|
// 0.1.9 had the Xbox prompts as a pad layout: pad_layout = xbox_prompts is pad_layout = nintendo with prompts = xbox.
|
|
// Read as those two, which the menu shows and the next cfg_save writes; a prompts line of the file's own wins. The
|
|
// value still works from the environment (input_pad_layout_pick, input_prompts_pick).
|
|
static void migrate(void) {
|
|
CfgEntry *layout = NULL, *prompts = NULL;
|
|
for (int i = 0; i < n_entries; i++) {
|
|
if (!strcmp(entries[i].key, "PAD_LAYOUT")) layout = &entries[i];
|
|
if (!strcmp(entries[i].key, "PROMPTS")) prompts = &entries[i];
|
|
}
|
|
if (!layout || strcasecmp(layout->value, "xbox_prompts")) return;
|
|
free(layout->value);
|
|
layout->value = strdup("nintendo");
|
|
layout->dirty = 1;
|
|
if (!prompts && n_entries < CFG_MAX_KEYS) entries[n_entries++] = (CfgEntry){strdup("PROMPTS"), strdup("xbox"), 1, 0};
|
|
rt_log("cfg: nx2navi.ini pad_layout = xbox_prompts is read as pad_layout = nintendo and prompts = %s",
|
|
prompts ? prompts->value : "xbox");
|
|
}
|
|
|
|
// A file saved as UTF-16 (Notepad's "Unicode": the byte order mark FF FE or FE FF, then two bytes per character) as
|
|
// UTF-8 in a new buffer; NULL for any other file. Read as bytes such a file ends at the zero after its first character:
|
|
// no line was taken, and every setting kept its default without a word in the log.
|
|
static uint8_t *utf16_text(const uint8_t *d, uint64_t size) {
|
|
if (size < 2 || !((d[0] == 0xFF && d[1] == 0xFE) || (d[0] == 0xFE && d[1] == 0xFF))) return NULL;
|
|
int be = d[0] == 0xFE;
|
|
uint8_t *out = malloc((size_t)(size / 2) * 3 + 1), *o = out;
|
|
if (!out) return NULL;
|
|
for (uint64_t i = 2; i + 1 < size; i += 2) {
|
|
uint32_t c = be ? (uint32_t)d[i] << 8 | d[i + 1] : (uint32_t)d[i + 1] << 8 | d[i];
|
|
if (!c) break;
|
|
if (c >= 0xD800 && c < 0xDC00 && i + 3 < size) { // a pair: one character above U+FFFF
|
|
uint32_t lo = be ? (uint32_t)d[i + 2] << 8 | d[i + 3] : (uint32_t)d[i + 3] << 8 | d[i + 2];
|
|
if (lo >= 0xDC00 && lo < 0xE000) c = 0x10000 + ((c - 0xD800) << 10) + (lo - 0xDC00), i += 2;
|
|
}
|
|
if (c < 0x80) *o++ = (uint8_t)c;
|
|
else if (c < 0x800) *o++ = (uint8_t)(0xC0 | c >> 6), *o++ = (uint8_t)(0x80 | (c & 0x3F));
|
|
else if (c < 0x10000)
|
|
*o++ = (uint8_t)(0xE0 | c >> 12), *o++ = (uint8_t)(0x80 | (c >> 6 & 0x3F)), *o++ = (uint8_t)(0x80 | (c & 0x3F));
|
|
else
|
|
*o++ = (uint8_t)(0xF0 | c >> 18), *o++ = (uint8_t)(0x80 | (c >> 12 & 0x3F)),
|
|
*o++ = (uint8_t)(0x80 | (c >> 6 & 0x3F)), *o++ = (uint8_t)(0x80 | (c & 0x3F));
|
|
}
|
|
*o = 0;
|
|
return out;
|
|
}
|
|
|
|
int cfg_load(const char *path) {
|
|
clear();
|
|
__atomic_add_fetch(&version, 1, __ATOMIC_RELEASE); // the lookup memo (cfg_memo) forgets the freed strings
|
|
uint64_t size = 0;
|
|
uint8_t *data = plat_file_read(path, &size);
|
|
if (!data) return 0;
|
|
uint8_t *wide = utf16_text(data, size);
|
|
if (wide) {
|
|
free(data);
|
|
data = wide, size = strlen((const char *)wide);
|
|
rt_log("cfg: %s is saved as UTF-16; read as that (the menu saves it as UTF-8)", path);
|
|
}
|
|
char *p = (char *)data;
|
|
if (size >= 3 && !memcmp(p, "\xEF\xBB\xBF", 3)) p += 3; // editors add a BOM
|
|
for (int line = 1; *p; line++) {
|
|
char *eol = strchr(p, '\n');
|
|
if (eol) *eol = 0;
|
|
char *s = trim(p);
|
|
char *eq = strchr(s, '=');
|
|
if (*s && *s != '#' && *s != ';' && *s != '[') {
|
|
if (!eq) rt_log("cfg: nx2navi.ini line %d: no '=' in '%s'", line, s);
|
|
else {
|
|
*eq = 0;
|
|
add(trim(s), trim(eq + 1), line);
|
|
}
|
|
}
|
|
if (!eol) break;
|
|
p = eol + 1;
|
|
}
|
|
free(data);
|
|
migrate();
|
|
return 1;
|
|
}
|
|
|
|
// ---- device profiles: defaults for a known machine, below the environment and nx2navi.ini and above the table, so any
|
|
// key the player sets wins and resetting a key in the menu returns to the profile's value. NX2NAVI_DEVICE_PROFILE = auto
|
|
// (default: the Steam Deck when the firmware says so), desktop (none) or deck.
|
|
//
|
|
// The Steam Deck (LCD Jupiter, OLED Galileo; 1280x800, 16:10, 4 Zen 2 cores, RDNA 2 with 8 CUs sharing 16 GB): the game
|
|
// at the screen's 1280x800 with its own 5/6 scene (the aspect patch gives the camera its 16:10, resolution.c, and the
|
|
// romfs-patches-1280x800 overlay the HUD's);
|
|
// fullscreen and vsync, as gamescope composes Game Mode; 40 fps, which divides 40 Hz (and 120 Hz on the OLED); the
|
|
// game's six virtual cores folded onto four, one per physical core (kernel.c cores_enabled); ASTC textures stored as
|
|
// BC3, as the automatic choice gives with 16 GB shared between the CPU and the GPU; and a spinning guest thread handed
|
|
// off after 30 us instead of 50 (kernel.c), with four cores to share.
|
|
// Starting points to replace with measured ones (docs/tester-guide.md, Steam Deck).
|
|
static const struct { const char *key, *value; } DECK[] = {
|
|
{"FULLSCREEN", "1"}, {"WINDOW_SIZE", "1280x800"}, {"OUTPUT_RES", "1280x800"}, {"RENDER_SCALE", "game"},
|
|
{"VK_PRESENT", "fifo"}, {"FPS_LIMIT", "40"}, {"CORES", "4"}, {"ASTC_BC", "1"}, {"SPIN_HANDOFF_US", "30"},
|
|
};
|
|
static int profile = -1; // 0 none, 1 Steam Deck
|
|
|
|
static int read_line(const char *path, char *out, size_t n) {
|
|
FILE *f = fopen(path, "r");
|
|
if (!f) return 0;
|
|
int ok = fgets(out, (int)n, f) != NULL;
|
|
fclose(f);
|
|
out[ok ? strcspn(out, "\r\n") : 0] = 0;
|
|
return ok;
|
|
}
|
|
|
|
// The Steam Deck: Valve's board, product Jupiter or Galileo (/sys/class/dmi/id); SteamDeck=1, which Steam sets on it.
|
|
static int on_steam_deck(void) {
|
|
const char *env = getenv("SteamDeck");
|
|
if (env && !strcmp(env, "1")) return 1;
|
|
char vendor[64], product[64];
|
|
return read_line("/sys/class/dmi/id/board_vendor", vendor, sizeof vendor) && !strcmp(vendor, "Valve") &&
|
|
read_line("/sys/class/dmi/id/product_name", product, sizeof product) &&
|
|
(!strcmp(product, "Jupiter") || !strcmp(product, "Galileo"));
|
|
}
|
|
|
|
static const char *raw_get(const char *name);
|
|
|
|
// low_end_gpu = 1 in nx2navi.ini (NX2NAVI_LOW_END_GPU): defaults for the keys the player does not set, as the device
|
|
// profile gives them. They turn on what was written for older graphics cards and ones short of video memory or bus
|
|
// bandwidth, and measured on a Radeon RX 580: mesh shaders run as compute where the card has none (vk_pipeline.c
|
|
// mesh_emu_on), a texture read after another image drew over its pool memory gets memory of its own (vk_texture.c
|
|
// mark_clobbered), game memory is copied for the GPU while the replay records and the game's wait after a frame's
|
|
// first submission ends once the replay has submitted it (vk_memory.c stage_on, vk_sync.c vk_sync_recorded_wait),
|
|
// texture checks share their write-watch queries (vk_memory.c snap_widen_n), video memory stays within the heaps'
|
|
// budgets (vk_device.c vram_heap_limits, vk_vma_budget.c), large upload batches go to the queue early (vk_memory.c
|
|
// vk_upload_end) and pools the game refills every frame stay in system memory (vk_memory.c hot_check). Without the
|
|
// key each of these keeps what 0.9.1 did. Asynchronous replay (vk_backend.c, NX2NAVI_REPLAY_ASYNC) is not among them:
|
|
// it can show a dark or flickering frame now and then, so the mode leaves it off and its row under the mode's switch
|
|
// turns it on for a player who wants it.
|
|
static const struct { const char *key, *value; } LOWEND[] = {
|
|
{"MESH_EMULATION", "1"}, {"TEX_CLOBBER", "1"}, {"MIRROR_STAGE", "1"}, {"SUBMIT_FINISH_GPU", "0"}, {"SNAP_WIDEN", "8"},
|
|
{"VRAM_HEAP_LIMIT", "auto"}, {"VRAM_RESERVE_MB", "512"}, {"UPLOAD_BATCH_MB", "256"}, {"MIRROR_HOT", "25"},
|
|
};
|
|
// The keys that count only while the mode is on (LOWEND_ONLY): without it each reads as unset, whatever the settings
|
|
// file or the environment says, so nothing of the mode acts unless a player turned it on. The menu shows their rows
|
|
// only then (ui/menu.cpp rows_low_end). Keys the game had before the mode (replay_async, freeze_idle, zstd_native,
|
|
// tex_released_max, astc_scratch_keep) are not among them.
|
|
static const char *const LOWEND_ONLY[] = {
|
|
"MESH_EMULATION", "MESH_EMU_MB", "MESH_EMU_TASKS", "MESH_EMU_BATCH", "TEX_CLOBBER", "MIRROR_STAGE", "MIRROR_HOT",
|
|
"SUBMIT_FINISH_GPU", "SUBMIT_FINISH_RESULTS", "SUBMIT_FINISH_KINDS", "SNAP_WIDEN", "VRAM_HEAP_LIMIT",
|
|
"VRAM_RESERVE_MB", "UPLOAD_BATCH_MB", "FOLIAGE", "FLUSH_SKIP_CLEAN", "VEC_INDEX_FIX", "GUEST_OPT", "PASS_TIMES",
|
|
"DUMP_KEY", "LOG_TEX_NEW", "SVC_WAITS",
|
|
};
|
|
static int lowend_only(const char *key) {
|
|
for (size_t i = 0; i < sizeof LOWEND_ONLY / sizeof *LOWEND_ONLY; i++)
|
|
if (!strcasecmp(LOWEND_ONLY[i], key)) return 1;
|
|
return 0;
|
|
}
|
|
static int lowend = -1;
|
|
static int lowend_of(void) {
|
|
int p = __atomic_load_n(&lowend, __ATOMIC_ACQUIRE);
|
|
if (p >= 0) return p;
|
|
const char *set = raw_get(PREFIX "LOW_END_GPU");
|
|
p = set && *set && strcmp(set, "0") != 0;
|
|
__atomic_store_n(&lowend, p, __ATOMIC_RELEASE);
|
|
return p;
|
|
}
|
|
int cfg_low_end_active(void) { return lowend_of(); }
|
|
static const char *lowend_value(const char *key) {
|
|
if (lowend_of())
|
|
for (size_t i = 0; i < sizeof LOWEND / sizeof *LOWEND; i++)
|
|
if (!strcasecmp(LOWEND[i].key, key)) return LOWEND[i].value;
|
|
return NULL;
|
|
}
|
|
|
|
static int profile_of(void) {
|
|
int p = __atomic_load_n(&profile, __ATOMIC_ACQUIRE);
|
|
if (p >= 0) return p;
|
|
const char *set = raw_get(PREFIX "DEVICE_PROFILE");
|
|
if (set && !strcmp(set, "deck")) p = 1;
|
|
else if (set && *set && strcmp(set, "auto")) p = 0;
|
|
else p = on_steam_deck();
|
|
__atomic_store_n(&profile, p, __ATOMIC_RELEASE);
|
|
return p;
|
|
}
|
|
|
|
const char *cfg_device_profile(void) { return profile_of() ? "deck" : "desktop"; }
|
|
|
|
static const char *raw_get(const char *name) {
|
|
const char *e = getenv(name);
|
|
if (e || strncmp(name, PREFIX, PREFIX_LEN)) return e;
|
|
int n = __atomic_load_n(&n_entries, __ATOMIC_ACQUIRE);
|
|
for (int i = 0; i < n; i++)
|
|
if (!strcasecmp(entries[i].key, name + PREFIX_LEN)) return __atomic_load_n(&entries[i].value, __ATOMIC_ACQUIRE);
|
|
return NULL;
|
|
}
|
|
|
|
static const char *cfg_get_now(const char *name) {
|
|
const char *v = raw_get(name);
|
|
if (strncmp(name, PREFIX, PREFIX_LEN)) return v;
|
|
if (!lowend_of() && lowend_only(name + PREFIX_LEN)) return NULL; // the mode is off: its settings are not read
|
|
if (v) return v;
|
|
if (profile_of())
|
|
for (size_t i = 0; i < sizeof DECK / sizeof *DECK; i++)
|
|
if (!strcasecmp(DECK[i].key, name + PREFIX_LEN)) return DECK[i].value;
|
|
return lowend_value(name + PREFIX_LEN);
|
|
}
|
|
|
|
// Whether name's value comes from the player (the environment or nx2navi.ini), not a profile or the table.
|
|
int cfg_is_set(const char *name) { return raw_get(name) != NULL; }
|
|
|
|
unsigned cfg_version(void) { return __atomic_load_n(&version, __ATOMIC_ACQUIRE); }
|
|
|
|
int cfg_setenv(const char *name, const char *value) {
|
|
int ok = plat_setenv(name, value);
|
|
__atomic_add_fetch(&version, 1, __ATOMIC_RELEASE);
|
|
return ok;
|
|
}
|
|
|
|
int cfg_from_env(const char *name) { return getenv(name) != NULL; }
|
|
|
|
void cfg_set(const char *name, const char *value) {
|
|
if (strncmp(name, PREFIX, PREFIX_LEN) || !value) return;
|
|
char key[64];
|
|
snprintf(key, sizeof key, "%s", name + PREFIX_LEN);
|
|
for (char *p = key; *p; p++) *p = (char)toupper((unsigned char)*p);
|
|
plat_mutex_lock(&set_lock);
|
|
int i = 0, n = n_entries;
|
|
while (i < n && strcmp(entries[i].key, key)) i++;
|
|
if (i < n) {
|
|
if (!entries[i].value || strcmp(entries[i].value, value)) {
|
|
if (entries[i].value) kept_n++, kept_bytes += strlen(entries[i].value) + 1; // cfg_sizes
|
|
__atomic_store_n(&entries[i].value, strdup(value), __ATOMIC_RELEASE); // the old string stays for readers
|
|
entries[i].dirty = 1;
|
|
}
|
|
entries[i].removed = 0; // a value set after cfg_reset replaces the line instead of deleting it
|
|
} else if (n < CFG_MAX_KEYS) {
|
|
entries[n] = (CfgEntry){strdup(key), strdup(value), 1, 0};
|
|
__atomic_store_n(&n_entries, n + 1, __ATOMIC_RELEASE);
|
|
}
|
|
__atomic_add_fetch(&version, 1, __ATOMIC_RELEASE);
|
|
plat_mutex_unlock(&set_lock);
|
|
}
|
|
|
|
int cfg_reset(const char *const *keys, int n) {
|
|
int done = 0;
|
|
plat_mutex_lock(&set_lock);
|
|
for (int k = 0; k < n; k++) {
|
|
char key[64], var[80];
|
|
snprintf(key, sizeof key, "%s", keys[k]);
|
|
const char *name = norm_key(key);
|
|
snprintf(var, sizeof var, PREFIX "%s", name);
|
|
if (getenv(var)) continue; // the environment wins; the file line stays
|
|
int i = 0;
|
|
while (i < n_entries && strcmp(entries[i].key, name)) i++;
|
|
if (i == n_entries) { // no value in memory ("key =" in the file): the line still has to go
|
|
if (i == CFG_MAX_KEYS) continue;
|
|
entries[i] = (CfgEntry){strdup(name), NULL, 0, 1};
|
|
__atomic_store_n(&n_entries, i + 1, __ATOMIC_RELEASE);
|
|
} else if (entries[i].value) {
|
|
__atomic_store_n(&entries[i].value, NULL, __ATOMIC_RELEASE); // the old string stays for readers
|
|
entries[i].dirty = 0;
|
|
entries[i].removed = 1; // cfg_save_to deletes the key's lines
|
|
done++;
|
|
} else {
|
|
entries[i].removed = 1;
|
|
}
|
|
}
|
|
if (done) __atomic_add_fetch(&version, 1, __ATOMIC_RELEASE);
|
|
plat_mutex_unlock(&set_lock);
|
|
return done;
|
|
}
|
|
|
|
int cfg_last_save_ok(void) { return __atomic_load_n(&save_ok, __ATOMIC_ACQUIRE); }
|
|
|
|
void cfg_each_active(void (*cb)(const char *key, const char *value, int from_env, void *ctx), void *ctx) {
|
|
int n = __atomic_load_n(&n_entries, __ATOMIC_ACQUIRE);
|
|
for (int i = 0; i < n; i++) {
|
|
const char *v = __atomic_load_n(&entries[i].value, __ATOMIC_ACQUIRE);
|
|
if (v) cb(entries[i].key, v, 0, ctx);
|
|
}
|
|
for (size_t i = 0; i < sizeof known / sizeof *known; i++) {
|
|
char var[80];
|
|
snprintf(var, sizeof var, PREFIX "%s", known[i]);
|
|
const char *e = getenv(var);
|
|
if (e) cb(known[i], e, 1, ctx);
|
|
}
|
|
}
|
|
|
|
const CfgMeta *cfg_meta(const char *key) {
|
|
char k[64];
|
|
snprintf(k, sizeof k, "%s", key);
|
|
const char *name = norm_key(k);
|
|
for (size_t i = 0; i < sizeof metas / sizeof *metas; i++)
|
|
if (!strcmp(metas[i].key, name)) return &metas[i];
|
|
return NULL;
|
|
}
|
|
|
|
const char *cfg_default(const char *key) {
|
|
char k[64];
|
|
snprintf(k, sizeof k, "%s", key);
|
|
const char *name = norm_key(k);
|
|
if (profile_of())
|
|
for (size_t i = 0; i < sizeof DECK / sizeof *DECK; i++)
|
|
if (!strcasecmp(DECK[i].key, name)) return DECK[i].value;
|
|
const char *low = lowend_value(name);
|
|
if (low) return low;
|
|
const CfgMeta *m = cfg_meta(key);
|
|
return m ? m->def : NULL;
|
|
}
|
|
|
|
const CfgMeta *cfg_meta_at(int i) { return i >= 0 && (size_t)i < sizeof metas / sizeof *metas ? &metas[i] : NULL; }
|
|
|
|
static const char *cfg_value_now(const char *name) {
|
|
const char *e = cfg_get_now(name);
|
|
if (e && *e) return e;
|
|
const CfgMeta *m = cfg_meta(name);
|
|
if (!m) rt_fatal("cfg: %s has no row in the settings table", name);
|
|
return m->def;
|
|
}
|
|
|
|
// Lookup memo. cfg_get reads the environment (getenv walks the whole block comparing names) and then the file's
|
|
// entries and the settings table by name, and several per-draw paths ask for live settings: on the replay thread those
|
|
// lookups were about a sixth of the samples in a play profile (ucrtbase's string routines). Each thread keeps the
|
|
// answers it got per name until the settings version changes (cfg_set, cfg_reset, cfg_load); values stay valid that
|
|
// long because a set keeps the old string for readers and a load bumps the version. NX2NAVI_CFG_MEMO=0 (environment
|
|
// only, read once) asks every time.
|
|
#define CFG_MEMO_N 256
|
|
typedef struct { unsigned ver; uint32_t h; uint8_t kind; char name[55]; const char *v; } CfgMemo;
|
|
static _Thread_local CfgMemo cfg_memo[CFG_MEMO_N];
|
|
static int cfg_memo_on = -1;
|
|
|
|
static const char *cfg_memo_get(const char *name, int kind) {
|
|
if (cfg_memo_on < 0) {
|
|
const char *e = getenv(PREFIX "CFG_MEMO");
|
|
cfg_memo_on = !(e && !strcmp(e, "0"));
|
|
}
|
|
size_t len = strlen(name);
|
|
if (!cfg_memo_on || len >= sizeof cfg_memo[0].name) return kind ? cfg_value_now(name) : cfg_get_now(name);
|
|
uint32_t h = 2166136261u;
|
|
for (size_t i = 0; i < len; i++) h = (h ^ (uint8_t)name[i]) * 16777619u;
|
|
h = h * 2 + (uint32_t)kind;
|
|
CfgMemo *m = &cfg_memo[h % CFG_MEMO_N];
|
|
unsigned ver = __atomic_load_n(&version, __ATOMIC_ACQUIRE) + 1; // 0 marks an empty slot
|
|
if (m->ver == ver && m->h == h && m->kind == kind && !memcmp(m->name, name, len + 1)) return m->v;
|
|
const char *v = kind ? cfg_value_now(name) : cfg_get_now(name); // read after ver: a change meanwhile misses next time
|
|
m->ver = ver, m->h = h, m->kind = (uint8_t)kind, m->v = v;
|
|
memcpy(m->name, name, len + 1);
|
|
return v;
|
|
}
|
|
|
|
const char *cfg_get(const char *name) { return cfg_memo_get(name, 0); }
|
|
const char *cfg_value(const char *name) { return cfg_memo_get(name, 1); }
|
|
|
|
// The row's default as a number, for a set value that does not parse.
|
|
static double default_number(const char *name) {
|
|
return strtod(cfg_meta(name)->def, NULL);
|
|
}
|
|
|
|
double cfg_double(const char *name) {
|
|
const char *v = cfg_value(name);
|
|
char *end;
|
|
double d = strtod(v, &end);
|
|
return end == v || *end || d != d ? default_number(name) : d;
|
|
}
|
|
|
|
int cfg_int(const char *name) {
|
|
const char *v = cfg_value(name);
|
|
char *end;
|
|
long l = strtol(v, &end, 10);
|
|
return end == v || *end ? (int)default_number(name) : (int)l;
|
|
}
|
|
|
|
int cfg_bool(const char *name) { return strcmp(cfg_value(name), "0") != 0; }
|
|
|
|
// ---- the restart-only settings' values at start (cfg_init), which the running game keeps using whatever the menu sets
|
|
// later: cfg_pending compares against them, so the menu says "after restart" only while a restart would change
|
|
// something. One per row of metas, NULL for a live one.
|
|
static const char *at_start[sizeof metas / sizeof *metas];
|
|
|
|
static const char *meta_value(size_t i) {
|
|
char name[80];
|
|
snprintf(name, sizeof name, PREFIX "%s", metas[i].key);
|
|
return cfg_value(name);
|
|
}
|
|
|
|
static void start_values(void) {
|
|
for (size_t i = 0; i < sizeof metas / sizeof *metas; i++)
|
|
if (!metas[i].live) at_start[i] = strdup(meta_value(i)); // a copy: cfg_load (the probes) frees what it read
|
|
}
|
|
|
|
// A value the environment sets never waits: the menu cannot change it and a restart keeps the environment. The command
|
|
// line's --monitor and --audio-device arrive that way after the start values are taken (main.c).
|
|
static int pending_at(size_t i) {
|
|
char name[80];
|
|
if (!at_start[i]) return 0;
|
|
snprintf(name, sizeof name, PREFIX "%s", metas[i].key);
|
|
return !cfg_from_env(name) && strcmp(at_start[i], cfg_value(name)) != 0;
|
|
}
|
|
|
|
int cfg_pending(const char *key) {
|
|
const CfgMeta *m = cfg_meta(key);
|
|
return m ? pending_at((size_t)(m - metas)) : 0;
|
|
}
|
|
|
|
int cfg_pending_count(void) {
|
|
int n = 0;
|
|
for (size_t i = 0; i < sizeof metas / sizeof *metas; i++) n += pending_at(i);
|
|
return n;
|
|
}
|
|
|
|
// ---- cfg_save: the file as lines, each with its line ending
|
|
typedef struct { const char *s; size_t n; } Line; // n includes the '\n' (and '\r') when present
|
|
|
|
static int line_count(const char *p, size_t size) {
|
|
int n = 0;
|
|
for (size_t i = 0; i < size; i++) n += p[i] == '\n';
|
|
return n + 1;
|
|
}
|
|
|
|
// The line's text without its ending, BOM and surrounding blanks, into out.
|
|
static void line_text(const Line *l, int first, char *out, size_t cap) {
|
|
size_t n = l->n;
|
|
const char *s = l->s;
|
|
if (first && n >= 3 && !memcmp(s, "\xEF\xBB\xBF", 3)) s += 3, n -= 3;
|
|
while (n && (s[n - 1] == '\n' || s[n - 1] == '\r')) n--;
|
|
if (n >= cap) n = cap - 1;
|
|
memcpy(out, s, n);
|
|
out[n] = 0;
|
|
char *t = trim(out);
|
|
memmove(out, t, strlen(t) + 1);
|
|
}
|
|
|
|
// The key of a "key = value" line (comment = 0) or of a commented "# key = value" line (comment = 1), normalised,
|
|
// into key; 0 when the line is not one.
|
|
static int line_key(const char *text, int comment, char *key, size_t cap) {
|
|
if (comment) {
|
|
if (*text != '#' && *text != ';') return 0;
|
|
text++;
|
|
while (*text == ' ' || *text == '\t') text++;
|
|
}
|
|
if (!*text || *text == '#' || *text == ';' || *text == '[') return 0;
|
|
const char *eq = strchr(text, '=');
|
|
if (!eq) return 0;
|
|
size_t n = (size_t)(eq - text);
|
|
while (n && (text[n - 1] == ' ' || text[n - 1] == '\t')) n--;
|
|
if (!n || n >= cap) return 0;
|
|
for (size_t i = 0; i < n; i++)
|
|
if (!isalnum((unsigned char)text[i]) && text[i] != '_') return 0;
|
|
char tmp[128];
|
|
memcpy(tmp, text, n);
|
|
tmp[n] = 0;
|
|
snprintf(key, cap, "%s", norm_key(tmp));
|
|
return 1;
|
|
}
|
|
|
|
// The section name of a "[name]" line, lower-case, into out; 0 when the line is not one.
|
|
static int line_section(const char *text, char *out, size_t cap) {
|
|
if (*text != '[') return 0;
|
|
const char *e = strchr(text, ']');
|
|
if (!e) return 0;
|
|
char tmp[128];
|
|
size_t n = (size_t)(e - text - 1);
|
|
if (n >= sizeof tmp) n = sizeof tmp - 1;
|
|
memcpy(tmp, text + 1, n);
|
|
tmp[n] = 0;
|
|
snprintf(out, cap, "%s", trim(tmp));
|
|
for (char *p = out; *p; p++) *p = (char)tolower((unsigned char)*p);
|
|
return 1;
|
|
}
|
|
|
|
typedef struct { char *buf; size_t n, cap; } Out;
|
|
static void put(Out *o, const char *s, size_t n) {
|
|
if (o->n + n + 1 > o->cap) {
|
|
o->cap = (o->n + n + 1) * 2;
|
|
o->buf = realloc(o->buf, o->cap);
|
|
if (!o->buf) rt_fatal("cfg: out of memory writing nx2navi.ini");
|
|
}
|
|
memcpy(o->buf + o->n, s, n);
|
|
o->n += n;
|
|
}
|
|
static void puts_(Out *o, const char *s) { put(o, s, strlen(s)); }
|
|
|
|
static const char *section_of(const char *key) {
|
|
for (size_t i = 0; i < sizeof sections / sizeof *sections; i++)
|
|
if (!strcmp(sections[i].key, key)) return sections[i].section;
|
|
if (!strncmp(key, "CHEAT_", 6)) return "cheats"; // cheats.c: one key per cheat of the cheat file
|
|
return NULL;
|
|
}
|
|
|
|
// A value as cfg_load reads it back: quoted when it starts or ends with a blank (cfg_load trims those) or is itself
|
|
// quoted (cfg_load takes one pair of quotes off). A mod's folder name in mods_order can start with a space.
|
|
static void put_value(Out *o, const char *v) {
|
|
size_t n = strlen(v);
|
|
int quote = n && (v[0] == ' ' || v[0] == '\t' || v[n - 1] == ' ' || v[n - 1] == '\t' ||
|
|
(n >= 2 && v[0] == '"' && v[n - 1] == '"'));
|
|
if (quote) puts_(o, "\"");
|
|
puts_(o, v);
|
|
if (quote) puts_(o, "\"");
|
|
}
|
|
|
|
// "key = value" + eol for entry e (key lower-case)
|
|
static void put_entry(Out *o, const CfgEntry *e, const char *eol) {
|
|
char k[64];
|
|
snprintf(k, sizeof k, "%s", e->key);
|
|
for (char *p = k; *p; p++) *p = (char)tolower((unsigned char)*p);
|
|
puts_(o, k);
|
|
puts_(o, " = ");
|
|
put_value(o, e->value);
|
|
puts_(o, eol);
|
|
}
|
|
|
|
int cfg_save_to(const char *path) {
|
|
plat_mutex_lock(&set_lock);
|
|
int n = n_entries, any = 0;
|
|
for (int i = 0; i < n; i++) any |= entries[i].dirty | entries[i].removed;
|
|
if (!any) {
|
|
__atomic_store_n(&save_ok, 1, __ATOMIC_RELEASE);
|
|
plat_mutex_unlock(&set_lock);
|
|
return 1;
|
|
}
|
|
uint64_t size = 0;
|
|
uint8_t *data = plat_file_read(path, &size);
|
|
if (!data && plat_file_exists(path) && plat_file_size(path) != 0) {
|
|
// the file is there but unreadable (locked, transient error): rebuilding it from nothing would lose its lines
|
|
rt_log("cfg: cannot read %s, nothing written", path);
|
|
__atomic_store_n(&save_ok, 0, __ATOMIC_RELEASE);
|
|
plat_mutex_unlock(&set_lock);
|
|
return 0;
|
|
}
|
|
const char *text = data ? (const char *)data : "";
|
|
int nl = data ? line_count(text, size) : 0;
|
|
Line *lines = calloc((size_t)nl + 1, sizeof *lines);
|
|
// after[i]: entries to write after line i (-1 = none); replace[i]: the entry whose value line i now carries
|
|
int *replace = malloc(sizeof(int) * ((size_t)nl + 1)), *after_key = malloc(sizeof(int) * (size_t)(n + 1));
|
|
char *drop = calloc((size_t)nl + 1, 1); // lines of a reset key
|
|
if (!lines || !replace || !after_key || !drop) rt_fatal("cfg: out of memory writing nx2navi.ini");
|
|
for (int i = 0, at = 0; i < nl; i++) {
|
|
size_t start = (size_t)at;
|
|
while ((uint64_t)at < size && text[at] != '\n') at++;
|
|
if ((uint64_t)at < size) at++;
|
|
lines[i] = (Line){text + start, (size_t)at - start};
|
|
replace[i] = -1;
|
|
}
|
|
if (nl && !lines[nl - 1].n) nl--; // the text ended with a newline: no last empty line
|
|
const char *eol = data && strstr(text, "\r\n") ? "\r\n" : "\n";
|
|
char t[1024], k[128], sec[128];
|
|
for (int e = 0; e < n; e++) {
|
|
after_key[e] = -2; // -2 clean, -1 append in a new section at the end, >= 0 insert after that line
|
|
if (entries[e].removed)
|
|
for (int i = 0; i < nl; i++) {
|
|
line_text(&lines[i], i == 0, t, sizeof t);
|
|
if (line_key(t, 0, k, sizeof k) && !strcmp(k, entries[e].key)) drop[i] = 1;
|
|
}
|
|
if (!entries[e].dirty) continue;
|
|
int last = -1;
|
|
for (int i = 0; i < nl; i++) {
|
|
line_text(&lines[i], i == 0, t, sizeof t);
|
|
if (line_key(t, 0, k, sizeof k) && !strcmp(k, entries[e].key)) last = i;
|
|
}
|
|
if (last >= 0) {
|
|
replace[last] = e;
|
|
continue;
|
|
}
|
|
const char *want = section_of(entries[e].key);
|
|
int in = 0, example = -1, tail = -1;
|
|
for (int i = 0; i < nl && want; i++) {
|
|
line_text(&lines[i], i == 0, t, sizeof t);
|
|
if (line_section(t, sec, sizeof sec)) {
|
|
in = !strcmp(sec, want);
|
|
if (in) tail = i;
|
|
continue;
|
|
}
|
|
if (!in) continue;
|
|
if (*t) tail = i;
|
|
if (line_key(t, 1, k, sizeof k) && !strcmp(k, entries[e].key)) example = i;
|
|
}
|
|
after_key[e] = example >= 0 ? example : tail;
|
|
}
|
|
Out o = {0};
|
|
for (int i = 0; i < nl; i++) {
|
|
if (drop[i]) {
|
|
// the line leaves the file; keys inserted after it still need a line ending before them
|
|
} else if (replace[i] >= 0) {
|
|
// keep the line's indentation and key spelling, put the new value after " = "
|
|
const char *s = lines[i].s;
|
|
size_t len = lines[i].n;
|
|
if (i == 0 && len >= 3 && !memcmp(s, "\xEF\xBB\xBF", 3)) put(&o, s, 3), s += 3, len -= 3;
|
|
const char *eq = memchr(s, '=', len);
|
|
size_t kn = (size_t)(eq - s);
|
|
while (kn && (s[kn - 1] == ' ' || s[kn - 1] == '\t')) kn--;
|
|
put(&o, s, kn);
|
|
puts_(&o, " = ");
|
|
put_value(&o, entries[replace[i]].value);
|
|
int had_eol = len && s[len - 1] == '\n';
|
|
if (had_eol) puts_(&o, eol);
|
|
} else {
|
|
put(&o, lines[i].s, lines[i].n);
|
|
}
|
|
for (int e = 0; e < n; e++)
|
|
if (after_key[e] == i) {
|
|
if (o.n && o.buf[o.n - 1] != '\n') puts_(&o, eol);
|
|
put_entry(&o, &entries[e], eol);
|
|
}
|
|
}
|
|
// keys whose section the file lacks: one new section each, at the end
|
|
for (int e = 0; e < n; e++) {
|
|
if (after_key[e] != -1) continue;
|
|
const char *want = section_of(entries[e].key);
|
|
if (o.n && o.buf[o.n - 1] != '\n') puts_(&o, eol);
|
|
if (want) {
|
|
if (o.n) puts_(&o, eol);
|
|
puts_(&o, "[");
|
|
puts_(&o, want);
|
|
puts_(&o, "]");
|
|
puts_(&o, eol);
|
|
}
|
|
put_entry(&o, &entries[e], eol);
|
|
// the following keys of the same new section go right after this one
|
|
for (int f = e + 1; f < n && want; f++)
|
|
if (after_key[f] == -1 && section_of(entries[f].key) && !strcmp(section_of(entries[f].key), want)) {
|
|
put_entry(&o, &entries[f], eol);
|
|
after_key[f] = -3;
|
|
}
|
|
}
|
|
int ok = plat_file_write_atomic(path, o.buf ? o.buf : "", o.n);
|
|
if (ok)
|
|
for (int e = 0; e < n; e++) entries[e].dirty = entries[e].removed = 0;
|
|
else
|
|
rt_log("cfg: cannot write %s", path);
|
|
__atomic_store_n(&save_ok, ok ? 1 : 0, __ATOMIC_RELEASE);
|
|
free(o.buf);
|
|
free(drop);
|
|
free(after_key);
|
|
free(replace);
|
|
free(lines);
|
|
free(data);
|
|
plat_mutex_unlock(&set_lock);
|
|
return ok;
|
|
}
|
|
|
|
static char ini_path[1024];
|
|
|
|
static int find_ini(void) {
|
|
if (ini_path[0]) return 1;
|
|
char path[1024];
|
|
if (!plat_exe_path(path, sizeof path)) return 0;
|
|
char *cut = plat_last_sep(path);
|
|
if (!cut) cut = strrchr(path, '/');
|
|
if (!cut || cut + 12 >= path + sizeof path) return 0; // room for "nx2navi.ini" and its NUL after the separator
|
|
strcpy(cut + 1, "nx2navi.ini");
|
|
snprintf(ini_path, sizeof ini_path, "%s", path);
|
|
return 1;
|
|
}
|
|
|
|
// A replay of a bug capture (bugcap.c) runs on the capture's settings and leaves the player's nx2navi.ini as it is:
|
|
// nothing is saved, whatever the run sets.
|
|
int cfg_save(void) {
|
|
if (bugcap_replay_dir()) return 1;
|
|
if (find_ini()) return cfg_save_to(ini_path);
|
|
__atomic_store_n(&save_ok, 0, __ATOMIC_RELEASE);
|
|
return 0;
|
|
}
|
|
|
|
// What the file sets, in the log once at start: the settings of this version with their values, in lines of up to
|
|
// about 900 characters, and then the keys that are no settings of this version, which do nothing. A report's log then
|
|
// shows which settings its run had and which of the file's lines had no effect; before, it held only the count of
|
|
// keys, and a player who had added a line this version does not read could take it for the cause of what changed.
|
|
static void log_file_settings(const char *file) {
|
|
char line[1024];
|
|
size_t k = 0;
|
|
for (int i = 0; i < n_entries; i++) {
|
|
if (!entries[i].value || !is_known(entries[i].key)) continue;
|
|
char key[64];
|
|
snprintf(key, sizeof key, "%s", entries[i].key);
|
|
for (char *p = key; *p; p++) *p = (char)tolower((unsigned char)*p);
|
|
// a private setting (cfg_private.h): only that it is set
|
|
const char *value = cfg_private_key(entries[i].key) && entries[i].value[0] ? CFG_PRIVATE_SHOWN : entries[i].value;
|
|
size_t vl = strlen(value);
|
|
if (vl > 200) vl = 200; // a long value (a list of mods) is cut: the line shows that the key is set
|
|
if (k && k + strlen(key) + vl + 5 > 900) {
|
|
rt_log("cfg: %s sets: %s", file, line);
|
|
k = 0;
|
|
}
|
|
k += (size_t)snprintf(line + k, sizeof line - k, "%s%s = %.*s", k ? ", " : "", key, (int)vl, value);
|
|
}
|
|
if (k) rt_log("cfg: %s sets: %s", file, line);
|
|
if (n_unknown)
|
|
rt_log("cfg: %s has %d key%s that %s no setting of this version and %s nothing: %s%s", file, n_unknown,
|
|
n_unknown == 1 ? "" : "s", n_unknown == 1 ? "is" : "are", n_unknown == 1 ? "does" : "do", unknown_keys,
|
|
unknown_cut ? ", ..." : "");
|
|
}
|
|
|
|
static void load_ini(void) {
|
|
// NX2NAVI_CAPTURE_REPLAY=<a bug capture's folder>: the settings that capture was taken with (its settings.ini) stand
|
|
// in for nx2navi.ini, which is neither read nor written in such a run; the environment still wins over them
|
|
const char *replay = bugcap_replay_dir();
|
|
if (replay) {
|
|
char path[1100];
|
|
snprintf(path, sizeof path, "%s/settings.ini", replay);
|
|
int ok = cfg_load(path);
|
|
rt_log(ok ? "cfg: replay of the bug capture in %s: its settings.ini (%d keys) in place of nx2navi.ini, which this run does not write"
|
|
: "cfg: replay of the bug capture in %s: no settings.ini there, so every setting has its default (%d keys); "
|
|
"nx2navi.ini is not read or written",
|
|
replay, n_entries);
|
|
if (ok) log_file_settings("the capture's settings.ini");
|
|
return;
|
|
}
|
|
if (!find_ini()) return;
|
|
if (!cfg_load(ini_path)) {
|
|
rt_log("cfg: no settings file at %s", ini_path);
|
|
return;
|
|
}
|
|
rt_log("cfg: read %s: %d keys", ini_path, n_entries);
|
|
log_file_settings("nx2navi.ini");
|
|
for (int i = 0; i < n_entries; i++) {
|
|
char var[128];
|
|
snprintf(var, sizeof var, PREFIX "%s", entries[i].key);
|
|
if (getenv(var)) rt_log("cfg: %s is set in the environment and overrides nx2navi.ini", var);
|
|
}
|
|
}
|
|
|
|
void cfg_init(void) {
|
|
load_ini();
|
|
__atomic_store_n(&lowend, -1, __ATOMIC_RELEASE); // asked before the file was read: decided again (lowend_of)
|
|
if (profile_of())
|
|
rt_log("cfg: device profile Steam Deck: defaults for the keys nx2navi.ini does not set (NX2NAVI_DEVICE_PROFILE=desktop "
|
|
"turns it off)");
|
|
if (lowend_of())
|
|
rt_log("cfg: low_end_gpu = 1: the settings for older graphics cards are on for the keys nx2navi.ini does not set "
|
|
"(mesh shaders as compute, video memory budgets, staged uploads and the shorter wait for the GPU, frames "
|
|
"prepared on their own thread)");
|
|
start_values();
|
|
}
|
|
|
|
// NX2NAVI_TRACE=perf "mem: sizes" (perf.c): settings entries, and the replaced values cfg_set keeps for readers.
|
|
void cfg_sizes(PerfSizes *s) {
|
|
plat_mutex_lock(&set_lock);
|
|
uint64_t n = (uint64_t)n_entries, kn = kept_n, kb = kept_bytes;
|
|
plat_mutex_unlock(&set_lock);
|
|
perf_size(s, "settings", n, CFG_MAX_KEYS, PERF_SIZE_NONE);
|
|
perf_size(s, "replaced setting values kept", kn, 0, kb);
|
|
}
|