Files
itaskaev2andClaude Opus 5.5 f804facd8b Take the feedback-layout images back to GENERAL before the contact shadows' march
vk_contact_exec ended the pass and recorded a memory barrier only. Once a G-buffer draw samples the linear depth or the
scene colour in its own pass, those images stay in ATTACHMENT_FEEDBACK_LOOP_OPTIMAL_EXT across passes
(vk_exec_draw.c, feedback loops), and the march then bound them in GENERAL as a sampled and a storage image. The Vulkan
validation layer reported it on every frame with contact shadows on (vkCmdDispatch imageLayout-00344, followed by
barrier, BeginRendering and QueueSubmit layout errors: 40 per run at Lookout Landing, 0 with contact_shadows = 0).
vk_exec_barrier ends the pass and moves those images back to GENERAL in the same full barrier, as every other
barrier does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:59:34 +01:00

214 lines
12 KiB
C

// Screen-space contact shadows (TOTK_CONTACT_SHADOWS, percent strength, 0 = off). TotK 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.
//
// vk_contact_plan decides that in replay order and vk_contact_exec records it: a chunked replay's scan
// plans the march (vk_exec.c scan_contact) and the chunk that holds its op records it.
//
// Inputs from the game, read where the march is planned 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 read, tried, ok, strength, debug, logged;
PFN_vkCmdPushDescriptorSetKHR push;
VkDescriptorSetLayout dsl;
VkPipelineLayout layout;
VkPipeline pipe;
VkSampler sampler;
VkExecTarget scene, depth; // the last G-buffer's scene colour and linear depth
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.read) { // once: the scan asks at every draw
int s = cfg_int("TOTK_CONTACT_SHADOWS");
C.strength = s < 0 ? 0 : s > 100 ? 100 : s;
C.read = 1;
}
return C.strength;
}
static int init(VkCtx *c) {
C.tried = 1;
vk_contact_strength();
if (!C.strength) return 0;
const char *e = cfg_get("TOTK_CONTACT_DEBUG"); // 1: the frame shows the contact shadow factor in grey
C.debug = e && *e && strcmp(e, "0");
C.push = (PFN_vkCmdPushDescriptorSetKHR)vkGetDeviceProcAddr(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 VkExecTarget **scene, const VkExecTarget **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;
if (t->format == VK_FORMAT_R32_SFLOAT && !*depth) *depth = t;
}
return *scene && *depth && (*scene)->img && ((*scene)->img->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);
}
// 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.image && ex->pass_ncolor >= 1 && ex->color[0].image == C.scene.image && !ex->color[0].level &&
ex->depth.format == VK_FORMAT_D16_UNORM && ex->pass_w == C.scene.width && ex->pass_h == C.scene.height;
}
int vk_contact_plan(const VkExec *ex, int pass, VkExecJob *j) {
if (!vk_contact_strength()) return 0;
if (pass) {
const VkExecTarget *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;
if (is_gbuffer(ex, &scene, &depth)) {
C.pending = 0;
C.scene = *scene, C.depth = *depth;
return 0;
}
if (is_composite(ex)) {
C.pending = C.c4 != 0;
return 0;
}
}
if (!C.pending) return 0;
C.pending = 0;
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 0;
float w = (float)C.scene.width, h = (float)C.scene.height;
*j = (VkExecJob){.scene = C.scene, .depth = C.depth,
.pc = {px, py, zfar, (float)C.strength / 100.0f,
lx / ll, ly / ll, lz / ll, 0.6f,
w, h, 1.0f / w, 1.0f / h,
0, 0, 0, 0}};
return 1;
}
void vk_contact_exec(VkExec *ex, const VkExecJob *j) {
if (!C.tried) init(vk_ctx());
if (!C.ok) return;
VkImageView in = vk_exec_target_view(j->depth.img, VK_FORMAT_R32_SFLOAT, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_ASPECT_COLOR_BIT,
0, 0, 1, 0);
VkImageView out = vk_exec_target_view(j->scene.img, VK_FORMAT_B10G11R11_UFLOAT_PACK32, VK_IMAGE_VIEW_TYPE_2D,
VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1, 0);
if (!in || !out) return;
// A full barrier that also takes the images in the feedback layout back to GENERAL (vk_exec.c barrier_to): the
// G-buffer's linear depth and scene colour are such images once a draw sampled them in their own pass, and the march
// binds them in GENERAL. A memory barrier alone left them in ATTACHMENT_FEEDBACK_LOOP_OPTIMAL_EXT.
vk_exec_barrier(ex);
VkCommandBuffer cb = ex->cb;
float pcv[16];
memcpy(pcv, j->pc, sizeof pcv);
pcv[12] = (float)C.debug;
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", j->scene.width, j->scene.height,
pcv[4], pcv[5], pcv[6], pcv[2]);
}
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, (j->scene.width + 7) / 8, (j->scene.height + 7) / 8, 1);
mem_barrier(cb); // and what follows sees the darkened colour
ex->recorded += 6;
vk_exec_draw_new_cb(ex); // the game's bound compute pipeline and descriptors were replaced: bind them again
}
// The sequential replay's hooks; a chunk leaves the marches to the scan.
void vk_contact_pass_begin(VkExec *ex) {
VkExecJob j;
if (!ex->chunked && vk_contact_plan(ex, 1, &j)) vk_contact_exec(ex, &j);
}
void vk_contact_flush(VkExec *ex) {
VkExecJob j;
if (!ex->chunked && C.pending && vk_contact_plan(ex, 0, &j)) vk_contact_exec(ex, &j);
}
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);
}