Files
NX2navi/runtime/vk_tonemap.c
chrg6 e0f69489af Bring the 0.10.0 line into the VRAM pass, keeping the line's own releases where the two overlap.
The line freed the upscaler stand-in's scratch images, FSR 1's and SMAA's images and the DLSS sizes on its own while
this pass did: its versions stay. Of this pass's versions, what they do not cover stays: the stand-in's views of a game
image it read as it is now go with that image (vk_tonemap_forget from vk_exec_image_gone, under a lock), and the
upscaler release frees only what the line's idle release leaves, the reactive mask's snapshots, the FSR upscale
contexts and the DLSS sizes after a failure, at the same 600 presents and with vram_use = high keeping them as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 00:50:21 -05:00

252 lines
17 KiB
C

// Decode-and-scale pass standing in for the scene upscaler (vk.h vk_tonemap_record; used by vk_exec_draw.c
// untranslated_dispatch_fallback).
#include "platform/platform.h"
#include "vk.h"
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "fallback_tonemap_spv.h"
#include "fsr_easu_spv.h"
#include "host.h"
// The game's upscaler (tensor-core, untranslatable) reads the post pass's picture, whose channels hold the square root of
// their linear value packed as RGB9E5 (prog 102), and writes the linear picture the HUD is drawn over; the frame's last
// pass encodes to sRGB. This pass (shaders/fallback_tonemap.spvasm) scales with linear filtering in the stored values
// and squares them into an RGBA16F image of the target's size, which the caller blits in 1:1. The game composites
// and tonemaps before the post pass, so nothing here changes exposure. (It was a tonemap once, when the input was
// taken for the raw HDR scene; the blit's sRGB decode that stood in since darkened mid-tones and lifted the darkest
// values.)
//
// The filter: the console's upscaler is a network that rebuilds edges, and a linear filter over a scene drawn at about
// three quarters of the picture is visibly softer than that. With NX2NAVI_STANDIN_FILTER = easu (the default) the
// pass that enlarges is shaders/fsr_easu.comp instead, the edge-adaptive filter the present scales to the window with
// (vk_fsr.c), built here with its SQUARE constant set: it filters the same stored values, which are the perceptual
// encoding that filter is made for, and squares the result as the linear pass does, so only how the scaled sample is
// taken differs. bilinear keeps the linear pass. The linear pass also stays where nothing is enlarged (the filter only
// enlarges) and where the caller says another upscaler's picture replaces this one (vk_exec_draw.c
// untranslated_dispatch_fallback: DLSS or FSR 3.1, vk_dlaa.c). A scene as large as the picture (scene_share = 1, the
// menu's Native: widescreen.c plain_share) is not scaled at all: the linear pass samples it with a nearest-texel
// sampler, so each output texel is its source texel squared, whatever the filtering hardware's sub-texel precision.
// The movie's frames do not come through here: the caller blits those (RGBA8) itself.
static struct {
int tried, ok;
PFN_vkCmdPushDescriptorSetKHR push;
VkDescriptorSetLayout dsl;
VkPipelineLayout layout;
VkPipeline pipe; // scale with linear filtering, then square (shaders/fallback_tonemap.spvasm)
VkPipeline easu; // scale with the edge-adaptive filter, then square (shaders/fsr_easu.comp)
int easu_tried;
int filter_logs; // lines tm_filter_log has written
int filter_last; // what its last line named: 1 the linear filter, 2 the edge-adaptive one, 3 a copy 1:1
VkSampler sampler;
VkSampler nearest; // the same with nearest-texel filtering: a source of the output's size (copy 1:1)
struct { VkImage img; VkFormat f; VkImageView view; } views[16];
int nviews;
VkImage out; // the pass's own output (RGBA16F, the target's size); the caller blits it in
VmaAllocation out_alloc;
VkImageView out_view;
uint32_t ow, oh;
} TM;
// The output image at w x h, made again when the size changes (the old one is left to the deferred release).
static int tm_out(VkCommandBuffer cb, uint32_t w, uint32_t h) {
if (TM.out && TM.ow == w && TM.oh == h) return 1;
VkCtx *c = vk_ctx();
if (TM.out) vk_defer_view(TM.out_view), vk_defer_image(TM.out, TM.out_alloc), TM.out = VK_NULL_HANDLE;
VkImageCreateInfo ii = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
.format = VK_FORMAT_R16G16B16A16_SFLOAT, .extent = {w, h, 1}, .mipLevels = 1, .arrayLayers = 1,
.samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
if (vmaCreateImage(c->vma, &ii, &ai, &TM.out, &TM.out_alloc, NULL) != VK_SUCCESS) return TM.out = VK_NULL_HANDLE, 0;
VkImageViewCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = TM.out,
.viewType = VK_IMAGE_VIEW_TYPE_2D, .format = VK_FORMAT_R16G16B16A16_SFLOAT,
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
if (vkCreateImageView(c->device, &vi, NULL, &TM.out_view) != VK_SUCCESS) {
vmaDestroyImage(c->vma, TM.out, TM.out_alloc);
return TM.out = VK_NULL_HANDLE, 0;
}
VkImageMemoryBarrier2 b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT,
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = TM.out, .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
VkDependencyInfo d = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &b};
vkCmdPipelineBarrier2(cb, &d);
TM.ow = w, TM.oh = h;
return 1;
}
static int tm_init(void) {
if (TM.tried) return TM.ok;
TM.tried = 1;
VkCtx *c = vk_ctx();
TM.push = (PFN_vkCmdPushDescriptorSetKHR)vk_dpa(c->device, "vkCmdPushDescriptorSetKHR");
VkDescriptorSetLayoutBinding b[2] = {
{.binding = 0, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT},
{.binding = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT}};
VkDescriptorSetLayoutCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR,
.bindingCount = 2, .pBindings = b};
VkPushConstantRange pr = {VK_SHADER_STAGE_COMPUTE_BIT, 0, 32}; // the linear pass reads 24 bytes, tm_easu's 32
VkSamplerCreateInfo si = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .magFilter = VK_FILTER_LINEAR,
.minFilter = VK_FILTER_LINEAR, .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, .maxLod = 0};
VkShaderModuleCreateInfo mi = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = sizeof fallback_tonemap_spv, .pCode = fallback_tonemap_spv};
VkShaderModule m = VK_NULL_HANDLE;
int ok = TM.push && vkCreateDescriptorSetLayout(c->device, &dci, NULL, &TM.dsl) == VK_SUCCESS;
VkPipelineLayoutCreateInfo li = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
.pSetLayouts = &TM.dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pr};
VkSamplerCreateInfo ni = si;
ni.magFilter = ni.minFilter = VK_FILTER_NEAREST;
ok = ok && vkCreatePipelineLayout(c->device, &li, NULL, &TM.layout) == VK_SUCCESS &&
vkCreateSampler(c->device, &si, NULL, &TM.sampler) == VK_SUCCESS &&
vkCreateSampler(c->device, &ni, NULL, &TM.nearest) == VK_SUCCESS &&
vkCreateShaderModule(c->device, &mi, NULL, &m) == VK_SUCCESS;
if (ok) {
VkComputePipelineCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
.stage = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m, .pName = "main"},
.layout = TM.layout};
ok = vkCreateComputePipelines(c->device, VK_NULL_HANDLE, 1, &ci, NULL, &TM.pipe) == VK_SUCCESS;
}
if (m) vkDestroyShaderModule(c->device, m, NULL);
rt_log("vk_exec: upscaler stand-in: %s", ok ? "scale and square-decode pass" : "blit (its pass could not be created)");
return TM.ok = ok;
}
// The edge-adaptive pipeline, made at the first frame that asks for it (a run that never does, with the setting at
// bilinear or DLSS on, never builds it): the present's EASU shader with specialization constant 0 (SQUARE) set, on
// this file's layout, whose two bindings are that shader's. VK_NULL_HANDLE when it cannot be made; the linear pass
// then stays.
static VkPipeline tm_easu(void) {
if (TM.easu_tried) return TM.easu;
TM.easu_tried = 1;
VkCtx *c = vk_ctx();
VkShaderModuleCreateInfo mi = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = sizeof fsr_easu_spv,
.pCode = fsr_easu_spv};
VkShaderModule m = VK_NULL_HANDLE;
static const VkBool32 square = VK_TRUE;
VkSpecializationMapEntry me = {.constantID = 0, .offset = 0, .size = sizeof square};
VkSpecializationInfo sp = {.mapEntryCount = 1, .pMapEntries = &me, .dataSize = sizeof square, .pData = &square};
if (vkCreateShaderModule(c->device, &mi, NULL, &m) == VK_SUCCESS) {
VkComputePipelineCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
.stage = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m, .pName = "main",
.pSpecializationInfo = &sp},
.layout = TM.layout};
if (vkCreateComputePipelines(c->device, VK_NULL_HANDLE, 1, &ci, NULL, &TM.easu) != VK_SUCCESS)
TM.easu = VK_NULL_HANDLE;
vkDestroyShaderModule(c->device, m, NULL);
}
if (!TM.easu) rt_log("vk_exec: upscaler stand-in: its edge-adaptive pass could not be created; it scales bilinearly");
return TM.easu;
}
// One log line when the filter the pass scales with changes (the setting, DLSS or FSR switched on or off, a scene as
// large as the picture), 8 at most: a scene whose size changes from frame to frame can cross the picture's size often.
static void tm_filter_log(int easu, int edge, uint32_t sw, uint32_t sh, uint32_t dw, uint32_t dh) {
int same = sw == dw && sh == dh, now = easu ? 2 : same && edge ? 3 : 1;
if (TM.filter_last == now || TM.filter_logs >= 8) return;
TM.filter_last = now, TM.filter_logs++;
if (easu)
rt_log("vk_exec: upscaler stand-in scales %ux%u to %ux%u with the edge-adaptive filter "
"(NX2NAVI_STANDIN_FILTER=bilinear: the linear one)", sw, sh, dw, dh);
else if (now == 3)
rt_log("vk_exec: upscaler stand-in copies %ux%u 1:1, decoded and not scaled: the scene is the picture's size", sw,
sh);
else
rt_log("vk_exec: upscaler stand-in scales %ux%u to %ux%u with the linear filter: %s", sw, sh, dw, dh,
!edge ? "DLSS or FSR scales the scene and its picture goes over this one"
: dw < sw || dh < sh ? "nothing to enlarge"
: strcmp(cfg_value("NX2NAVI_STANDIN_FILTER"), "easu") ? "NX2NAVI_STANDIN_FILTER asks for it"
: "the edge-adaptive pass is not there");
}
// Views of the images the pass samples, kept as long as the image: vk_tonemap_forget drops those of an image
// fallback_scratch frees, and those of a game image the pass read as it is when the game's texture goes
// (vk_exec_image_gone), which also gives their places back. Without the second, a game image's view stayed naming a
// destroyed image whose handle a new image could get. The texture's destroy runs on any thread, hence the lock.
static PlatMutex tm_view_lock = PLAT_MUTEX_INIT;
void vk_tonemap_forget(VkImage src) {
plat_mutex_lock(&tm_view_lock);
for (int i = 0; i < TM.nviews;)
if (TM.views[i].img == src) vk_defer_view(TM.views[i].view), TM.views[i] = TM.views[--TM.nviews];
else i++;
plat_mutex_unlock(&tm_view_lock);
}
static VkImageView tm_view(VkImage img, VkFormat f) {
plat_mutex_lock(&tm_view_lock);
VkImageView v = VK_NULL_HANDLE;
for (int i = 0; i < TM.nviews && !v; i++)
if (TM.views[i].img == img && TM.views[i].f == f) v = TM.views[i].view;
VkImageViewCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = img, .viewType = VK_IMAGE_VIEW_TYPE_2D,
.format = f, .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
if (!v && TM.nviews < 16 && vkCreateImageView(vk_ctx()->device, &vi, NULL, &v) == VK_SUCCESS)
TM.views[TM.nviews].img = img, TM.views[TM.nviews].f = f, TM.views[TM.nviews].view = v, TM.nviews++;
plat_mutex_unlock(&tm_view_lock);
return v;
}
// Records the pass from src (sampled, sw x sh) into the pass's own RGBA16F image of dw x dh and returns it (the caller
// blits it into the target); VK_NULL_HANDLE when it cannot (the caller blits the source instead). edge: the picture is
// what the screen shows, so the edge-adaptive filter may scale it (0: another upscaler's picture replaces it).
VkImage vk_tonemap_record(VkCommandBuffer cb, VkImage src, VkFormat sf, uint32_t sw, uint32_t sh, uint32_t dw,
uint32_t dh, int edge) {
static int disp = -1; // NX2NAVI_TM_DISPATCH=0: everything but the dispatch, =2: not at all (diagnosis)
if (disp < 0) disp = rt_knob("TM_DISPATCH", "1");
if (disp == 2 || !tm_init() || !tm_out(cb, dw, dh)) return VK_NULL_HANDLE;
VkImageView sv = tm_view(src, sf), dv = TM.out_view;
if (!sv) return VK_NULL_HANDLE;
// NX2NAVI_STANDIN_FILTER, read each frame (the setting is live). The edge-adaptive filter only enlarges: a target
// smaller than the source in either direction, or of its size, keeps the linear pass.
int want = edge && dw >= sw && dh >= sh && (dw > sw || dh > sh) && !strcmp(cfg_value("NX2NAVI_STANDIN_FILTER"), "easu");
VkPipeline easu = want ? tm_easu() : VK_NULL_HANDLE;
tm_filter_log(easu != VK_NULL_HANDLE, edge, sw, sh, dw, dh);
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
.dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT};
VkDependencyInfo dep = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
vkCmdPipelineBarrier2(cb, &dep);
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, easu ? easu : TM.pipe);
// the same size both ways: each output texel takes its own source texel (see the top of the file)
VkSampler smp = !easu && dw == sw && dh == sh ? TM.nearest : TM.sampler;
VkDescriptorImageInfo ii[2] = {{smp, sv, VK_IMAGE_LAYOUT_GENERAL}, {VK_NULL_HANDLE, dv, VK_IMAGE_LAYOUT_GENERAL}};
VkWriteDescriptorSet w[2] = {
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 0, .descriptorCount = 1,
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = &ii[0]},
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 1, .descriptorCount = 1,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &ii[1]}};
rec_push(TM.push, cb, VK_PIPELINE_BIND_POINT_COMPUTE, TM.layout, 0, 2, w); // through the recorder (vk_rec.h), in order
if (easu) {
// shaders/fsr_easu.comp's constants: the part of the source that is the picture and the source's size (the
// whole image both times, as the linear pass maps it), then the output's size and the picture's corner in the
// source. Each of its groups writes a 16x16 tile.
const float pc[8] = {(float)sw, (float)sh, (float)sw, (float)sh, (float)dw, (float)dh, 0, 0};
vkCmdPushConstants(cb, TM.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, pc);
if (disp) vkCmdDispatch(cb, (dw + 15) / 16, (dh + 15) / 16, 1);
} else {
struct { float inv[2], unused, pad; int32_t size[2]; } pc = {{1.0f / (float)dw, 1.0f / (float)dh}, 0, 0,
{(int32_t)dw, (int32_t)dh}};
vkCmdPushConstants(cb, TM.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, &pc);
if (disp) vkCmdDispatch(cb, (dw + 7) / 8, (dh + 7) / 8, 1);
}
VkMemoryBarrier2 after = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
.srcAccessMask = VK_ACCESS_2_SHADER_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT,
.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT};
VkDependencyInfo dep2 = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &after};
vkCmdPipelineBarrier2(cb, &dep2);
return TM.out;
}