mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
A review of the DLSS and frame generation paths against NVIDIA's guides (the DLSS programming guide 310.6.0, the DLSS-FG guide 310.7.0, Streamline 2.14.16's guides and headers). What follows them: the texture bias (log2(render width / output width) - 1 on top of the game's own, at each change of the mode), the jitter (Halton, 8 x (output / render)^2 phases and 16 at least, in render pixels within half a pixel, the scene drawn with it and the motion vectors made without it), the creation flags (HDR, vectors at the render size, auto-exposure, depth not inverted) and the constants Streamline gets (unjittered matrices, the jitter in pixels, the vectors' scale of one over the render size). What did not: vk_dlssg.c holds a change of generation's state for FG_SETTLE_FRAMES frames, so for up to 12 frames after the depth, the motion vectors or the camera went away (a menu, a fade, a load) generation stayed on and nx2sl_frame set no constants and tagged nothing. Streamline's guide asks for the constants at every frame and for null tags where depth or motion vectors are not valid; it logged "Unable to find 'common' constants for frame" and "Failed to find global tag 'kBufferTypeDepth'" and used the frame before's (in every play log with frame generation). Such a frame now gets the last frame's constants with reset set and the three tags as null. Not run in a game yet: nx2navi_dlssg.dll builds and exports its 15 functions. What the guides leave open: the sign of the jitter offsets against the shift of the picture, which NVIDIA says to find by trying, and the bias reaches only the tiled, trilinear samplers, not every texture of the scene. For both there is a test switch now, read once: NX2NAVI_DLAA_JITTER_TEST=<x>,<y> tells the upscaler the jitter times two factors, and NX2NAVI_DLSS_LOD_BIAS_TEST=<bias> puts a bias in the rule's place. Neither was measured: the runs that would do it start the game. The line about the presents a second (the commit before) came at 59 a second under a limit of 60: it comes under 56 now. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
1457 lines
92 KiB
C
1457 lines
92 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. For a setting with a set of values (a row
|
|
// of metas[] with values), a blank followed by ';' or '#' starts a comment after the value too (cut_comment). A switch
|
|
// (values 0|1) reads false, off and no as 0 and true, on and yes as 1 (switch_word). The environment wins over the
|
|
// file.
|
|
//
|
|
// Live settings: cfg_set changes a value in memory (readers see the new pointer; the old string is kept, so a pointer
|
|
// cfg_get handed out stays valid) and cfg_save writes only the changed keys back, keeping the rest of the file.
|
|
// Readers take no lock: an entry is complete before n_entries counts it, and value pointers are swapped atomically.
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "bugcap.h"
|
|
#include "cfg.h"
|
|
#include "cfg_private.h"
|
|
#include "discord_app.h"
|
|
#include "host.h"
|
|
#include "platform/platform.h"
|
|
|
|
#define PREFIX "NX2NAVI_"
|
|
#define PREFIX_LEN (sizeof PREFIX - 1)
|
|
#define CFG_MAX_KEYS 512
|
|
|
|
typedef struct { char *key, *value; int dirty, removed; } CfgEntry; // key upper-case, without the prefix; value NULL after cfg_reset
|
|
|
|
static CfgEntry entries[CFG_MAX_KEYS];
|
|
static int n_entries;
|
|
static unsigned version;
|
|
static PlatMutex set_lock = PLAT_MUTEX_INIT; // cfg_set, cfg_reset, cfg_save
|
|
static uint64_t kept_n, kept_bytes; // set_lock: values cfg_set replaced and kept for readers (cfg_sizes)
|
|
static int save_ok = 1; // the last cfg_save_to wrote (or had nothing to write)
|
|
|
|
// Every name the runtime reads (and the patch sets in generated/), without the prefix. A file key outside this list is
|
|
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
|
|
static const char *const known[] = {
|
|
"CORES", "TM_DISPATCH", "NVN2_CS_TABLE", "SKIP_SHADERS", "MOVIE_DEBAND", "MOVIE_FPS", "MOVIE_SKIP", "MOVIE_OVERLAY", "MOVIES", "NPLN", "NVN2_PROGRAMS", "NVN_TRAP", "OCARINA_REALTIME", "ONESHOT_BUDGET", "ONESHOT_WAIT_MS", "OCARINA_STEP", "PHYSICS_STEPS", "PHYSICS_STEPS_FULL", "PICKUP_COUNT", "WATER_STEPS", "SOUND_STEPS", "FLOOR_PROBE", "FLOOR_SCAN", "FLOOR_WATCH", "FLOOR_CARRY", "MOVIE_PTS", "STEP_STEADY", "VI_HZ", "VI_VRR", "TIME_DUMP", "SHADER_DUMP", "SHOT_AFTER", "SHOT_DIR", "SHOT_EVERY", // Switch 2 port: movie skip (romfs.c), NVN2 proc traps (nvn_procs.c)
|
|
"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", "MESH_FALLBACK_HOLD", "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_JITTER_TEST", "DLSS_LOD_BIAS_TEST", "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",
|
|
"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_RATE_LOW", "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 settings the low-end GPU mode had rows for up to 0.10.1 (lowend_set, below)
|
|
{"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"}, {"HDR_GAMMA", "display"}, {"HDR_TONEMAP", "display"}, {"HDR_EXPOSURE", "display"},
|
|
{"HDR_HIGHLIGHTS", "display"}, {"HDR_SHADOWS", "display"}, {"HDR_CONTRAST", "display"}, {"HDR_SATURATION", "display"},
|
|
{"HDR_HIGHLIGHT_SAT", "display"}, {"HDR_BLOWOUT", "display"}, {"HDR_FLARE", "display"}, {"HDR_HUE", "display"},
|
|
{"HDR_GRADE", "display"}, {"HDR_MIDGRAY", "display"}, {"HDR_MATCH_SLOPE", "display"}, {"HDR_TONE_SLOPE", "display"},
|
|
{"HDR_HUE_CLIP", "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"}, {"MESH_FALLBACK_HOLD", "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>|<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
|
|
// 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, off, the tone mapper and its grading 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 grading sliders: 50 leaves the picture as it is, exposure in percent. HDR_GAMMA is the display the present
|
|
// shows the picture as, with every tone mapper; off shows the sRGB code as it decodes, the dark tones lighter.
|
|
{"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}, {"HDR_GAMMA", "2.2", "off|2.2|2.4", LIVE},
|
|
{"HDR_TONEMAP", "psychov35", "vanilla|psychov35|renodrt", LIVE}, {"HDR_EXPOSURE", "100", "0..200", LIVE},
|
|
// the observer-response mapper's: where scene grey lands, its slope there matched to the game's curve, that slope
|
|
// (50 unchanged) and how much of the game's hue bend at its clamp bright colours get
|
|
{"HDR_MIDGRAY", "vanilla", "render|vanilla", LIVE}, {"HDR_MATCH_SLOPE", "1", "0|1", LIVE},
|
|
{"HDR_TONE_SLOPE", "50", "0..100", LIVE}, {"HDR_HUE_CLIP", "50", "0..100", LIVE},
|
|
{"HDR_HIGHLIGHTS", "50", "0..100", LIVE}, {"HDR_SHADOWS", "50", "0..100", LIVE}, {"HDR_CONTRAST", "50", "0..100", LIVE},
|
|
{"HDR_SATURATION", "50", "0..100", LIVE}, {"HDR_HIGHLIGHT_SAT", "50", "0..100", LIVE},
|
|
{"HDR_BLOWOUT", "0", "0..100", LIVE}, {"HDR_FLARE", "0", "0..100", LIVE}, {"HDR_HUE", "100", "0..100", LIVE},
|
|
{"HDR_GRADE", "100", "0..100", 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>|<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
|
|
// 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", "auto", "auto|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16", RESTART}, // vk_exec.c rw_init, vk_exec_replay_workers_auto
|
|
// vk_exec.c "the early tail"; on by default since 0.10.0
|
|
{"TAIL_EARLY", "1", "0|1", RESTART},
|
|
{"REPLAY_SMALL", "0", "0..63", RESTART}, // vk_exec.c group_start
|
|
{"FREE_CORES", "0", "0|1", RESTART}, {"TIMER_1MS", "1", "0|1", RESTART},
|
|
// dynamic resolution (dynres.c): the scene follows the GPU time toward DYN_RES_TARGET milliseconds (under 1: the
|
|
// frame limit's frame time), holding each step DYN_RES_STEP 60 Hz frames of the clock
|
|
{"DYN_RES", "0", "0|1", LIVE}, {"DYN_RES_TARGET", "0", "0..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
|
|
// vk_exec_draw.c mfb_hold_role: a list's depth-only draw waits for its colour draw's pipeline
|
|
{"MESH_FALLBACK_HOLD", "1", "0|1", RESTART},
|
|
{"PIPE_AHEAD", "1", "0|1", RESTART}, // vk_exec.c vk_exec_ahead, vk_pipeline.c vk_pipeline_hint
|
|
{"LINE_WIDTH", "1", "0|1", RESTART}, // vk_pipeline.c key_lines
|
|
{"POOL_SHOWN_FIRST", "1", "0|1", RESTART}, // vk_texture.c shown_take
|
|
{"POOL_DATA_FIRST", "1", "0|1", RESTART}, // vk_texture.c data_take
|
|
{"TEX_REUSE_NAMED", "0", "0|1", RESTART}, // vk_texture.c slot_names
|
|
{"TEX_OVERWRITTEN_USED", "1", "0|1", RESTART}, // vk_texture.c drop_overwritten
|
|
{"GPL_FEEDBACK", "1", "0|1", RESTART}, // vk_pipeline.c gpl_feedback
|
|
{"GPL_MESH", "1", "0|1", RESTART}, // vk_pipeline.c mesh_lib_init
|
|
{"PIPE_FAST_WORKERS", "1", "0|1", RESTART}, // vk_pipeline.c lib_worker, fast_take
|
|
{"FRAME_HOLD_MS", "120", "0..500", RESTART}, // vk_backend.c frame_hold
|
|
{"PACE_SMOOTH", "1", "0|1", RESTART}, // vk_backend.c pace_smooth
|
|
{"OQ_BATCH", "1", "0|1", RESTART}, // vk_exec_xfer.c oq_take
|
|
{"CLEAR_SMALL_CPU", "1", "0|1", RESTART}, // vk_exec_xfer.c clear_small
|
|
// vk_exec_xfer.c oq_report_count: a samples-passed report missing draws the full query ring left uncounted
|
|
// repeats its record's last complete count where that is more than the part counted
|
|
{"OQ_HOLD", "0", "0|1", RESTART},
|
|
// vk_exec_draw.c draw_idx_rewritten: indexed quads drawn as two triangles each, indexed line loops closed
|
|
{"QUAD_INDEX", "0", "0|1", RESTART},
|
|
// the patch set occlusion_off (recomp/patches-v111.txt): the game makes no occlusion queries. The row is what
|
|
// gives the set its default (patches.c rt_patches_init).
|
|
{"PATCH_OCCLUSION_OFF", "0", "0|1", RESTART},
|
|
{"GATE_WAIT_MS", "10", "0..100", RESTART}, // vk_exec.c vk_exec_gate_wait
|
|
{"LIB_PREBUILD", "1", "0|1", RESTART}, {"LIB_PREBUILD_THREADS", "", "1..64", RESTART},
|
|
{"LIB_WARM", "1", "0|1|force", RESTART}, // vk_pipeline.c lw_start: the driver's cache of stage libraries, warmed when cold
|
|
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PIPE_SEED", "1", "0|1", RESTART}, {"PIPE_SEED_BOOT", "auto", "0|1|auto", RESTART}, // vk_pipeline.c keys_open_seed
|
|
{"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "", "0|1", RESTART}, // vk_caps.c vk_caps_gpl: by the driver, off with AMD's for RDNA 2 and older
|
|
{"LIB_PREBUILD_BIG_BG", "1", "0|1", RESTART}, {"PIPE_FEEDBACK", "1", "0|1|2", RESTART},
|
|
{"PIPELINE_CACHE_MAX_MB", "", "0..65536", RESTART}, // vk_pipeline.c cache_cap: unset, from the RAM (1024 to 4096)
|
|
{"FFX_PIPE_CACHE", "1", "0|1", RESTART}, // vk_ffx.cpp vk_ffx_proc_addr: FSR 3.1's pipelines through the pipeline caches
|
|
{"PIPELINE_CACHE_PLAY", "1", "0|1", RESTART}, {"PIPELINE_CACHE_PLAY_MB", "64", "1..65536", RESTART}, // vk_pipeline.c "the play cache": what a run compiles goes into a file of its own
|
|
{"PIPE_BINARIES", "0", "0|1", RESTART}, {"ONESHOT_WAIT_MS", "30000", "0..60000", RESTART}, {"ONESHOT_BUDGET", "1", "0|1", RESTART}, {"PIPE_BINARIES_MAX_MB", "1024", "0..65536", RESTART}, {"BIG_BUILD_FREE_MB", "8192", "0..262144", RESTART},
|
|
{"BIG_BUILD_MAX_MB", "4096", "0..262144", RESTART},
|
|
{"BUILD_FREE_MB", "4096", "0..1048576", RESTART}, {"BUILD_RESERVE_MB", "64", "0..65536", RESTART}, // vk_pipeline.c gate_enter
|
|
{"MEM_NEED_MB", "24576", "0..1048576", RESTART}, // memory.c mem_start_note; the default is mem_need.h MEM_NEED_DEFAULT_MB
|
|
// all|<count>: the compute pipelines the boot keeps, those dispatched most recently (vk_pipeline.c, compute-used.bin)
|
|
{"COMPUTE_KEEP", "512", NULL, RESTART},
|
|
{"SHADER_CARRY", "1", "0|1", RESTART}, // gfx_shader.c "Carry": a previous translator's translations until translated again
|
|
// gfx_shader.c "SPIR-V at rest": a shader's SPIR-V is kept zstd-compressed in memory once no pipeline was made of
|
|
// it for the idle seconds; 0 keeps every shader's SPIR-V plain
|
|
{"SHADER_COMPRESS", "1", "0|1", RESTART},
|
|
{"SHADER_COMPRESS_IDLE_S", "30", "1..3600", RESTART},
|
|
// the settings the low-end GPU mode had rows for up to 0.10.1 (lowend_set, below): each is read by itself now
|
|
{"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", "60", "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},
|
|
// the switch of the low-end GPU mode, which is gone: still a known key, and it does nothing (lowend_set, below)
|
|
{"LOW_END_GPU", "0", "0|1", RESTART},
|
|
{"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)
|
|
|
|
// ---- a comment after a value (cfg_load)
|
|
// "pace_smooth = 0 ; off" kept the comment in the value, which cfg_bool read as on, and "fps_limit = 144 # monitor"
|
|
// gave 60. For a key whose row has a set of values (numbers, ranges, words: none has a blank followed by ';' or '#')
|
|
// the value ends at the first blank followed by ';' or '#' in the text after '=' ("key = ; note" is empty), and the
|
|
// line is logged once at load. Free text keeps the whole value: a row without a set (window_size, monitor,
|
|
// audio_device) and a key without a row (mods_dir, pipeline_cache, the bindings), where a path or a name may hold
|
|
// " #". So does a value in double quotes. The menu's save writes a changed key's line again without its comment
|
|
// (cfg_save_to), the same way every time. At most CHECK_LINES lines per load, then a count (check_end).
|
|
#define CHECK_LINES 16
|
|
static int cuts;
|
|
|
|
// raw: the text after '=' as written; returns the value trimmed, cut at its comment when key has a set of values.
|
|
static char *cut_comment(const char *key, char *raw, int line) {
|
|
char *v = trim(raw);
|
|
size_t n = strlen(v);
|
|
if (n >= 2 && v[0] == '"' && v[n - 1] == '"') return v;
|
|
const CfgMeta *m = cfg_meta(key);
|
|
if (!m || !m->values) return v;
|
|
char *c = raw;
|
|
while (*c && !((*c == ' ' || *c == '\t') && (c[1] == ';' || c[1] == '#'))) c++;
|
|
if (!*c) return v;
|
|
char note[160];
|
|
snprintf(note, sizeof note, "%s", c + 1);
|
|
*c = 0;
|
|
v = trim(raw);
|
|
if (++cuts <= CHECK_LINES) {
|
|
char low[64];
|
|
size_t k = 0;
|
|
for (; key[k] && k < sizeof low - 1; k++) low[k] = (char)tolower((unsigned char)key[k]);
|
|
low[k] = 0;
|
|
rt_log("cfg: nx2navi.ini line %d: %s = '%s', and '%s' after it is a comment", line, low, v, note);
|
|
}
|
|
return v;
|
|
}
|
|
|
|
// value: the text after '=' as written (cut_comment trims it).
|
|
static void add(char *key, char *value, int line) {
|
|
key = norm_key(key);
|
|
value = cut_comment(key, value, line);
|
|
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, a comment after a
|
|
// value of a row with a set cut first, cut_comment)
|
|
// Hand-edited files had values no consumer takes, and nothing said so: "sharpness = 250" is past 0..100. Only the
|
|
// sets that are the consumers' own are checked: 0|1 (with the words switch_word reads), 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. A ';' or '#' after a blank is still named where it
|
|
// stays in the value: a free-text row's value, and the environment's. At most CHECK_LINES lines per load, then a count.
|
|
static int misfits;
|
|
static int switch_of(const char *v);
|
|
|
|
// 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 switch_of(v) < 0 ? "is not 0, 1, on, off, true, false, yes or no" : 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 0..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);
|
|
if (cuts > CHECK_LINES) rt_log("cfg: %d more comments after a value are not listed", cuts - CHECK_LINES);
|
|
misfits = cuts = 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), 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 (NX2NAVI_LOW_END_GPU) was the switch of the low-end GPU mode up to 0.10.1. With it on, nine keys got
|
|
// other defaults (mesh shaders run as compute where the card has none, a texture read after another image drew over
|
|
// its pool memory given memory of its own, game memory copied for the GPU while the replay records, video memory kept
|
|
// within the heaps' budgets, large upload batches sent early, pools the game refills every frame kept in system
|
|
// memory), and those keys and a dozen more were read only while it was on. The mode is gone. Its switch is still a
|
|
// known key, so that an old file's line is no unknown one, and does nothing. Each of its keys is read like any other,
|
|
// with the default of its row or of its reader: a value a player set by hand counts, and a file that sets none runs
|
|
// the game as it runs for everyone else. cfg_init says so once while the switch is on.
|
|
// Read through raw_get, which skips switch_word, so the words are taken here as cfg_bool takes them: "low_end_gpu =
|
|
// off" is off, and any value that is not 0 or one of the off words is on.
|
|
static int lowend_set(void) {
|
|
const char *set = raw_get(PREFIX "LOW_END_GPU");
|
|
return set && *set && switch_of(set) != 0;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// A switch written as a word: 0 for false, off and no, 1 for true, on and yes (any case); -1 for anything else but 0
|
|
// and 1, which give themselves.
|
|
static int switch_of(const char *v) {
|
|
static const char *const word[] = {"false", "off", "no", "true", "on", "yes"};
|
|
if (!strcmp(v, "0") || !strcmp(v, "1")) return v[0] - '0';
|
|
for (int i = 0; i < 6; i++)
|
|
if (!strcasecmp(v, word[i])) return i >= 3;
|
|
return -1;
|
|
}
|
|
|
|
// The value of a switch (a row whose set is 0|1) written as a word, as "0" or "1" for every reader: cfg_bool, rt_knob
|
|
// and the readers that compare with "0" (frame_rate.c frame_step_cap_ns for rtss) or parse a number (cfg_int).
|
|
// "fullscreen = false" and "rtss = off" were on, read as anything but "0". Other values and other rows are handed on
|
|
// as they are.
|
|
static const char *switch_word(const char *name, const char *v) {
|
|
if (!v || !strcmp(v, "0") || !strcmp(v, "1")) return v;
|
|
int on = switch_of(v);
|
|
if (on < 0) return v;
|
|
const CfgMeta *m = cfg_meta(name);
|
|
return m && m->values && !strcmp(m->values, "0|1") ? (on ? "1" : "0") : v;
|
|
}
|
|
|
|
static const char *cfg_get_now(const char *name) {
|
|
const char *v = raw_get(name);
|
|
if (strncmp(name, PREFIX, PREFIX_LEN)) return v;
|
|
if (v) return switch_word(name, 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 NULL;
|
|
}
|
|
|
|
// 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 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;
|
|
}
|
|
|
|
// Off for "0" and for false, off and no in any case (switch_of), also for a key whose row is not 0|1; on otherwise.
|
|
int cfg_bool(const char *name) {
|
|
const char *v = cfg_value(name);
|
|
return strcmp(v, "0") != 0 && switch_of(v) != 0;
|
|
}
|
|
|
|
// ---- the restart-only settings' values at start (cfg_init), which the running game keeps using whatever the menu sets
|
|
// later: cfg_pending compares against them, so the menu says "after restart" only while a restart would change
|
|
// something. One per row of metas, NULL for a live one.
|
|
static const char *at_start[sizeof metas / sizeof *metas];
|
|
|
|
static const char *meta_value(size_t i) {
|
|
char name[80];
|
|
snprintf(name, sizeof name, PREFIX "%s", metas[i].key);
|
|
return cfg_value(name);
|
|
}
|
|
|
|
static void start_values(void) {
|
|
for (size_t i = 0; i < sizeof metas / sizeof *metas; i++)
|
|
if (!metas[i].live) at_start[i] = strdup(meta_value(i)); // a copy: cfg_load (the probes) frees what it read
|
|
}
|
|
|
|
// A value the environment sets never waits: the menu cannot change it and a restart keeps the environment. The command
|
|
// line's --monitor and --audio-device arrive that way after the start values are taken (main.c).
|
|
static int pending_at(size_t i) {
|
|
char name[80];
|
|
if (!at_start[i]) return 0;
|
|
snprintf(name, sizeof name, PREFIX "%s", metas[i].key);
|
|
return !cfg_from_env(name) && strcmp(at_start[i], cfg_value(name)) != 0;
|
|
}
|
|
|
|
int cfg_pending(const char *key) {
|
|
const CfgMeta *m = cfg_meta(key);
|
|
return m ? pending_at((size_t)(m - metas)) : 0;
|
|
}
|
|
|
|
int cfg_pending_count(void) {
|
|
int n = 0;
|
|
for (size_t i = 0; i < sizeof metas / sizeof *metas; i++) n += pending_at(i);
|
|
return n;
|
|
}
|
|
|
|
// ---- cfg_save: the file as lines, each with its line ending
|
|
typedef struct { const char *s; size_t n; } Line; // n includes the '\n' (and '\r') when present
|
|
|
|
static int line_count(const char *p, size_t size) {
|
|
int n = 0;
|
|
for (size_t i = 0; i < size; i++) n += p[i] == '\n';
|
|
return n + 1;
|
|
}
|
|
|
|
// The line's text without its ending, BOM and surrounding blanks, into out.
|
|
static void line_text(const Line *l, int first, char *out, size_t cap) {
|
|
size_t n = l->n;
|
|
const char *s = l->s;
|
|
if (first && n >= 3 && !memcmp(s, "\xEF\xBB\xBF", 3)) s += 3, n -= 3;
|
|
while (n && (s[n - 1] == '\n' || s[n - 1] == '\r')) n--;
|
|
if (n >= cap) n = cap - 1;
|
|
memcpy(out, s, n);
|
|
out[n] = 0;
|
|
char *t = trim(out);
|
|
memmove(out, t, strlen(t) + 1);
|
|
}
|
|
|
|
// The key of a "key = value" line (comment = 0) or of a commented "# key = value" line (comment = 1), normalised,
|
|
// into key; 0 when the line is not one.
|
|
static int line_key(const char *text, int comment, char *key, size_t cap) {
|
|
if (comment) {
|
|
if (*text != '#' && *text != ';') return 0;
|
|
text++;
|
|
while (*text == ' ' || *text == '\t') text++;
|
|
}
|
|
if (!*text || *text == '#' || *text == ';' || *text == '[') return 0;
|
|
const char *eq = strchr(text, '=');
|
|
if (!eq) return 0;
|
|
size_t n = (size_t)(eq - text);
|
|
while (n && (text[n - 1] == ' ' || text[n - 1] == '\t')) n--;
|
|
if (!n || n >= cap) return 0;
|
|
for (size_t i = 0; i < n; i++)
|
|
if (!isalnum((unsigned char)text[i]) && text[i] != '_') return 0;
|
|
char tmp[128];
|
|
memcpy(tmp, text, n);
|
|
tmp[n] = 0;
|
|
snprintf(key, cap, "%s", norm_key(tmp));
|
|
return 1;
|
|
}
|
|
|
|
// The section name of a "[name]" line, lower-case, into out; 0 when the line is not one.
|
|
static int line_section(const char *text, char *out, size_t cap) {
|
|
if (*text != '[') return 0;
|
|
const char *e = strchr(text, ']');
|
|
if (!e) return 0;
|
|
char tmp[128];
|
|
size_t n = (size_t)(e - text - 1);
|
|
if (n >= sizeof tmp) n = sizeof tmp - 1;
|
|
memcpy(tmp, text + 1, n);
|
|
tmp[n] = 0;
|
|
snprintf(out, cap, "%s", trim(tmp));
|
|
for (char *p = out; *p; p++) *p = (char)tolower((unsigned char)*p);
|
|
return 1;
|
|
}
|
|
|
|
typedef struct { char *buf; size_t n, cap; } Out;
|
|
static void put(Out *o, const char *s, size_t n) {
|
|
if (o->n + n + 1 > o->cap) {
|
|
o->cap = (o->n + n + 1) * 2;
|
|
o->buf = realloc(o->buf, o->cap);
|
|
if (!o->buf) rt_fatal("cfg: out of memory writing nx2navi.ini");
|
|
}
|
|
memcpy(o->buf + o->n, s, n);
|
|
o->n += n;
|
|
}
|
|
static void puts_(Out *o, const char *s) { put(o, s, strlen(s)); }
|
|
|
|
static const char *section_of(const char *key) {
|
|
for (size_t i = 0; i < sizeof sections / sizeof *sections; i++)
|
|
if (!strcmp(sections[i].key, key)) return sections[i].section;
|
|
if (!strncmp(key, "CHEAT_", 6)) return "cheats"; // cheats.c: one key per cheat of the cheat file
|
|
return NULL;
|
|
}
|
|
|
|
// A value as cfg_load reads it back: quoted when it starts or ends with a blank (cfg_load trims those) or is itself
|
|
// quoted (cfg_load takes one pair of quotes off). A mod's folder name in mods_order can start with a space.
|
|
static void put_value(Out *o, const char *v) {
|
|
size_t n = strlen(v);
|
|
int quote = n && (v[0] == ' ' || v[0] == '\t' || v[n - 1] == ' ' || v[n - 1] == '\t' ||
|
|
(n >= 2 && v[0] == '"' && v[n - 1] == '"'));
|
|
if (quote) puts_(o, "\"");
|
|
puts_(o, v);
|
|
if (quote) puts_(o, "\"");
|
|
}
|
|
|
|
// "key = value" + eol for entry e (key lower-case)
|
|
static void put_entry(Out *o, const CfgEntry *e, const char *eol) {
|
|
char k[64];
|
|
snprintf(k, sizeof k, "%s", e->key);
|
|
for (char *p = k; *p; p++) *p = (char)tolower((unsigned char)*p);
|
|
puts_(o, k);
|
|
puts_(o, " = ");
|
|
put_value(o, e->value);
|
|
puts_(o, eol);
|
|
}
|
|
|
|
int cfg_save_to(const char *path) {
|
|
plat_mutex_lock(&set_lock);
|
|
int n = n_entries, any = 0;
|
|
for (int i = 0; i < n; i++) any |= entries[i].dirty | entries[i].removed;
|
|
if (!any) {
|
|
__atomic_store_n(&save_ok, 1, __ATOMIC_RELEASE);
|
|
plat_mutex_unlock(&set_lock);
|
|
return 1;
|
|
}
|
|
uint64_t size = 0;
|
|
uint8_t *data = plat_file_read(path, &size);
|
|
if (!data && plat_file_exists(path) && plat_file_size(path) != 0) {
|
|
// the file is there but unreadable (locked, transient error): rebuilding it from nothing would lose its lines
|
|
rt_log("cfg: cannot read %s, nothing written", path);
|
|
__atomic_store_n(&save_ok, 0, __ATOMIC_RELEASE);
|
|
plat_mutex_unlock(&set_lock);
|
|
return 0;
|
|
}
|
|
const char *text = data ? (const char *)data : "";
|
|
int nl = data ? line_count(text, size) : 0;
|
|
Line *lines = calloc((size_t)nl + 1, sizeof *lines);
|
|
// after[i]: entries to write after line i (-1 = none); replace[i]: the entry whose value line i now carries
|
|
int *replace = malloc(sizeof(int) * ((size_t)nl + 1)), *after_key = malloc(sizeof(int) * (size_t)(n + 1));
|
|
char *drop = calloc((size_t)nl + 1, 1); // lines of a reset key
|
|
if (!lines || !replace || !after_key || !drop) rt_fatal("cfg: out of memory writing nx2navi.ini");
|
|
for (int i = 0, at = 0; i < nl; i++) {
|
|
size_t start = (size_t)at;
|
|
while ((uint64_t)at < size && text[at] != '\n') at++;
|
|
if ((uint64_t)at < size) at++;
|
|
lines[i] = (Line){text + start, (size_t)at - start};
|
|
replace[i] = -1;
|
|
}
|
|
if (nl && !lines[nl - 1].n) nl--; // the text ended with a newline: no last empty line
|
|
const char *eol = data && strstr(text, "\r\n") ? "\r\n" : "\n";
|
|
char t[1024], k[128], sec[128];
|
|
for (int e = 0; e < n; e++) {
|
|
after_key[e] = -2; // -2 clean, -1 append in a new section at the end, >= 0 insert after that line
|
|
if (entries[e].removed)
|
|
for (int i = 0; i < nl; i++) {
|
|
line_text(&lines[i], i == 0, t, sizeof t);
|
|
if (line_key(t, 0, k, sizeof k) && !strcmp(k, entries[e].key)) drop[i] = 1;
|
|
}
|
|
if (!entries[e].dirty) continue;
|
|
int last = -1;
|
|
for (int i = 0; i < nl; i++) {
|
|
line_text(&lines[i], i == 0, t, sizeof t);
|
|
if (line_key(t, 0, k, sizeof k) && !strcmp(k, entries[e].key)) last = i;
|
|
}
|
|
if (last >= 0) {
|
|
replace[last] = e;
|
|
continue;
|
|
}
|
|
const char *want = section_of(entries[e].key);
|
|
int in = 0, example = -1, tail = -1;
|
|
for (int i = 0; i < nl && want; i++) {
|
|
line_text(&lines[i], i == 0, t, sizeof t);
|
|
if (line_section(t, sec, sizeof sec)) {
|
|
in = !strcmp(sec, want);
|
|
if (in) tail = i;
|
|
continue;
|
|
}
|
|
if (!in) continue;
|
|
if (*t) tail = i;
|
|
if (line_key(t, 1, k, sizeof k) && !strcmp(k, entries[e].key)) example = i;
|
|
}
|
|
after_key[e] = example >= 0 ? example : tail;
|
|
}
|
|
Out o = {0};
|
|
for (int i = 0; i < nl; i++) {
|
|
if (drop[i]) {
|
|
// the line leaves the file; keys inserted after it still need a line ending before them
|
|
} else if (replace[i] >= 0) {
|
|
// keep the line's indentation and key spelling, put the new value after " = "
|
|
const char *s = lines[i].s;
|
|
size_t len = lines[i].n;
|
|
if (i == 0 && len >= 3 && !memcmp(s, "\xEF\xBB\xBF", 3)) put(&o, s, 3), s += 3, len -= 3;
|
|
const char *eq = memchr(s, '=', len);
|
|
size_t kn = (size_t)(eq - s);
|
|
while (kn && (s[kn - 1] == ' ' || s[kn - 1] == '\t')) kn--;
|
|
put(&o, s, kn);
|
|
puts_(&o, " = ");
|
|
put_value(&o, entries[replace[i]].value);
|
|
int had_eol = len && s[len - 1] == '\n';
|
|
if (had_eol) puts_(&o, eol);
|
|
} else {
|
|
put(&o, lines[i].s, lines[i].n);
|
|
}
|
|
for (int e = 0; e < n; e++)
|
|
if (after_key[e] == i) {
|
|
if (o.n && o.buf[o.n - 1] != '\n') puts_(&o, eol);
|
|
put_entry(&o, &entries[e], eol);
|
|
}
|
|
}
|
|
// keys whose section the file lacks: one new section each, at the end
|
|
for (int e = 0; e < n; e++) {
|
|
if (after_key[e] != -1) continue;
|
|
const char *want = section_of(entries[e].key);
|
|
if (o.n && o.buf[o.n - 1] != '\n') puts_(&o, eol);
|
|
if (want) {
|
|
if (o.n) puts_(&o, eol);
|
|
puts_(&o, "[");
|
|
puts_(&o, want);
|
|
puts_(&o, "]");
|
|
puts_(&o, eol);
|
|
}
|
|
put_entry(&o, &entries[e], eol);
|
|
// the following keys of the same new section go right after this one
|
|
for (int f = e + 1; f < n && want; f++)
|
|
if (after_key[f] == -1 && section_of(entries[f].key) && !strcmp(section_of(entries[f].key), want)) {
|
|
put_entry(&o, &entries[f], eol);
|
|
after_key[f] = -3;
|
|
}
|
|
}
|
|
int ok = plat_file_write_atomic(path, o.buf ? o.buf : "", o.n);
|
|
if (ok)
|
|
for (int e = 0; e < n; e++) entries[e].dirty = entries[e].removed = 0;
|
|
else
|
|
rt_log("cfg: cannot write %s", path);
|
|
__atomic_store_n(&save_ok, ok ? 1 : 0, __ATOMIC_RELEASE);
|
|
free(o.buf);
|
|
free(drop);
|
|
free(after_key);
|
|
free(replace);
|
|
free(lines);
|
|
free(data);
|
|
plat_mutex_unlock(&set_lock);
|
|
return ok;
|
|
}
|
|
|
|
static char ini_path[1024];
|
|
|
|
static int find_ini(void) {
|
|
if (ini_path[0]) return 1;
|
|
char path[1024];
|
|
if (!plat_exe_path(path, sizeof path)) return 0;
|
|
char *cut = plat_last_sep(path);
|
|
if (!cut) cut = strrchr(path, '/');
|
|
if (!cut || cut + 12 >= path + sizeof path) return 0; // room for "nx2navi.ini" and its NUL after the separator
|
|
strcpy(cut + 1, "nx2navi.ini");
|
|
snprintf(ini_path, sizeof ini_path, "%s", path);
|
|
return 1;
|
|
}
|
|
|
|
// A replay of a bug capture (bugcap.c) runs on the capture's settings and leaves the player's nx2navi.ini as it is:
|
|
// nothing is saved, whatever the run sets.
|
|
int cfg_save(void) {
|
|
if (bugcap_replay_dir()) return 1;
|
|
if (find_ini()) return cfg_save_to(ini_path);
|
|
__atomic_store_n(&save_ok, 0, __ATOMIC_RELEASE);
|
|
return 0;
|
|
}
|
|
|
|
// What the file sets, in the log once at start: the settings of this version with their values, in lines of up to
|
|
// about 900 characters, and then the keys that are no settings of this version, which do nothing. A report's log then
|
|
// shows which settings its run had and which of the file's lines had no effect; before, it held only the count of
|
|
// keys, and a player who had added a line this version does not read could take it for the cause of what changed.
|
|
static void log_file_settings(const char *file) {
|
|
char line[1024];
|
|
size_t k = 0;
|
|
for (int i = 0; i < n_entries; i++) {
|
|
if (!entries[i].value || !is_known(entries[i].key)) continue;
|
|
char key[64];
|
|
snprintf(key, sizeof key, "%s", entries[i].key);
|
|
for (char *p = key; *p; p++) *p = (char)tolower((unsigned char)*p);
|
|
// a private setting (cfg_private.h): only that it is set
|
|
const char *value = cfg_private_key(entries[i].key) && entries[i].value[0] ? CFG_PRIVATE_SHOWN : entries[i].value;
|
|
size_t vl = strlen(value);
|
|
if (vl > 200) vl = 200; // a long value (a list of mods) is cut: the line shows that the key is set
|
|
if (k && k + strlen(key) + vl + 5 > 900) {
|
|
rt_log("cfg: %s sets: %s", file, line);
|
|
k = 0;
|
|
}
|
|
k += (size_t)snprintf(line + k, sizeof line - k, "%s%s = %.*s", k ? ", " : "", key, (int)vl, value);
|
|
}
|
|
if (k) rt_log("cfg: %s sets: %s", file, line);
|
|
if (n_unknown)
|
|
rt_log("cfg: %s has %d key%s that %s no setting of this version and %s nothing: %s%s", file, n_unknown,
|
|
n_unknown == 1 ? "" : "s", n_unknown == 1 ? "is" : "are", n_unknown == 1 ? "does" : "do", unknown_keys,
|
|
unknown_cut ? ", ..." : "");
|
|
}
|
|
|
|
static void load_ini(void) {
|
|
// NX2NAVI_CAPTURE_REPLAY=<a bug capture's folder>: the settings that capture was taken with (its settings.ini) stand
|
|
// in for nx2navi.ini, which is neither read nor written in such a run; the environment still wins over them
|
|
const char *replay = bugcap_replay_dir();
|
|
if (replay) {
|
|
char path[1100];
|
|
snprintf(path, sizeof path, "%s/settings.ini", replay);
|
|
int ok = cfg_load(path);
|
|
rt_log(ok ? "cfg: replay of the bug capture in %s: its settings.ini (%d keys) in place of nx2navi.ini, which this run does not write"
|
|
: "cfg: replay of the bug capture in %s: no settings.ini there, so every setting has its default (%d keys); "
|
|
"nx2navi.ini is not read or written",
|
|
replay, n_entries);
|
|
if (ok) log_file_settings("the capture's settings.ini");
|
|
return;
|
|
}
|
|
if (!find_ini()) return;
|
|
if (!cfg_load(ini_path)) {
|
|
rt_log("cfg: no settings file at %s", ini_path);
|
|
return;
|
|
}
|
|
rt_log("cfg: read %s: %d keys", ini_path, n_entries);
|
|
log_file_settings("nx2navi.ini");
|
|
for (int i = 0; i < n_entries; i++) {
|
|
char var[128];
|
|
snprintf(var, sizeof var, PREFIX "%s", entries[i].key);
|
|
if (getenv(var)) rt_log("cfg: %s is set in the environment and overrides nx2navi.ini", var);
|
|
}
|
|
}
|
|
|
|
void cfg_init(void) {
|
|
load_ini();
|
|
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();
|
|
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_set())
|
|
rt_log("cfg: low_end_gpu is on, and the low-end GPU mode was removed in 0.11.0: the line does nothing, and the game "
|
|
"runs as it does without it. The mode's single settings (mesh_emulation, tex_clobber, foliage, "
|
|
"vram_reserve_mb and the others) still count where nx2navi.ini or the environment sets them");
|
|
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);
|
|
}
|