mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
With NX2NAVI_REPLAY_PIPE=1 (off by default) the submitting thread hands a group's chunks to the workers, takes chunks of it itself until none is left to take, and goes on with the ops after the group in a new command buffer after the chunks' places in the part (vk_sync_append_later), also into the next group's front pass. Up to four groups are handed out and not in their part yet; their chunks wait in one queue in the order of the stream. The submitting thread waits for them only where the order needs it: for room, before a part goes to the GPU, before copies and texture or buffer clears, where DLAA's run or the contact shadows' hook acts, and where a lookup would make a texture entry or take back a retired one (the op goes again afterwards, as a chunk's does in turn). What the chunks read stays as the stream had it while the submitting thread runs ahead: uniform snapshot drops count from their place in the stream, a chunk reads only the inline uniform updates before its group, the HDR present's source goes by stream position, the texture pool set syncs under the chunks' lock, each group keeps its own render conditions, occlusion segments and closed query slots, and the queries after a group's last report go into the next segment made after it. A group's first chunk runs its pass hooks after the group before began its pass; turns wait for every earlier chunk, of earlier groups too. Locks the chunks take, the submitting thread takes while it is behind. Draws keep the sampled-texture descriptors they resolved per draw context and reuse them without the texture lock while their slot is unchanged; the workers waited for that lock at nearly every draw. Texture lookups that change nothing no longer wait for their turn. The perf line says where the submitting thread waited, what the chunks took, and which texture lookups waited; NX2NAVI_GPU_TIMELINE also shows the chunks and the waits. Off by default because it does not pay yet: in Hyrule Field with 3 workers it shortens the replay (11.5 against 12.1 ms a frame) but not the frame (49.2 against 49.6 fps in the same batch). The GPU gets a group's commands only once its last chunk is recorded, so the main thread's wait for the GPU after the frame's first submission grew by 0.8 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
200 lines
11 KiB
C
200 lines
11 KiB
C
// Screen-space contact shadows (NX2NAVI_CONTACT_SHADOWS, percent strength, 0 = off). NX2navi fills a full-resolution
|
|
// G-buffer whose targets include a B10G11R11 scene colour, an R32F linear depth (view distance / far) and a D32F depth
|
|
// (is_gbuffer), lights it in half resolution, and then composites the lighting into that scene colour in one pass
|
|
// whose only target is the scene colour, with a D16 depth that sorts pixels into material classes (is_composite).
|
|
// When the pass after the composite begins, or a dispatch or copy comes first, runtime/shaders/contact_shadow.comp
|
|
// marches each pixel a short way toward the sun through the linear depth and darkens the scene colour where the ray
|
|
// passes behind nearby geometry: the small shadows at feet, under stones and in grass that the shadow maps are too
|
|
// coarse for. Transparent surfaces, the sky and post-processing come after, untouched.
|
|
//
|
|
// Inputs from the game, read from the fragment stage's uniform block c4 bound in the composite pass (guest memory,
|
|
// so it may be one frame newer than the replay): the projection scales c4[7].x and c4[8].y, the far plane c4[14].y,
|
|
// and the first sun cascade's depth row c4[44].xyz, whose negated direction points from a view-space position toward
|
|
// the sun (the G-buffer shaders compare dot(c4[44].xyz, pos) + c4[44].w with the cascade map). The scene colour images
|
|
// get STORAGE usage when the setting is on (vk_texture.c create_tex).
|
|
#include "vk.h"
|
|
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
#include "cfg.h"
|
|
#include "contact_shadow_spv.h"
|
|
#include "host.h"
|
|
#include "vk_exec.h"
|
|
|
|
static struct {
|
|
int tried, ok, strength, debug, logged;
|
|
PFN_vkCmdPushDescriptorSetKHR push;
|
|
VkDescriptorSetLayout dsl;
|
|
VkPipelineLayout layout;
|
|
VkPipeline pipe;
|
|
VkSampler sampler;
|
|
const VkTexImage *scene, *depth; // the last G-buffer's scene colour and linear depth
|
|
uint32_t w, h;
|
|
int pending; // the composite into scene ran; the march waits for the next pass
|
|
uint64_t c4;
|
|
} C;
|
|
|
|
int vk_contact_strength(void) {
|
|
if (!C.tried) {
|
|
int s = cfg_int("NX2NAVI_CONTACT_SHADOWS");
|
|
C.strength = s < 0 ? 0 : s > 100 ? 100 : s;
|
|
}
|
|
return C.strength;
|
|
}
|
|
|
|
static int init(VkCtx *c) {
|
|
C.tried = 1;
|
|
vk_contact_strength();
|
|
if (!C.strength) return 0;
|
|
const char *e = cfg_get("NX2NAVI_CONTACT_DEBUG"); // 1: the frame shows the contact shadow factor in grey
|
|
C.debug = e && *e && strcmp(e, "0");
|
|
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, 64};
|
|
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, .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 contact_shadow_spv, .pCode = contact_shadow_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: contact shadows off: their 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: contact shadows off: their pipeline could not be created");
|
|
return 0;
|
|
}
|
|
rt_log("vk: contact shadows on, strength %d%%%s", C.strength, C.debug ? " (debug view)" : "");
|
|
return C.ok = 1;
|
|
}
|
|
|
|
// A G-buffer pass: a B10G11R11 scene colour, an R32F linear depth and a D32F depth, all one size, at least 640 wide.
|
|
static int is_gbuffer(const VkExec *ex, const VkTexImage **scene, const VkTexImage **depth) {
|
|
*scene = *depth = NULL;
|
|
if (ex->pass_noatt || ex->pass_w < 640 || ex->depth.format != VK_FORMAT_D32_SFLOAT || ex->depth.level) return 0;
|
|
for (uint32_t i = 0; i < ex->pass_ncolor && i < 8; i++) {
|
|
const VkExecTarget *t = &ex->color[i];
|
|
if (!t->image || t->level || t->width != ex->pass_w || t->height != ex->pass_h) continue;
|
|
if (t->format == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && !*scene) *scene = t->img;
|
|
if (t->format == VK_FORMAT_R32_SFLOAT && !*depth) *depth = t->img;
|
|
}
|
|
return *scene && *depth && ((*scene)->usage & VK_IMAGE_USAGE_STORAGE_BIT);
|
|
}
|
|
|
|
static void mem_barrier(VkCommandBuffer cb) {
|
|
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_ALL_COMMANDS_BIT,
|
|
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT};
|
|
VkDependencyInfo d = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
|
|
vkCmdPipelineBarrier2(cb, &d);
|
|
}
|
|
|
|
int vk_contact_pending(void) { return C.pending; }
|
|
|
|
void vk_contact_flush(VkExec *ex) {
|
|
// The submitting thread past a group (vk_exec.c NX2NAVI_REPLAY_PIPE): the group's first chunk may have made the
|
|
// pass due and a later one records it where the replay would; only one still due after the group is this one's.
|
|
if (vk_exec_behind()) {
|
|
vk_exec_hooks_wait();
|
|
if (!C.pending) return;
|
|
vk_exec_turn();
|
|
}
|
|
if (!C.pending) return;
|
|
C.pending = 0;
|
|
if (ex->in_pass) return;
|
|
const float *u = (const float *)(uintptr_t)C.c4;
|
|
float lx = -u[44 * 4 + 0], ly = -u[44 * 4 + 1], lz = -u[44 * 4 + 2], ll = sqrtf(lx * lx + ly * ly + lz * lz);
|
|
float px = u[7 * 4 + 0], py = u[8 * 4 + 1], zfar = u[14 * 4 + 1];
|
|
if (!(ll > 1e-6f) || !(px > 0) || !(py > 0) || !(zfar > 1)) return;
|
|
VkImageView in = vk_exec_target_view(C.depth, VK_FORMAT_R32_SFLOAT, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_ASPECT_COLOR_BIT,
|
|
0, 0, 1, 0);
|
|
VkImageView out = vk_exec_target_view(C.scene, VK_FORMAT_B10G11R11_UFLOAT_PACK32, VK_IMAGE_VIEW_TYPE_2D,
|
|
VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1, 0);
|
|
if (!in || !out) return;
|
|
VkCommandBuffer cb = ex->cb;
|
|
float pcv[16] = {px, py, zfar, (float)C.strength / 100.0f,
|
|
lx / ll, ly / ll, lz / ll, 0.6f,
|
|
(float)C.w, (float)C.h, 1.0f / (float)C.w, 1.0f / (float)C.h,
|
|
(float)C.debug, 0, 0, 0};
|
|
if (!C.logged) {
|
|
C.logged = 1;
|
|
rt_log("vk: contact shadows on a %ux%u scene, sun toward %.3f %.3f %.3f (view space), far %g", C.w, C.h,
|
|
pcv[4], pcv[5], pcv[6], zfar);
|
|
}
|
|
mem_barrier(cb); // the compose passes' writes, before the march reads and writes them
|
|
VkDescriptorImageInfo ii = {C.sampler, in, VK_IMAGE_LAYOUT_GENERAL}, oi = {VK_NULL_HANDLE, out, 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);
|
|
vkCmdPushConstants(cb, C.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pcv, pcv);
|
|
vkCmdDispatch(cb, (C.w + 7) / 8, (C.h + 7) / 8, 1);
|
|
mem_barrier(cb); // and what follows sees the darkened colour
|
|
ex->recorded += 6;
|
|
vk_exec_draw_new_cb(); // the game's bound compute pipeline and descriptors were replaced: bind them again
|
|
}
|
|
|
|
// The lighting composite: the remembered scene colour as the only target, with a D16 depth of its size.
|
|
static int is_composite(const VkExec *ex) {
|
|
return C.scene && ex->pass_ncolor >= 1 && ex->color[0].img == C.scene && !ex->color[0].level &&
|
|
ex->depth.format == VK_FORMAT_D16_UNORM && ex->pass_w == C.w && ex->pass_h == C.h;
|
|
}
|
|
|
|
void vk_contact_pass_begin(VkExec *ex) {
|
|
if (!C.tried && !init(vk_ctx())) return;
|
|
if (!C.ok) return;
|
|
const VkTexImage *scene, *depth;
|
|
// The camera block: c4 of the fragment stage, 0x940 bytes. The composite binds it only after its pass begins, so
|
|
// the newest one seen at any pass start serves.
|
|
const p_bind_buf *b = &ex->ubo[1][1];
|
|
if (b->addr && b->size == 0x940 && C.c4 != b->addr) vk_exec_hook_turn(), C.c4 = b->addr;
|
|
if (is_gbuffer(ex, &scene, &depth)) {
|
|
vk_exec_hook_turn();
|
|
C.pending = 0;
|
|
C.scene = scene, C.depth = depth, C.w = ex->pass_w, C.h = ex->pass_h;
|
|
return;
|
|
}
|
|
if (is_composite(ex)) {
|
|
vk_exec_hook_turn();
|
|
C.pending = C.c4 != 0;
|
|
return;
|
|
}
|
|
vk_contact_flush(ex);
|
|
}
|
|
|
|
void vk_contact_release(void) {
|
|
VkCtx *c = vk_ctx();
|
|
if (!c || !C.tried) return;
|
|
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);
|
|
memset(&C, 0, sizeof C);
|
|
}
|