// 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 #include #include #include #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) "OCARINA_TIMING", "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", "AUDIO_STRICT", "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", "UPSCALER_IDLE_WAIT", "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", "FREEZE_QUIET_MS", "FREEZE_LOAD_HOLD_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", "HANDLE_CENSUS", "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_HARD_ORDER", "TEX_STALE_KEPT", "ASTC_SCRATCH_TRIM", "PRESENT_HELD", "HELD_SYNC", "PRESENT_ORDER", "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", "REFLEX_LIMIT", "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", "WB_LAZY_UNLOCKED", "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", "GEN_SEED", "COPY_RETIRE", "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 // developer switches read with cfg_get, rt_knob or vk_caps.c env_u32, without a menu row (tests/cfg_rows_check.py) "AUDIO_DUMP", "DEFER_STAMP", "DEPTH_TWIN", "DLAA_ZFIT", "DUMP_SKIP_RUNS", "DUMP_TEX", "DUMP_UBO", "EVENT_ORDER", "GATHER_BIAS", "PIN_SWEEP", "REC_CHECK", "REC_DELAY_US", "REG_ORDER", "SHADOW", "ZERO_LOCALS", // hang_watch.c: the hang watch's seconds "HANG_WATCH_S", // 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"}, {"OCARINA_TIMING", "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"}, {"UPSCALER_IDLE_WAIT", "display"}, {"FRAMEGEN", "display"}, {"FRAMEGEN_MULTIPLIER", "display"}, {"FG_MULT", "display"}, {"SKIN_MOTION", "display"}, {"LOW_LATENCY", "display"}, {"REFLEX_LIMIT", "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"}, {"AUDIO_STRICT", "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"}, {"FREEZE_QUIET_MS", "performance"}, {"FREEZE_LOAD_HOLD_MS", "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_UNLOCKED", "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"}, {"PRESENT_ORDER", "diagnostics"}, {"HANDLE_CENSUS", "diagnostics"}, {"HANG_WATCH_S", "diagnostics"}, {"VMA_BLOCK_MB", "diagnostics"}, {"DEVICE_LOST_AT", "diagnostics"}, {"STAGING_FAIL_AT", "diagnostics"}, {"VRAM_BALLAST_MB", "diagnostics"}, {"GEN_SEED", "diagnostics"}, {"COPY_RETIRE", "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>| (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 // vk_dlaa.c vk_upscaler_idle_wait: 1 waits for the device to be idle wherever an upscaler lets go of something, as up // to 0.10.1 (what was let go of still goes once the GPU is past it, behind its present gate) {"UPSCALER_IDLE_WAIT", "0", "0|1", RESTART}, {"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}, // vk_dlssg.c limit_allowed: with DLSS Frame Generation, Reflex holds the frame limit where each frame starts; 0 leaves // it to the game's own limiter {"REFLEX_LIMIT", "1", "0|1", 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}, // the set orecord of patches-v111.txt: the ocarina recorder's clock keeps each frame's fraction above 60 fps {"OCARINA_TIMING", "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>| (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 // audren.c audio_bad: 1 ends the process at the first game audio value the renderer cannot use, as 0.10.1 did for // most of them (a limiter time that is not a number and a negative mix buffer count end it too) {"AUDIO_STRICT", "0", "0|1", RESTART}, // 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", "0", "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"; off by default (on in 0.10.0 only) {"TAIL_EARLY", "0", "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}, // vk_memory.c freeze_pass: the backoff's quiet time and the hold around area loads {"FREEZE_QUIET_MS", "5000", "0..60000", RESTART}, {"FREEZE_LOAD_HOLD_MS", "3000", "0..30000", 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 and on AMD's Windows driver for RDNA 1 and 2 (vk_mesh_path.h). 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|: 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}, {"WB_LAZY_UNLOCKED", "1", "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 // vk_backend.c present_order_on: a frame's bookkeeping between two replays (vk_frame_owed.h); 0 at the present {"PRESENT_ORDER", "1", "0|1", RESTART}, // perf.c hc_main: with NX2NAVI_TRACE=perf, the process's handles by kind when their count grew {"HANDLE_CENSUS", "0", "0|1", RESTART}, // hang_watch.c: a report when no frame is shown with the window in front, or the GPU does not finish, this long; 0 off {"HANG_WATCH_S", "20", "0..600", RESTART}, {"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 // vk_memory.c gen_seed: a test that starts the page write generations at this value (at most 2^62) {"GEN_SEED", "0", "0..4611686018427387904", RESTART}, // 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 // nvn_api_cmd.c retire_pass: counts the recording copies whose control memory the game reused; frees nothing {"COPY_RETIRE", "0", "0|1", RESTART}, {"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; } // The table holds CFG_MAX_KEYS keys and keeps none past them: a file's later keys were not read, and a menu change was // lost, without a word (every cheat ever switched keeps a line, cheats.c cheats_set). A line says so once per load, and // once a run for the changes (cfg_set, cfg_reset, migrate). static int full_load_told, full_set_told; static void full_set(const char *key, const char *what) { if (!full_set_told++) rt_log("cfg: the settings table is full (%d keys): %s %s", CFG_MAX_KEYS, key, what); } 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; full_load_told = 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 int entry_line[CFG_MAX_KEYS]; // the file line each entry's value came from (cfg_load's check_values) 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); entry_line[i] = line; return; } if (n_entries == CFG_MAX_KEYS) { if (!full_load_told++) rt_log("cfg: nx2navi.ini has more than %d keys: line %d (%s) and the new keys after it are not read", CFG_MAX_KEYS, line, key); return; } entry_line[n_entries] = line; entries[n_entries++] = (CfgEntry){strdup(key), strdup(value), 0, 0}; } // ---- values outside their rows' sets, logged at load (the values are read as they are written) // Hand-edited files had values no consumer takes, and nothing said so: "pace_smooth = 0 ; off" keeps the comment in // the value, which cfg_bool reads as on; "fullscreen = false" is on too; "sharpness = 250" is past 0..100. Only the // sets that are the consumers' own are checked: 0|1, ranges lo..hi, and lists of words. A list with a number in it is // the menu's presets, and its consumer takes more (fps_limit any rate from 30 to 240, render_scale 25..400), so it is // left alone. A list of words names the menu's choices; a consumer may take more (dlss_preset any letter a..m), so the // line says no more than that. At most CHECK_LINES lines per load, then a count. #define CHECK_LINES 16 static int misfits; // NULL when v fits the set, else what the log line says of it (in buf). static const char *misfit(const char *set, const char *v, char *buf, size_t cap) { if (!strcmp(set, "0|1")) return strcmp(v, "0") && strcmp(v, "1") ? "is not 0 or 1" : NULL; const char *dots = strstr(set, ".."); if (dots && !strchr(set, '|')) { char lo_s[32], *end; // the low end apart: strtod would take "0." of "0..100" for a number size_t ln = (size_t)(dots - set); if (!ln || ln >= sizeof lo_s) return NULL; memcpy(lo_s, set, ln), lo_s[ln] = 0; double lo = strtod(lo_s, &end); if (*end) return NULL; double hi = strtod(dots + 2, &end); if (end == dots + 2 || *end) return NULL; // any number: some rows with whole ends take fractions (dyn_res_target 16.7 in 1..100) double x = strtod(v, &end); if (end != v && !*end && x == x && x >= lo && x <= hi) return NULL; snprintf(buf, cap, "is not a number in %s", set); return buf; } for (const char *m = set; *m;) { // a member that is a number: the menu's presets, not checked char *end; size_t n = strcspn(m, "|"); strtod(m, &end); if (end == m + n) return NULL; m += n + (m[n] == '|'); } for (const char *m = set; *m;) { size_t n = strcspn(m, "|"); if (strlen(v) == n && !strncasecmp(m, v, n)) return NULL; m += n + (m[n] == '|'); } snprintf(buf, cap, "is none of the menu's choices %s", set); return buf; } // key: upper case without the prefix; line 0: the value comes from the environment. static void check_value(const char *key, const char *v, int line) { const CfgMeta *m = cfg_meta(key); if (!m) return; char buf[160]; const char *bad = m->values ? misfit(m->values, v, buf, sizeof buf) : NULL; int comment = strstr(v, " ;") || strstr(v, " #") || strstr(v, "\t;") || strstr(v, "\t#"); if (!bad && !comment) return; if (++misfits > CHECK_LINES) return; char low[64], where[96]; size_t n = 0; for (; key[n] && n < sizeof low - 1; n++) low[n] = (char)tolower((unsigned char)key[n]); low[n] = 0; if (line) snprintf(where, sizeof where, "nx2navi.ini line %d: %s", line, low); else snprintf(where, sizeof where, "the environment's " PREFIX "%s", key); rt_log("cfg: %s = '%s' %s%s%s", where, v, bad ? bad : "", bad && comment ? "; " : "", comment ? "a ';' or '#' after the value is part of it (a comment goes on a line of its own)" : ""); } static void check_end(void) { if (misfits > CHECK_LINES) rt_log("cfg: %d more values outside their settings' choices are not listed", misfits - CHECK_LINES); misfits = 0; } static void check_values(void) { for (int i = 0; i < n_entries; i++) if (entries[i].value && entry_line[i]) check_value(entries[i].key, entries[i].value, entry_line[i]); check_end(); } // 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) entry_line[n_entries] = 0, entries[n_entries++] = (CfgEntry){strdup("PROMPTS"), strdup("xbox"), 1, 0}; else if (!prompts) full_set("PROMPTS", "= xbox is not kept"); 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(); check_values(); // after migrate: a value it rewrites is checked as the game reads it 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); } else { full_set(key, "is not kept"); } __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) { full_set(name, "keeps its line in the file"); 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; 0 for a key without a row, whose set value // cfg_value hands out without one (tests/cfg_rows_check.py keeps such reads guarded). static double default_number(const char *name) { const CfgMeta *m = cfg_meta(name); return m ? strtod(m->def, NULL) : 0; } 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=: 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(); for (size_t i = 0; i < sizeof metas / sizeof *metas; i++) { // the environment's values, as the file's (cfg_load) char var[80]; snprintf(var, sizeof var, PREFIX "%s", metas[i].key); const char *e = getenv(var); if (e && *e) check_value(metas[i].key, e, 0); } check_end(); __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); }