Files
NX2navi/runtime/vk_color.c
chrg6 1f7e9c975b Speed up the replay and fix what the perf pass found, and add a frame capture key for chasing rendering faults.
Performance: draws reuse the bindings that did not change and record only the dynamic states that differ from what
the command buffer holds; small uploads come from a staging arena; finished submissions are held briefly and handed to
the queue in batches; the recorder thread can keep a CPU core of its own; ASTC block-linear textures are deswizzled on
the GPU; dynamic resolution steps the upscaler mode to hold a frame-time target. Each has a setting in nx2navi.ini
(bind_reuse, dyn_delta, stg_arena, submit_batch, recorder_core, recorder_prio, astc_gpu_deswizzle, dyn_res) and the
staging cache now frees what it holds at shutdown.

Capture: NX2NAVI_DUMP=key saves the frame after each press of Insert, with mesh draws listed by task, mesh and fragment
shader, depth test and face culling, and mesh_prims.txt giving each mesh draw's primitive, clipping and fragment counts.
All of it is off unless NX2NAVI_DUMP is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 03:34:46 -05:00

209 lines
12 KiB
C

// Colour adjustment on the present (vk_color.h): brightness, contrast, saturation and gamma of the game's picture, after
// the game and before the FSR 1 pass or the blit scale it to the window. Settings NX2NAVI_COLOR_BRIGHTNESS (-30..30, %
// added), NX2NAVI_COLOR_CONTRAST and NX2NAVI_COLOR_SATURATION (%, 100 = unchanged) and NX2NAVI_COLOR_GAMMA (exponent, 1 =
// unchanged), read when the settings change. NX2NAVI_COLOR_PRESET names a profile (vk_color_preset) whose values win
// over the four, so a profile set in nx2navi.ini or by NX2NAVI_MENU_TEST_SET applies without the menu; custom uses the
// four. At the default values nothing is recorded and the present is what it was. Design and preset values: the
// colour profiles of issue #136.
//
// One compute pass (shaders/color_grade.comp) reads the crop rectangle of the game's image and writes an RGBA16F image
// of its size, which present_image then hands to FSR or the blit in place of the game's image. The HUD is part of that
// picture and is adjusted with it; the in-game menu is drawn later, straight into the swapchain image, and is not.
// The source view of slot s is destroyed when slot s records again (present_image has waited for that slot's fence);
// the output image is shared by all slots, and each frame's first barrier orders it after the previous frame's reads.
#include "vk.h"
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "color_grade_spv.h"
#include "host.h"
#define SLOTS 8
static struct {
int tried, ok;
PFN_vkCmdPushDescriptorSetKHR push;
VkDescriptorSetLayout dsl;
VkPipelineLayout layout;
VkPipeline pipe;
VkSampler sampler;
VkImage img; // the adjusted picture, RGBA16F at the crop size
VmaAllocation alloc;
VkImageView view;
uint32_t w, h;
VkImageView src_view[SLOTS]; // the source view each slot's last frame read
unsigned seen; // cfg_version the values below were read at
float adj[4]; // brightness (added), contrast, saturation, gamma
} C = {.seen = ~0u};
static int init(VkCtx *c) {
C.tried = 1;
C.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};
VkPipelineLayoutCreateInfo li = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
.pSetLayouts = &C.dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pr};
VkSamplerCreateInfo si = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .magFilter = VK_FILTER_NEAREST,
.minFilter = VK_FILTER_NEAREST, .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 color_grade_spv,
.pCode = color_grade_spv};
VkShaderModule m = VK_NULL_HANDLE;
if (!C.push || vkCreateDescriptorSetLayout(c->device, &dci, NULL, &C.dsl) != VK_SUCCESS ||
vkCreatePipelineLayout(c->device, &li, NULL, &C.layout) != VK_SUCCESS ||
vkCreateSampler(c->device, &si, NULL, &C.sampler) != VK_SUCCESS ||
vkCreateShaderModule(c->device, &mi, NULL, &m) != VK_SUCCESS) {
rt_log("vk: colour adjustment off: its pipeline could not be created");
return 0;
}
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 = C.layout};
VkResult r = vkCreateComputePipelines(c->device, VK_NULL_HANDLE, 1, &ci, NULL, &C.pipe);
vkDestroyShaderModule(c->device, m, NULL);
if (r != VK_SUCCESS) {
rt_log("vk: colour adjustment off: its pipeline could not be created");
return 0;
}
return C.ok = 1;
}
static void free_image(VkCtx *c) {
if (C.view) vkDestroyImageView(c->device, C.view, NULL);
if (C.img) vmaDestroyImage(c->vma, C.img, C.alloc);
C.view = VK_NULL_HANDLE, C.img = VK_NULL_HANDLE, C.w = C.h = 0;
}
static int ensure_image(VkCtx *c, uint32_t w, uint32_t h) {
if (C.img && C.w == w && C.h == h) return 1;
// another crop size (a resolution change): earlier presents may still read the old image
vk_queue_lock();
vkDeviceWaitIdle(c->device);
vk_queue_unlock();
free_image(c);
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_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
if (vmaCreateImage(c->vma, &ii, &ai, &C.img, &C.alloc, NULL) != VK_SUCCESS) {
C.img = VK_NULL_HANDLE;
return 0;
}
VkImageViewCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = C.img,
.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, &C.view) != VK_SUCCESS) {
C.view = VK_NULL_HANDLE;
free_image(c);
return 0;
}
C.w = w, C.h = h;
return 1;
}
const char *const *vk_color_preset(const char *name) {
static const char *const presets[][5] = {
{"default", "0", "100", "100", "1"}, {"vivid", "0", "115", "135", "0.95"}, {"clarity", "2", "110", "115", "1"}};
for (size_t i = 0; i < sizeof presets / sizeof *presets; i++)
if (!strcmp(name, presets[i][0])) return presets[i] + 1;
return NULL;
}
// The four values from the profile, or from their own settings under custom (or an unknown profile name); re-read when
// the settings change. 1 when any differs from "unchanged".
static int adjusting(void) {
// The menu has no colour rows (ui/menu.cpp tab_screen): values left in nx2navi.ini could not be undone there, so
// the pass is off and the picture is the game's own
return 0;
unsigned v = cfg_version();
if (v != C.seen) {
C.seen = v;
const char *const *p = vk_color_preset(cfg_value("NX2NAVI_COLOR_PRESET"));
static const char *const keys[4] = {"NX2NAVI_COLOR_BRIGHTNESS", "NX2NAVI_COLOR_CONTRAST",
"NX2NAVI_COLOR_SATURATION", "NX2NAVI_COLOR_GAMMA"};
double d[4];
for (int i = 0; i < 4; i++) d[i] = p ? strtod(p[i], NULL) : cfg_double(keys[i]);
C.adj[0] = (float)d[0] / 100.0f;
C.adj[1] = (float)d[1] / 100.0f;
C.adj[2] = (float)d[2] / 100.0f;
C.adj[3] = (float)d[3];
}
return C.adj[0] != 0.0f || C.adj[1] != 1.0f || C.adj[2] != 1.0f || C.adj[3] != 1.0f;
}
static void barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkPipelineStageFlags2 ss, VkAccessFlags2 sa,
VkPipelineStageFlags2 ds, VkAccessFlags2 da) {
VkImageMemoryBarrier2 b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = ss, .srcAccessMask = sa,
.dstStageMask = ds, .dstAccessMask = da, .oldLayout = from,
.newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = img,
.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);
}
VkImage vk_color_record(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt, const int crop[4]) {
if (!adjusting()) return VK_NULL_HANDLE;
VkCtx *c = vk_ctx();
if (!C.tried) init(c);
if (!C.ok || !ensure_image(c, (uint32_t)crop[2], (uint32_t)crop[3])) return VK_NULL_HANDLE;
slot %= SLOTS;
if (C.src_view[slot]) vkDestroyImageView(c->device, C.src_view[slot], NULL);
VkImageViewCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = src,
.viewType = VK_IMAGE_VIEW_TYPE_2D, .format = fmt,
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
if (vkCreateImageView(c->device, &vi, NULL, &C.src_view[slot]) != VK_SUCCESS) {
C.src_view[slot] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
const VkPipelineStageFlags2 CS = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, ALL = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
// the game's writes to src are visible; last frame's reads of the output are done (UNDEFINED: its old content goes)
barrier(cb, src, VK_IMAGE_LAYOUT_GENERAL, ALL, VK_ACCESS_2_MEMORY_WRITE_BIT, CS, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT);
barrier(cb, C.img, VK_IMAGE_LAYOUT_UNDEFINED, ALL, 0, CS, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT);
VkDescriptorImageInfo ii = {C.sampler, C.src_view[slot], VK_IMAGE_LAYOUT_GENERAL},
oi = {VK_NULL_HANDLE, C.view, 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},
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 1, .descriptorCount = 1,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &oi}};
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, C.pipe);
rec_push(C.push, cb, VK_PIPELINE_BIND_POINT_COMPUTE, C.layout, 0, 2, w);
struct { int32_t offset[2], size[2]; float adj[4]; } pc = {{crop[0], crop[1]}, {crop[2], crop[3]},
{C.adj[0], C.adj[1], C.adj[2], C.adj[3]}};
vkCmdPushConstants(cb, C.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, &pc);
vkCmdDispatch(cb, ((uint32_t)crop[2] + 7) / 8, ((uint32_t)crop[3] + 7) / 8, 1);
// later submissions that write src wait for this read; FSR and the blit read the output
barrier(cb, src, VK_IMAGE_LAYOUT_GENERAL, CS, 0, ALL, 0);
barrier(cb, C.img, VK_IMAGE_LAYOUT_GENERAL, CS, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, ALL,
VK_ACCESS_2_MEMORY_READ_BIT);
return C.img;
}
void vk_color_release(void) {
VkCtx *c = vk_ctx();
for (int i = 0; i < SLOTS; i++)
if (C.src_view[i]) vkDestroyImageView(c->device, C.src_view[i], NULL), C.src_view[i] = VK_NULL_HANDLE;
free_image(c);
if (C.pipe) vkDestroyPipeline(c->device, C.pipe, NULL);
if (C.sampler) vkDestroySampler(c->device, C.sampler, NULL);
if (C.layout) vkDestroyPipelineLayout(c->device, C.layout, NULL);
if (C.dsl) vkDestroyDescriptorSetLayout(c->device, C.dsl, NULL);
C.pipe = VK_NULL_HANDLE, C.sampler = VK_NULL_HANDLE, C.layout = VK_NULL_HANDLE, C.dsl = VK_NULL_HANDLE;
C.tried = C.ok = 0;
}