mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
The physics' two water managers and the game's interactive floor simulation (the Deku Tree's ground webs) step their surfaces in fixed steps of 1/60 s, as many whole steps as the frame's time holds, and drop the fraction, so every frame shorter than 1/60 s ran none and the surfaces stood still above 60 fps. The new set owater (water_steps, on by default) calls the page between the division and the game's cut, which keeps the fraction per object; at 60 fps the count is the game's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1286 lines
82 KiB
C
1286 lines
82 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 "cfg.h"
|
|
#include "cfg_private.h"
|
|
#include "coop_app.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", "PICKUP_COUNT", "WATER_STEPS", "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",
|
|
"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", "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", "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_MEMO", "SHADOW_BULK_WRITE", "SHADOW_CHECK", "SHADOW_COMPARE", "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",
|
|
"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",
|
|
"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
|
|
"COOP", "COOP_BETA", "COOP_NAME", "COOP_SERVER", "COOP_PORT", "COOP_ROOM", "COOP_AUTO", "COOP_DROP", // co-op (coop.c); the last three have no menu row
|
|
"COOP_PROGRESS", "COOP_ITEMS", "COOP_SONGS", "COOP_QUEST", "COOP_KEYS", "COOP_RUPEES", "COOP_AMMO", // the room's rules (coop_game.c)
|
|
"COOP_TEST_SET", // test: sets flags by hash at given frames (coop_game.c); no menu row
|
|
"COOP_ACCEPT", // accepts a co-op room's members and rules before anything of it is applied (coop_game.c)
|
|
"COOP_REMOVE", // the room's maker removes a member: its id (coop.c), or a key's fingerprint (coop_game.c); no row (the Co-op tab's Remove)
|
|
"COOP_RESET_POOLS", // the room's maker resets the pools when it changes to a value other than 0 (coop_game.c); no row (the tab's Reset the pools)
|
|
};
|
|
|
|
// 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"},
|
|
{"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"},
|
|
{"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"}, {"VRAM_USE", "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"},
|
|
{"COOP", "coop"}, {"COOP_BETA", "coop"}, {"COOP_NAME", "coop"}, {"COOP_SERVER", "coop"}, {"COOP_PORT", "coop"},
|
|
{"COOP_PROGRESS", "coop"}, {"COOP_ITEMS", "coop"}, {"COOP_SONGS", "coop"}, {"COOP_QUEST", "coop"}, {"COOP_KEYS", "coop"},
|
|
{"COOP_RUPEES", "coop"}, {"COOP_AMMO", "coop"}, {"COOP_ACCEPT", "coop"},
|
|
{"SHADOW_SCALE", "display"}, {"SHADOW_RANGE", "display"}, {"CONTACT_SHADOWS", "display"},
|
|
{"PCSS", "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"},
|
|
{"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"},
|
|
{"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"},
|
|
{"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"},
|
|
{"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
|
|
// video memory use: low, the default, keeps released textures for the hold and frees what features switched off
|
|
// hold; high sets the VRAMHIGH defaults below and keeps those (cfg_vram_keep: vk_dlaa.c idle_release, vk_fsr.c
|
|
// idle_note, vk_aa.c work_idle). balanced, the default before 0.10, reads as low.
|
|
{"VRAM_USE", "low", "low|high", RESTART},
|
|
// 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},
|
|
// 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},
|
|
{"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},
|
|
// Co-op (coop.c): off until the player turns it on. The server is host[:port] of a relay (tools/coop_relay.py), and has no
|
|
// default until coop_app.h names one; the name is at most 24 bytes, empty for Player-xxxx; the port is the local UDP port
|
|
// (0: any). COOP_ROOM, COOP_AUTO, COOP_DROP and COOP_TEST_SET are read with cfg_get, no row.
|
|
{"COOP", "0", "0|1", LIVE}, {"COOP_BETA", "0", "0|1", LIVE}, {"COOP_NAME", "", NULL, LIVE},
|
|
{"COOP_SERVER", NX2NAVI_COOP_SERVER_DEFAULT, NULL, LIVE}, {"COOP_PORT", "0", "0..65535", RESTART},
|
|
// The rules a room made by this player syncs by (coop_state.h, docs/coop.md); a joiner's own are not used in the room.
|
|
{"COOP_PROGRESS", "shared", "shared|own", LIVE}, {"COOP_ITEMS", "shared", "shared|own", LIVE},
|
|
{"COOP_SONGS", "shared", "shared|own", LIVE}, {"COOP_QUEST", "shared", "shared|own", LIVE},
|
|
{"COOP_KEYS", "pool", "pool|own", LIVE}, {"COOP_RUPEES", "own", "own|pool", LIVE}, {"COOP_AMMO", "own", "own|pool", LIVE},
|
|
// Nothing from a room is applied before its members and rules are accepted (coop_game.c): ask (the log names the
|
|
// offer's number, which this setting or the menu accepts), always (every room, the save still copied first), or a number.
|
|
{"COOP_ACCEPT", "ask", 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
|
|
{"TAIL_EARLY", "0", "0|1", RESTART}, // vk_exec.c "the early tail"
|
|
{"REPLAY_SMALL", "0", "0..1024", 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
|
|
// 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
|
|
// 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); vram_use = high sets 0
|
|
{"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},
|
|
// 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
|
|
{"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;
|
|
}
|
|
|
|
// vram_use (NX2NAVI_VRAM_USE, the Screen tab's Video memory use row): low, the default, or high. low keeps a released
|
|
// texture until it has gone unused for the hold (vk_texture.c drop_aged, TEX_RELEASED_HOLD 180 s; vk_tex_release.h has
|
|
// the take-back figures behind it: in play the game took released textures back within a minute or not at all) and
|
|
// frees what a switched-off upscaler or SMAA holds.
|
|
// high sets the VRAMHIGH defaults for the keys the player does not set, after the device profile and the low-end mode:
|
|
// TEX_RELEASED_HOLD 0, released textures stay up to the size bound (a quarter of the video memory budget) for the whole
|
|
// session, as before 0.10; and it keeps what a switched-off feature holds (cfg_vram_keep). balanced, the default before
|
|
// 0.10, and anything else read as low.
|
|
// What the low of the 0.10 previews set and did cost something stays to be set on its own:
|
|
// - TEX_RELEASED_MB 1024: released textures keep at most 1 GiB; those the game takes back after a drop are made and
|
|
// uploaded or decoded again and show their stand-in for a frame (81 to 218 in each 3600 presents of a session on an
|
|
// RTX 5090, vk_texture.c released_cap_bytes).
|
|
// - TEX_VRAM_PRESSURE 70: textures that come back intact give way from 70% of the budget on, not 80% (sweep_pressure),
|
|
// and are made again when next used.
|
|
// - PIPE_SEED_BOOT 0: the shared list's pipelines this install never drew are made at their first draw, not at the
|
|
// start, which leaves out 0.3-0.6 GiB of video memory and 2-4 GB of commit; a session so set missed 1,091 pipelines
|
|
// at a draw, 115 of them for 8 to 34 ms, and their draws were skipped (vk_pipeline.c seed_boot).
|
|
// Unsampled live textures are not dropped in either (tex_disuse_evict stays 0): that eviction left the ground flat and
|
|
// see-through for good (vk_texture.c disuse_evict_on).
|
|
static const struct { const char *key, *value; } VRAMHIGH[] = {
|
|
{"TEX_RELEASED_HOLD", "0"},
|
|
};
|
|
static const char *vram_use_value(const char *key) {
|
|
const char *set = raw_get(PREFIX "VRAM_USE");
|
|
if (!set || strcmp(set, "high")) return NULL;
|
|
for (size_t i = 0; i < sizeof VRAMHIGH / sizeof *VRAMHIGH; i++)
|
|
if (!strcasecmp(VRAMHIGH[i].key, key)) return VRAMHIGH[i].value;
|
|
return NULL;
|
|
}
|
|
int cfg_vram_keep(void) {
|
|
const char *set = cfg_get(PREFIX "VRAM_USE");
|
|
return set && !strcmp(set, "high");
|
|
}
|
|
|
|
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;
|
|
const char *low = lowend_value(name + PREFIX_LEN);
|
|
return low ? low : vram_use_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;
|
|
if ((low = vram_use_value(name))) 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;
|
|
}
|
|
|
|
int cfg_save(void) {
|
|
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(void) {
|
|
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 co-op room's secret, the relay, the player's name: only that they are set (cfg_private.h)
|
|
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: nx2navi.ini sets: %s", 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: nx2navi.ini sets: %s", line);
|
|
if (n_unknown)
|
|
rt_log("cfg: nx2navi.ini has %d key%s that %s no setting of this version and %s nothing: %s%s", 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) {
|
|
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();
|
|
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);
|
|
}
|
|
}
|
|
|
|
int cfg_ini_read(const char *path, const char *key, char *out, size_t cap) {
|
|
if (cap) out[0] = 0;
|
|
uint64_t size = 0;
|
|
uint8_t *data = plat_file_read(path, &size);
|
|
if (!data || !size) { // gone or empty for a moment (an editor saving it), or locked: no reading, not "no value"
|
|
free(data);
|
|
return -1;
|
|
}
|
|
int found = 0;
|
|
char *p = (char *)data;
|
|
if (size >= 3 && !memcmp(p, "\xEF\xBB\xBF", 3)) p += 3; // as cfg_load: editors add a BOM
|
|
for (;;) {
|
|
char *eol = strchr(p, '\n');
|
|
if (eol) *eol = 0;
|
|
char *s = trim(p), *eq = strchr(s, '=');
|
|
if (*s && *s != '#' && *s != ';' && *s != '[' && eq) {
|
|
*eq = 0;
|
|
const char *k = trim(s);
|
|
size_t i = 0;
|
|
while (k[i] && key[i] && tolower((unsigned char)k[i]) == tolower((unsigned char)key[i])) i++;
|
|
if (!k[i] && !key[i]) { // as cfg_load: a repeated key's later line wins, quotes go, "key =" sets nothing
|
|
char *v = trim(eq + 1);
|
|
size_t vl = strlen(v);
|
|
if (vl >= 2 && v[0] == '"' && v[vl - 1] == '"') v[vl - 1] = 0, v++;
|
|
snprintf(out, cap, "%s", v);
|
|
found = *v != 0;
|
|
}
|
|
}
|
|
if (!eol) break;
|
|
p = eol + 1;
|
|
}
|
|
free(data);
|
|
return found;
|
|
}
|
|
|
|
int cfg_ini_now(const char *key, char *out, size_t cap) { return find_ini() ? cfg_ini_read(ini_path, key, out, cap) : 0; }
|
|
|
|
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);
|
|
}
|