diff --git a/docs/README-BuildKit.md b/docs/README-BuildKit.md index b34e3146..c21e1a02 100644 --- a/docs/README-BuildKit.md +++ b/docs/README-BuildKit.md @@ -108,7 +108,7 @@ beside the game. "Preparing shaders" once while the driver rebuilds them. With a driver that has Vulkan's graphics pipeline library it lasts at most about 20 seconds once the shaders are translated, and the remaining pipelines are built in the background while you play. -- **Screen** in the menu has Upscaling (DLSS on NVIDIA RTX cards, FSR 3.1 on any card), Frame generation (NVIDIA DLSS +- **Screen** in the menu has Texture filtering (anisotropic, up to 16x, at once), Upscaling (DLSS on NVIDIA RTX cards, FSR 3.1 on any card), Frame generation (NVIDIA DLSS on RTX 40 and 50 cards, with a DLSS multiplier of 2x, or 3x and 4x on RTX 50 cards; FSR 3.1 on any card; it needs DLSS or FSR 3.1 upscaling on, and the frame limit then counts the frames on screen; with no limit chosen it is the display's refresh rate), HDR (the game's picture tone mapped to your display, with its own grading rows, or Native, the game's own HDR; turn HDR on in Windows first) and the diff --git a/nx2navi.ini.example b/nx2navi.ini.example index 3799b373..13450974 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -281,7 +281,8 @@ # aspect = fit # Anisotropic filtering of surface textures: game (the level the game asks for, as on the Switch) or 2, 4, 8, 16. Keeps # ground, roads and walls seen at a shallow angle sharp into the distance. Only filtered, mipmapped textures that tile -# are raised; shadow maps, data maps and screen-sized buffers keep the game's setting. Read at start. +# are raised; shadow maps, data maps and screen-sized buffers keep the game's setting. Changes while the game runs take +# effect at once. # aniso = game # Sun shadow map size: game (the game's own, which changes with the view and drops to about 400 near the ground, so # leaf shadows look blocky and jump) or a fixed 256, 512, 768, 1024, 1536 or 2048. Above 960 the texture heap grows with diff --git a/runtime/cfg.c b/runtime/cfg.c index f490535a..761f485b 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -145,7 +145,7 @@ static const CfgMeta metas[] = { {"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", "game", "game|2|4|8|16", RESTART}, // vk_texture.c sampler_features + {"ANISO", "game", "game|2|4|8|16", LIVE}, // vk_texture.c sampler_features, vk_sampler_aniso_live // 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}, diff --git a/runtime/ui/menu.cpp b/runtime/ui/menu.cpp index ff4ab868..6b3df312 100644 --- a/runtime/ui/menu.cpp +++ b/runtime/ui/menu.cpp @@ -988,6 +988,10 @@ static void tab_screen(float scale) { int dl = cfg_bool("NX2NAVI_DLAA"), fsr3 = dl && !strcmp(cfg_value("NX2NAVI_TAA"), "fsr3"); if (!fsr3 && (strcmp(cfg_value("NX2NAVI_UPSCALER"), "fsr") || dl)) opt_disable_next(UI_MENU_SHARPNESS_NEEDS_FSR); opt_slider("NX2NAVI_SHARPNESS", UI_MENU_SHARPNESS, 0, 100, UI_MENU_SHARPNESS_DESC, 5, "%.0f %%"); + // Anisotropic filtering of the surface textures that tile (vk_texture.c aniso_raised), at once (vk_sampler_aniso_live) + static const char *const aniso[] = {"game", "2", "4", "8", "16"}; + const char *aniso_names[] = {menu_text(UI_MENU_ANISO_GAME), "2x", "4x", "8x", "16x"}; + opt_choice_list("NX2NAVI_ANISO", UI_MENU_ANISO, aniso, aniso_names, 5, UI_MENU_ANISO_DESC); opt_end(); } diff --git a/runtime/vk.h b/runtime/vk.h index 81ae6766..ec16d27c 100644 --- a/runtime/vk.h +++ b/runtime/vk.h @@ -526,6 +526,7 @@ uint32_t vk_texture_entry_gen(void); // vk_texture.c: bumped by int vk_texture_dirty_slots_locked(uint64_t *pos, void (*fn)(int id, void *arg), void *arg); uint64_t vk_texture_dirty_pos_locked(void); void vk_sampler_dlss_bias(float bias); // texture LOD bias added for DLSS upscaling (material samplers) +void vk_sampler_aniso_live(void); // each present: NX2NAVI_ANISO after a settings change (tiled samplers) uint32_t vk_sampler_gen(void); // changes whenever a sampler slot is registered void vk_texture_gpu_written(const tex_t *t); // rendered or copied into by the GPU: no re-upload from storage // The live image of the given format and img's size at img's storage address, NULL when there is none. Texture lock held. diff --git a/runtime/vk_backend.c b/runtime/vk_backend.c index 953f8842..4533f0c4 100644 --- a/runtime/vk_backend.c +++ b/runtime/vk_backend.c @@ -105,6 +105,7 @@ static void present_image(const tex_t *t, const int crop[4], uint64_t at_ns, uin vk_exec_frame_log(t, frame); dynres_frame(); // before vk_dlaa_frame reads the DLSS mode it may override vk_dlaa_frame(); + vk_sampler_aniso_live(); // NX2NAVI_ANISO changed in the menu: the surface textures' samplers take the new level vk_mvsurvey_frame(); vk_mvprev_frame(); vk_mem_census(frame); diff --git a/runtime/vk_texture.c b/runtime/vk_texture.c index 0a462775..816ad79d 100644 --- a/runtime/vk_texture.c +++ b/runtime/vk_texture.c @@ -3128,6 +3128,17 @@ static VkCompareOp compare_op(uint32_t f) { static struct { int init, mirror_clamp_edge, minmax, border_no_format; float aniso; } sf; // aniso: NX2NAVI_ANISO, 0 = the game's +static float aniso_setting(void) { + const char *an = cfg_value("NX2NAVI_ANISO"); + return strcmp(an, "game") ? (float)atoi(an) : 0; +} + +static void aniso_log(void) { + if (sf.aniso > 1) rt_log("vk: anisotropic filtering: %gx", sf.aniso); + else rt_log("vk: anisotropic filtering: the game's"); +} + +// Caller holds the lock. static void sampler_features(VkCtx *c) { if (sf.init) return; VkPhysicalDeviceCustomBorderColorFeaturesEXT cbf = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_FEATURES_EXT}; @@ -3138,12 +3149,25 @@ static void sampler_features(VkCtx *c) { sf.mirror_clamp_edge = f12.samplerMirrorClampToEdge; sf.minmax = f12.samplerFilterMinmax; sf.border_no_format = c->has_custom_border_color && cbf.customBorderColors && cbf.customBorderColorWithoutFormat; - const char *an = cfg_value("NX2NAVI_ANISO"); - sf.aniso = strcmp(an, "game") ? (float)atoi(an) : 0; - rt_log("vk: anisotropic filtering: %s", sf.aniso > 1 ? an : "the game's"); + sf.aniso = aniso_setting(); + aniso_log(); sf.init = 1; } +// NX2NAVI_ANISO raises the level for filtered, mipmapped colour samplers that repeat on U and V: the surface textures, +// which tile, so ground and walls seen at a shallow angle stay sharp into the distance. Depth-compare (shadow) +// samplers, nearest or unmipmapped ones (data maps, LUTs) and clamped ones keep the game's level: clamped samplers +// read screen-sized buffers, whose coordinates can stretch steeply at an object's edge, where anisotropic taps reach +// across it and leave a speckled fringe (the inventory's character preview). The game's own wrap modes decide: a +// mirror-clamp one maps to MIRRORED_REPEAT where the device lacks mirror-clamp-to-edge (make_sampler). Caller holds the +// lock, after sampler_features. +static int aniso_raised(const samp_par *d) { + int tiled = (d->ws == NVN_WRAP_MODE_REPEAT || d->ws == NVN_WRAP_MODE_MIRRORED_REPEAT) && + (d->wt == NVN_WRAP_MODE_REPEAT || d->wt == NVN_WRAP_MODE_MIRRORED_REPEAT); + return sf.aniso > d->aniso && tiled && !d->cmp_mode && d->magf == NVN_MAG_FILTER_LINEAR && + (d->minf == NVN_MIN_FILTER_LINEAR_MIPMAP_NEAREST || d->minf == NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR); +} + static VkSampler make_sampler(VkCtx *c, const samp_par *d) { sampler_features(c); VkSamplerCreateInfo si = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; @@ -3175,21 +3199,10 @@ static VkSampler make_sampler(VkCtx *c, const samp_par *d) { if (si.maxLod < si.minLod) si.maxLod = si.minLod; float lim = c->props.limits.maxSamplerLodBias; si.mipLodBias = fmaxf(-lim, fminf(lim, d->lod_bias)); - // NX2NAVI_ANISO raises the level for filtered, mipmapped colour samplers that repeat on U and V: the surface textures, - // which tile, so ground and walls seen at a shallow angle stay sharp into the distance. Depth-compare (shadow) - // samplers, nearest or unmipmapped ones (data maps, LUTs) and clamped ones keep the game's level: clamped samplers - // read screen-sized buffers, whose coordinates can stretch steeply at an object's edge, where anisotropic taps reach - // across it and leave a speckled fringe (the inventory's character preview). - float aniso = d->aniso; - // The game's own wrap modes: a mirror-clamp one maps to MIRRORED_REPEAT where the device lacks mirror-clamp-to-edge. - int tiled = (d->ws == NVN_WRAP_MODE_REPEAT || d->ws == NVN_WRAP_MODE_MIRRORED_REPEAT) && - (d->wt == NVN_WRAP_MODE_REPEAT || d->wt == NVN_WRAP_MODE_MIRRORED_REPEAT); - if (sf.aniso > aniso && tiled && !d->cmp_mode && si.magFilter == VK_FILTER_LINEAR && si.minFilter == VK_FILTER_LINEAR && - (mf == NVN_MIN_FILTER_LINEAR_MIPMAP_NEAREST || mf == NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR)) - aniso = sf.aniso; - if (aniso > 1) { + // d->aniso holds NX2NAVI_ANISO's level where it applies (sampler_for, aniso_raised) + if (d->aniso > 1) { si.anisotropyEnable = VK_TRUE; - si.maxAnisotropy = fminf(aniso, c->props.limits.maxSamplerAnisotropy); + si.maxAnisotropy = fminf(d->aniso, c->props.limits.maxSamplerAnisotropy); } if (d->cmp_mode) { si.compareEnable = VK_TRUE; @@ -3264,12 +3277,35 @@ void vk_sampler_dlss_bias(float b) { unlock(); } +// NX2NAVI_ANISO, read again at each present after a settings change (vk_backend.c present_image, beside the DLSS bias of +// vk_dlaa_frame): the raised level is part of the sampler key (sampler_for), so on a change the slots look their +// samplers up again and the descriptor caches follow samp_gen, as for vk_sampler_dlss_bias. The old level's samplers +// stay until shutdown like every sampler: one set per level at most. Before the first sampler there is nothing to do: +// sampler_features reads the setting then. +void vk_sampler_aniso_live(void) { + static unsigned seen = ~0u; + unsigned v = cfg_version(); + if (v == seen) return; + seen = v; + lock(); + float a = aniso_setting(); + if (sf.init && a != sf.aniso) { + sf.aniso = a; + aniso_log(); + for (int i = 0; i < nsslots; i++) sslots[i].s = sslots[i].sp = sslots[i].sc = VK_NULL_HANDLE; // looked up again (the key moves) + samp_gen++; + } + unlock(); +} + // Caller holds the lock. static VkSampler sampler_for(VkCtx *c, const samp_par *s) { + sampler_features(c); samp_par k; memset(&k, 0, sizeof k); k = *s; if (dlss_bias != 0 && dlss_biased(&k)) k.lod_bias += dlss_bias; // part of the key: samplers live until shutdown + if (aniso_raised(&k)) k.aniso = sf.aniso; // likewise VkSampler v = (VkSampler)nvn_hm_get(&samplers, &k); if (!v) { v = make_sampler(c, &k);