// The Vulkan 1.3 backend: bindless textures (one descriptor array), buffer device addresses // for every per-draw and per-frame buffer (passed in push constants), dynamic rendering. #include "ui/ui.h" #include "render/backend.h" #include "render/screenshot.h" #include "rhi/vulkan/vk_device.h" #include "core/log.h" #include "platform/window.h" #include #include #include #include #include #include namespace keel { namespace { #define SPV(name) const uint32_t name[] = SPV(kBoxVert) #include "box.vert.spv.inc" ; SPV(kBoxFrag) #include "box.frag.spv.inc" ; SPV(kMeshVert) #include "mesh.vert.spv.inc" ; SPV(kMeshFrag) #include "mesh.frag.spv.inc" ; SPV(kDepthFrag) #include "depth.frag.spv.inc" ; SPV(kCullComp) #include "cull.comp.spv.inc" ; SPV(kFroxelComp) #include "froxel.comp.spv.inc" ; SPV(kFroxelRtComp) #include "froxel_rt.comp.spv.inc" ; SPV(kGiComp) #include "gi.comp.spv.inc" ; SPV(kSkinnedVert) #include "skinned.vert.spv.inc" ; SPV(kFullscreenVert) #include "fullscreen.vert.spv.inc" ; SPV(kTonemapFrag) #include "tonemap.frag.spv.inc" ; SPV(kSkyVert) #include "sky.vert.spv.inc" ; SPV(kSkyFrag) #include "sky.frag.spv.inc" ; SPV(kSmokeSkyFrag) #include "smoke_sky.frag.spv.inc" ; SPV(kHzbFrag) #include "hzb.frag.spv.inc" ; SPV(kSsaoFrag) #include "ssao.frag.spv.inc" ; SPV(kSsaoBlurFrag) #include "ssao_blur.frag.spv.inc" ; SPV(kBloomDownFrag) #include "bloom_down.frag.spv.inc" ; SPV(kBloomUpFrag) #include "bloom_up.frag.spv.inc" ; SPV(kTaaFrag) #include "taa.frag.spv.inc" ; SPV(kSsrFrag) #include "ssr.frag.spv.inc" ; SPV(kVolumetricFrag) #include "volumetric.frag.spv.inc" ; SPV(kSpriteVert) #include "sprite.vert.spv.inc" ; SPV(kSpriteFrag) #include "sprite.frag.spv.inc" ; SPV(kAdaptFrag) #include "adapt.frag.spv.inc" ; #undef SPV struct BoxPush { glm::mat4 viewProj; gpu::Box box; }; struct ModelPush { // matches model.glsl (Vulkan) glm::mat4 viewProj; VkDeviceAddress vertices, skin, joints, draw, frame; uint32_t textures; // albedo | lightmap << 16 uint32_t tint; uint32_t material; // relief + gloss map, 0 = none }; struct TonemapPush { uint32_t scene; float exposure; uint32_t ao, bloom; float bloomStrength, saturation, vignette, sharpen; uint32_t depth, pad[3]; glm::vec4 shafts, sunColor; }; struct SsaoPush { uint32_t depth, w, h, outW, outH; float radius, strength, pad; glm::vec4 proj; }; struct SsaoBlurPush { uint32_t ao, depth, w, h, outW, outH; float pad0, pad1; glm::vec4 proj; }; struct TaaPush { glm::mat4 reproject; uint32_t scene, history, depth, w, h, reset, ssr; float pad1; }; struct AdaptPush { uint32_t src, prev; float dt, speed, key, minE, maxE, pad; }; struct SpritePush { glm::mat4 viewProj; gpu::Sprite sprite; }; struct VolumetricPush { uint64_t frame; uint32_t depth, w, h, outW, outH; float pad[5]; // proj sits at 48 (vec4 alignment) glm::vec4 proj; }; struct SsrPush { uint32_t depth, gbuf, scene, w, h, outW, outH; float strength; glm::vec4 proj; }; struct BloomPush { uint32_t src, srcW, srcH, outW, outH; uint32_t extra; // down: first level; up: radius (float bits) }; struct HzbPush { uint32_t depth, srcW, srcH, dstW, dstH; }; static_assert(sizeof(BoxPush) <= 128 && sizeof(ModelPush) <= 128 && sizeof(gpu::SkyPush) <= 128 && sizeof(SsaoPush) <= 128, "push constants must fit the guaranteed 128 bytes"); constexpr VkShaderStageFlags kAllStages = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_COMPUTE_BIT; constexpr VkFormat kSceneFormat = VK_FORMAT_R16G16B16A16_SFLOAT; constexpr VkFormat kGbufFormat = VK_FORMAT_R16G16B16A16_SFLOAT; // view normal (oct), roughness, reflection weight constexpr VkFormat kDepthFormat = VK_FORMAT_D32_SFLOAT; // One per-frame buffer holds the lights, shadow views, cluster cells and light indices. constexpr VkDeviceSize kLightsOffset = 0, kViewsOffset = kMaxLights * sizeof(gpu::Light), kCellsOffset = kViewsOffset + kMaxShadowViews * sizeof(gpu::ShadowView), kIndicesOffset = kCellsOffset + kClusterX * kClusterY * kClusterZ * 8, kLightBufferSize = kIndicesOffset + kMaxLightIndices * 4; struct PipelineDesc { const uint32_t* vs; size_t vsSize; const uint32_t* fs; size_t fsSize; VkFormat color; // UNDEFINED = depth only (shadow maps) bool depth; bool depthBias = false; bool blend = false; // alpha blended, depth test without writes (translucent pass) bool sky = false; // on the far plane: depth less-or-equal, no writes bool additive = false; // adds onto what's there (bloom coming up) bool over = false; // premultiplied, onto what's there: src + dst × src alpha (fog, smoke) int gbuf = 0; // the scene pass's second attachment (G-buffer): 0 none, 1 there but not written, 2 written bool glow = false; // light added on by its alpha, depth tested but not written (particles) bool prepassed = false; // the depth is already there (pre-pass): test less-or-equal, don't write }; VkPipeline makePipeline(VkDevice dev, VkPipelineLayout layout, const PipelineDesc& d) { auto shader = [&](const uint32_t* code, size_t size) { VkShaderModuleCreateInfo ci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; ci.codeSize = size; ci.pCode = code; VkShaderModule m; VK_CHECK(vkCreateShaderModule(dev, &ci, nullptr, &m)); return m; }; VkShaderModule vs = shader(d.vs, d.vsSize), fs = d.fs ? shader(d.fs, d.fsSize) : VK_NULL_HANDLE; VkPipelineShaderStageCreateInfo stages[2] = { {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_VERTEX_BIT, vs, "main", nullptr}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_FRAGMENT_BIT, fs, "main", nullptr}, }; VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPipelineViewportStateCreateInfo vp{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; vp.viewportCount = 1; vp.scissorCount = 1; VkPipelineRasterizationStateCreateInfo rs{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.lineWidth = 1.0f; if (d.depthBias) { rs.depthBiasEnable = VK_TRUE; rs.depthBiasConstantFactor = 2.0f; rs.depthBiasSlopeFactor = 2.5f; } VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkPipelineDepthStencilStateCreateInfo ds{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; ds.depthTestEnable = d.depth; ds.depthWriteEnable = d.depth; ds.depthCompareOp = d.sky ? VK_COMPARE_OP_LESS_OR_EQUAL : VK_COMPARE_OP_LESS; if (d.sky) ds.depthWriteEnable = VK_FALSE; if (d.prepassed) ds.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL, ds.depthWriteEnable = VK_FALSE; VkPipelineColorBlendAttachmentState blendAtt{}; blendAtt.colorWriteMask = 0xF; if (d.additive) { blendAtt.blendEnable = VK_TRUE; blendAtt.srcColorBlendFactor = blendAtt.dstColorBlendFactor = VK_BLEND_FACTOR_ONE; blendAtt.srcAlphaBlendFactor = blendAtt.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; blendAtt.colorBlendOp = blendAtt.alphaBlendOp = VK_BLEND_OP_ADD; } if (d.over) { blendAtt.blendEnable = VK_TRUE; blendAtt.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; blendAtt.dstColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; blendAtt.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; blendAtt.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; blendAtt.colorBlendOp = blendAtt.alphaBlendOp = VK_BLEND_OP_ADD; } if (d.glow) { blendAtt.blendEnable = VK_TRUE; blendAtt.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; blendAtt.dstColorBlendFactor = VK_BLEND_FACTOR_ONE; blendAtt.srcAlphaBlendFactor = blendAtt.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; blendAtt.colorBlendOp = blendAtt.alphaBlendOp = VK_BLEND_OP_ADD; ds.depthWriteEnable = VK_FALSE; } if (d.blend) { blendAtt.blendEnable = VK_TRUE; blendAtt.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; blendAtt.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; blendAtt.colorBlendOp = VK_BLEND_OP_ADD; blendAtt.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; blendAtt.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; blendAtt.alphaBlendOp = VK_BLEND_OP_ADD; ds.depthWriteEnable = VK_FALSE; } const bool hasColor = d.color != VK_FORMAT_UNDEFINED; VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; VkPipelineColorBlendAttachmentState atts[2] = {blendAtt, {}}; atts[1].colorWriteMask = d.gbuf == 2 ? 0xF : 0; blend.attachmentCount = hasColor ? (d.gbuf ? 2 : 1) : 0; blend.pAttachments = atts; VkDynamicState dynStates[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; VkPipelineDynamicStateCreateInfo dyn{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; dyn.dynamicStateCount = 2; dyn.pDynamicStates = dynStates; VkPipelineRenderingCreateInfo rendering{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO}; const VkFormat formats[2] = {d.color, kGbufFormat}; rendering.colorAttachmentCount = hasColor ? (d.gbuf ? 2 : 1) : 0; rendering.pColorAttachmentFormats = hasColor ? formats : nullptr; rendering.depthAttachmentFormat = d.depth ? kDepthFormat : VK_FORMAT_UNDEFINED; VkGraphicsPipelineCreateInfo pci{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; pci.pNext = &rendering; pci.stageCount = fs ? 2 : 1; pci.pStages = stages; pci.pVertexInputState = &vertexInput; pci.pInputAssemblyState = &ia; pci.pViewportState = &vp; pci.pRasterizationState = &rs; pci.pMultisampleState = &ms; pci.pDepthStencilState = &ds; pci.pColorBlendState = &blend; pci.pDynamicState = &dyn; pci.layout = layout; VkPipeline p; VK_CHECK(vkCreateGraphicsPipelines(dev, VK_NULL_HANDLE, 1, &pci, nullptr, &p)); vkDestroyShaderModule(dev, vs, nullptr); if (fs) vkDestroyShaderModule(dev, fs, nullptr); return p; } class VulkanBackend final : public RenderBackend { public: const char* name() const override { return "Vulkan 1.3"; } GpuInfo gpuInfo() const override { GpuInfo g{device_.gpuName(), device_.gpuDriver()}; device_.vram(g.vramTotal, g.vramUsed, g.vramOurs); return g; } bool init(const Window& window, bool validation) override { if (!device_.init(window, validation)) return false; swapFormat_ = device_.swapchainFormat(); createBindless(); createPipelines(); // GPU timing (r_profile): timestamps between passes, one set per frame in flight. VkQueryPoolCreateInfo qi{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; qi.queryType = VK_QUERY_TYPE_TIMESTAMP; qi.queryCount = vk::Device::kFramesInFlight * kMaxMarks; vkCreateQueryPool(device_.device(), &qi, nullptr, &queries_); VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(device_.physicalDevice(), &props); tickNs_ = props.limits.timestampPeriod; return initImGui(window); } // ── GPU timing ── static constexpr uint32_t kMaxMarks = 16; VkQueryPool queries_ = VK_NULL_HANDLE; float tickNs_ = 1.0f; const char* markNames_[vk::Device::kFramesInFlight][kMaxMarks] = {}; uint32_t markCount_[vk::Device::kFramesInFlight] = {}; std::vector> gpuTimes_; // This slot's last results (its fence has passed), then start it again. void beginMarks(VkCommandBuffer cmd) { const uint32_t fi = device_.frameIndex(), n = markCount_[fi]; if (n > 1) { uint64_t t[kMaxMarks]; if (vkGetQueryPoolResults(device_.device(), queries_, fi * kMaxMarks, n, sizeof t, t, sizeof(uint64_t), VK_QUERY_RESULT_64_BIT) == VK_SUCCESS) { gpuTimes_.clear(); for (uint32_t i = 1; i < n; ++i) gpuTimes_.push_back({markNames_[fi][i], float(double(t[i] - t[i - 1]) * tickNs_ * 1e-6)}); } } vkCmdResetQueryPool(cmd, queries_, fi * kMaxMarks, kMaxMarks); markCount_[fi] = 0; mark(cmd, "start"); } // The time now on the GPU; the span since the last mark is called `name`. void mark(VkCommandBuffer cmd, const char* name) { const uint32_t fi = device_.frameIndex(); if (!queries_ || markCount_[fi] >= kMaxMarks) return; markNames_[fi][markCount_[fi]] = name; vkCmdWriteTimestamp(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, queries_, fi * kMaxMarks + markCount_[fi]++); } void gpuTimes(std::vector>& out) const override { out = gpuTimes_; } void shutdown() override { if (!device_.device()) return; device_.waitIdle(); if (queries_) vkDestroyQueryPool(device_.device(), queries_, nullptr), queries_ = VK_NULL_HANDLE; if (imgui_) { ImGui_ImplVulkan_Shutdown(); ImGui_ImplSDL3_Shutdown(); ImGui::DestroyContext(); imgui_ = false; } VkDevice dev = device_.device(); destroyTexture(scene_); destroyTexture(depth_); for (Texture* t : {&mirror_, &mirrorGbuf_, &mirrorDepth_}) destroyTexture(*t); for (auto& b : mirrorFrames_) device_.destroyBuffer(b); for (auto& b : smoke_) device_.destroyBuffer(b); destroyTexture(hzb_); for (Texture& t : hzbMips_) destroyTexture(t); destroyTexture(ao_); destroyTexture(gbuf_); destroyTexture(ssr_); for (Texture& t : adapt_) destroyTexture(t); destroyTexture(aoBlur_); for (Texture& t : bloom_) destroyTexture(t); for (Texture& t : taa_) destroyTexture(t); for (auto& b : hzbReadback_) device_.destroyBuffer(b); for (Texture& t : textures_) destroyTexture(t); for (auto& [frame, m] : retired_) destroyModel(m); for (auto& [frame, t] : retiredTextures_) destroyTexture(t); for (Model& m : models_) destroyModel(m); for (vk::Buffer* b : {&froxelCells_[0], &froxelCells_[1], &froxelOut_, &smokeSky_, &giProbes_[0], &giProbes_[1]}) device_.destroyBuffer(*b); for (Tlas& t : tlas_) destroyTlas(t); for (vk::Buffer* list : {jointBuffers_, drawBuffers_, frameBuffers_, lightBuffers_, cullBoxes_, cullCmds_, cullCounter_}) for (uint32_t i = 0; i < vk::Device::kFramesInFlight; ++i) device_.destroyBuffer(list[i]); destroyTexture(shadowMap_); destroyTexture(shadowAtlas_); for (VkPipeline p : {meshBlendPipeline_, skinnedBlendPipeline_, boxPipeline_, meshPipeline_, skinnedPipeline_, shadowMeshPipeline_, shadowSkinnedPipeline_, cullPipeline_, froxelPipeline_, froxelRtPipeline_, giPipeline_, depthMeshPipeline_, depthSkinnedPipeline_, meshAfterDepthPipeline_, skinnedAfterDepthPipeline_, tonemapPipeline_, skyPipeline_, hzbPipeline_, ssaoPipeline_, ssaoBlurPipeline_, bloomDownPipeline_, bloomUpPipeline_, taaPipeline_, ssrPipeline_, adaptPipeline_, volumetricPipeline_, volumetricAddPipeline_, spritePipeline_, glowPipeline_, smokeSkyPipeline_}) vkDestroyPipeline(dev, p, nullptr); vkDestroyPipelineLayout(dev, layout_, nullptr); vkDestroyDescriptorPool(dev, pool_, nullptr); vkDestroyDescriptorSetLayout(dev, setLayout_, nullptr); vkDestroySampler(dev, sampler_, nullptr); vkDestroySampler(dev, shadowSampler_, nullptr); vkDestroySampler(dev, clampSampler_, nullptr); device_.shutdown(); } void beginUi() override { ImGui_ImplVulkan_NewFrame(); ImGui_ImplSDL3_NewFrame(); ui::applyPixelScale(); ImGui::NewFrame(); } void setVsync(bool on) override { device_.setVsync(on); } void onResize() override { device_.markResized(); } bool renderScale() const override { return true; } bool blockCompression() const override { VkFormatProperties p{}; vkGetPhysicalDeviceFormatProperties(device_.physicalDevice(), VK_FORMAT_BC7_SRGB_BLOCK, &p); return p.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; } void createTexture(TextureId slot, uint32_t w, uint32_t h, bool srgb, const std::vector>& mips, TexFormat fmt = TexFormat::RGBA8) override { const VkFormat format = fmt == TexFormat::BC7 ? (srgb ? VK_FORMAT_BC7_SRGB_BLOCK : VK_FORMAT_BC7_UNORM_BLOCK) : (srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM); Texture t; VkImageCreateInfo ici{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; ici.imageType = VK_IMAGE_TYPE_2D; ici.format = format; ici.extent = {w, h, 1}; ici.mipLevels = uint32_t(mips.size()); ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; ici.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; VmaAllocationCreateInfo aci{}; aci.usage = VMA_MEMORY_USAGE_AUTO; VK_CHECK(vmaCreateImage(device_.allocator(), &ici, &aci, &t.image, &t.alloc, nullptr)); size_t total = 0; for (auto& m : mips) total += m.size(); vk::Buffer staging = device_.createBuffer(total, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true); std::vector copies; size_t offset = 0; for (uint32_t level = 0; level < mips.size(); ++level) { std::memcpy((uint8_t*)staging.mapped + offset, mips[level].data(), mips[level].size()); VkBufferImageCopy c{}; c.bufferOffset = offset; c.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, 1}; c.imageExtent = {std::max(1u, w >> level), std::max(1u, h >> level), 1}; copies.push_back(c); offset += mips[level].size(); } device_.immediateSubmit([&](VkCommandBuffer cmd) { vk::transitionImage(cmd, t.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); vkCmdCopyBufferToImage(cmd, staging.buffer, t.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, uint32_t(copies.size()), copies.data()); vk::transitionImage(cmd, t.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); }); device_.destroyBuffer(staging); VkImageViewCreateInfo vci{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; vci.image = t.image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = format; vci.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, uint32_t(mips.size()), 0, 1}; VK_CHECK(vkCreateImageView(device_.device(), &vci, nullptr, &t.view)); if (textures_.size() <= slot) textures_.resize(slot + 1); // Replaced (hot reload): frames in flight may still sample the old one. if (textures_[slot].image) retiredTextures_.emplace_back(frameNumber_, textures_[slot]); textures_[slot] = t; bind(slot, t.view); } void uploadModel(uint32_t index, const KmdlModel& kmdl) override { if (models_.size() <= index) models_.resize(index + 1); if (models_[index].vertices.buffer) retired_.emplace_back(frameNumber_, models_[index]); Model m; if (!kmdl.vertices.empty() && !kmdl.indices.empty()) { const VkBufferUsageFlags rt = device_.rayQuery() ? VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR : 0; m.vertices = device_.uploadBuffer(kmdl.vertices.data(), kmdl.vertices.size() * sizeof(KmdlVertex), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | rt); m.indices = device_.uploadBuffer(kmdl.indices.data(), kmdl.indices.size() * sizeof(uint32_t), VK_BUFFER_USAGE_INDEX_BUFFER_BIT | rt); if (device_.rayQuery() && kmdl.skin.empty()) buildBlas(m, kmdl); if (!kmdl.skin.empty()) m.skin = device_.uploadBuffer(kmdl.skin.data(), kmdl.skin.size() * sizeof(KmdlSkin), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } models_[index] = m; } uint64_t uiTexture(TextureId slot) override { if (slot >= textures_.size() || !textures_[slot].view) return 0; auto it = uiTextures_.find(slot); if (it != uiTextures_.end() && it->second.view != textures_[slot].view) { // (the slot's picture was replaced: a new set, the old one let go later) retiredUiSets_.push_back({frameNumber_, it->second.set}); uiTextures_.erase(it), it = uiTextures_.end(); } if (it == uiTextures_.end()) it = uiTextures_.emplace(slot, UiTexture{uint64_t(ImGui_ImplVulkan_AddTexture(sampler_, textures_[slot].view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)), textures_[slot].view}).first; return it->second.set; } bool beginFrame(uint32_t& width, uint32_t& height) override { cmd_ = device_.beginFrame(); ++frameNumber_; // Models replaced a few frames ago: no frame in flight uses their buffers any more. std::erase_if(retiredTextures_, [&](auto& r) { if (frameNumber_ - r.first <= vk::Device::kFramesInFlight + 1) return false; destroyTexture(r.second); return true; }); std::erase_if(retiredUiSets_, [&](auto& r) { if (frameNumber_ - r.first <= vk::Device::kFramesInFlight + 1) return false; ImGui_ImplVulkan_RemoveTexture(VkDescriptorSet(r.second)); return true; }); std::erase_if(retired_, [&](auto& r) { if (frameNumber_ - r.first <= vk::Device::kFramesInFlight + 1) return false; destroyModel(r.second); return true; }); if (!cmd_) return false; // This slot's last frame has finished (its fence passed): its occlusion grid is ready. if (const uint32_t fi = device_.frameIndex(); hzbTags_[fi]) { vmaInvalidateAllocation(device_.allocator(), hzbReadback_[fi].alloc, 0, VK_WHOLE_SIZE); const float* d = static_cast(hzbReadback_[fi].mapped); if (hzbTags_[fi] > hzbLatest_.tag) hzbLatest_ = {hzbTags_[fi], std::vector(d, d + kHzbWidth * kHzbHeight)}; hzbTags_[fi] = 0; } const VkExtent2D e = device_.extent(); // (targets are sized to the scene in drawFrame) width = e.width; height = e.height; return true; } void render(FramePacket& p) override; bool takeHzb(HzbReadback& out) override { if (hzbLatest_.tag <= hzbTaken_) return false; out = hzbLatest_; hzbTaken_ = hzbLatest_.tag; return true; } private: struct Texture { VkImage image = VK_NULL_HANDLE; VmaAllocation alloc = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; }; // A ray-tracing shape (bottom-level acceleration structure): one per submesh, by first index. struct Blas { VkAccelerationStructureKHR as = VK_NULL_HANDLE; vk::Buffer buffer; VkDeviceAddress address = 0; }; struct Model { vk::Buffer vertices, indices, skin; // skin: KmdlSkin per vertex, skinned models only std::vector> blas; // with ray queries }; void destroyTexture(Texture& t) { if (t.view) vkDestroyImageView(device_.device(), t.view, nullptr); if (t.image) vmaDestroyImage(device_.allocator(), t.image, t.alloc); t = {}; } void destroyModel(Model& m) { for (auto& [first, b] : m.blas) { vkDestroyAccelerationStructureKHR(device_.device(), b.as, nullptr); device_.destroyBuffer(b.buffer); } m.blas.clear(); device_.destroyBuffer(m.vertices); device_.destroyBuffer(m.indices); device_.destroyBuffer(m.skin); } // Each submesh's ray-tracing shape, built once (now, waiting for it). void buildBlas(Model& m, const KmdlModel& kmdl) { VkDevice dev = device_.device(); std::vector geoms; std::vector infos; std::vector ranges; std::vector scratch; geoms.reserve(kmdl.submeshes.size()); for (const KmdlSubmesh& sm : kmdl.submeshes) { if (sm.indexCount < 3) continue; VkAccelerationStructureGeometryKHR g{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR}; g.geometryType = VK_GEOMETRY_TYPE_TRIANGLES_KHR; g.flags = VK_GEOMETRY_OPAQUE_BIT_KHR; auto& t = g.geometry.triangles; t.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR; t.vertexFormat = VK_FORMAT_R32G32B32_SFLOAT; t.vertexData.deviceAddress = m.vertices.address; t.vertexStride = sizeof(KmdlVertex); t.maxVertex = uint32_t(kmdl.vertices.size() - 1); t.indexType = VK_INDEX_TYPE_UINT32; t.indexData.deviceAddress = m.indices.address; geoms.push_back(g); VkAccelerationStructureBuildGeometryInfoKHR bi{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR}; bi.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR; bi.flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR; bi.mode = VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR; bi.geometryCount = 1; bi.pGeometries = &geoms.back(); const uint32_t prims = sm.indexCount / 3; VkAccelerationStructureBuildSizesInfoKHR sz{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR}; vkGetAccelerationStructureBuildSizesKHR(dev, VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR, &bi, &prims, &sz); Blas b; b.buffer = device_.createBuffer(sz.accelerationStructureSize, VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR, false); VkAccelerationStructureCreateInfoKHR ci{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR}; ci.buffer = b.buffer.buffer; ci.size = sz.accelerationStructureSize; ci.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR; VK_CHECK(vkCreateAccelerationStructureKHR(dev, &ci, nullptr, &b.as)); VkAccelerationStructureDeviceAddressInfoKHR ai{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR}; ai.accelerationStructure = b.as; b.address = vkGetAccelerationStructureDeviceAddressKHR(dev, &ai); scratch.push_back(device_.createBuffer(sz.buildScratchSize + scratchAlign_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, false)); bi.dstAccelerationStructure = b.as; bi.scratchData.deviceAddress = alignUp(scratch.back().address, scratchAlign_); infos.push_back(bi); ranges.push_back({prims, sm.firstIndex * 4u, 0, 0}); m.blas.emplace_back(sm.firstIndex, b); } if (infos.empty()) return; std::vector rangePtrs; for (auto& r : ranges) rangePtrs.push_back(&r); device_.immediateSubmit([&](VkCommandBuffer cmd) { vkCmdBuildAccelerationStructuresKHR(cmd, uint32_t(infos.size()), infos.data(), rangePtrs.data()); }); for (auto& b : scratch) device_.destroyBuffer(b); } static VkDeviceAddress alignUp(VkDeviceAddress a, VkDeviceAddress align) { return (a + align - 1) / align * align; } void bind(TextureId slot, VkImageView view) { VkDescriptorImageInfo info{VK_NULL_HANDLE, view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; VkWriteDescriptorSet w{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; w.dstSet = set_; w.dstBinding = 0; w.dstArrayElement = slot; w.descriptorCount = 1; w.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; w.pImageInfo = &info; vkUpdateDescriptorSets(device_.device(), 1, &w, 0, nullptr); } Texture makeTarget(VkFormat format, VkExtent2D extent, VkImageUsageFlags usage, VkImageAspectFlags aspect) { Texture t; VkImageCreateInfo ici{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; ici.imageType = VK_IMAGE_TYPE_2D; ici.format = format; ici.extent = {extent.width, extent.height, 1}; ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; ici.usage = usage; VmaAllocationCreateInfo aci{}; aci.usage = VMA_MEMORY_USAGE_AUTO; aci.flags = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT; VK_CHECK(vmaCreateImage(device_.allocator(), &ici, &aci, &t.image, &t.alloc, nullptr)); VkImageViewCreateInfo vci{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; vci.image = t.image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = format; vci.subresourceRange = {aspect, 0, 1, 0, 1}; VK_CHECK(vkCreateImageView(device_.device(), &vci, nullptr, &t.view)); return t; } void createBindless(); void createPipelines(); bool initImGui(const Window& window); void ensureTargets(VkExtent2D extent); void drawMeshes(VkCommandBuffer cmd, const std::vector& draws, const glm::mat4& viewProj, bool shadow, VkPipeline mesh, VkPipeline skinned, const vk::Buffer* indirect = nullptr, bool skipLiquid = false); void shadowPasses(VkCommandBuffer cmd, Texture& target, uint32_t size, const std::vector& passes); vk::Device device_; // GPU occlusion culling (cull.comp): per frame in flight, the opaque draws' boxes, their // indirect commands (written by the GPU) and how many it hid. struct CullBox { glm::vec4 lo, hi; glm::uvec4 draw; }; struct CullPush { glm::mat4 viewProj; VkDeviceAddress boxes, cmds, counter; uint32_t count, hzb, occlusion, pad; }; static constexpr uint32_t kMaxCulled = 32768; vk::Buffer cullBoxes_[vk::Device::kFramesInFlight], cullCmds_[vk::Device::kFramesInFlight], cullCounter_[vk::Device::kFramesInFlight]; VkPipeline cullPipeline_ = VK_NULL_HANDLE; bool hzbReady_ = false, hzbNext_ = false; // the grid holds last frame's depth (and is readable) Texture hzbMips_[kHzbLevels]; // [0] unused (that's hzb_) uint32_t gpuCulled_ = 0; const vk::Buffer* cullFor_ = nullptr; // this frame's commands, while the opaque lists are drawn bool gpuCulling() const override { return cullPipeline_ != VK_NULL_HANDLE; } // The fog grid (froxel.comp): two cell buffers (this frame's, last frame's) and the integrated one. struct FroxelPush { VkDeviceAddress frame, cur, prev, integrated; glm::vec4 proj, prevPos, prevFwd; uint32_t mode, frameIndex; VkDeviceAddress tlas, smoke, smokeSky; glm::vec4 smokeOrigin; }; struct SmokeSkyPush { VkDeviceAddress frame, sky; }; vk::Buffer smokeSky_; // the fog grid's columns, smoke alone (smoke_sky.frag) VkPipeline smokeSkyPipeline_ = VK_NULL_HANDLE; bool smokeSkyOn_ = false; // this frame static_assert(sizeof(FroxelPush) == 128); // (the whole push range) vk::Buffer smoke_[vk::Device::kFramesInFlight]; // the smoke grid, written each frame there is smoke static constexpr uint32_t kFroxelCount = 160 * 90 * 64; // kFroxels in froxel.glsl vk::Buffer froxelCells_[2], froxelOut_; VkPipeline froxelPipeline_ = VK_NULL_HANDLE, froxelRtPipeline_ = VK_NULL_HANDLE; // (rays: sky seen from each cell) uint32_t froxelFrame_ = 0; glm::vec4 froxelPrevPos_{0.0f}, froxelPrevFwd_{0.0f}; bool froxelFog() const override { return froxelPipeline_ != VK_NULL_HANDLE; } // Real-time GI (gi.comp): the scene for rays (per frame in flight), and the probes (this // frame's, last frame's). struct Tlas { VkAccelerationStructureKHR as = VK_NULL_HANDLE; vk::Buffer buffer, scratch, instances, info; VkDeviceAddress address = 0; uint32_t capacity = 0; }; struct RayInst { VkDeviceAddress vertices, indices; uint32_t firstIndex, textures, tint, pad; }; // RayInst in gi.comp struct GiPush { VkDeviceAddress frame, cur, prev, insts; VkDeviceAddress tlas; uint32_t fresh; float hysteresis; glm::vec4 rotation; }; static constexpr uint32_t kGiProbes = 24 * 24 * 12; // kGiDims in gi.glsl static constexpr uint32_t kGiProbeSize = 128; // GiProbe Tlas tlas_[vk::Device::kFramesInFlight]; vk::Buffer giProbes_[2]; VkPipeline giPipeline_ = VK_NULL_HANDLE; uint32_t giFrame_ = 0, giIdle_ = 0; bool giFresh_ = true, giOn_ = false; VkDeviceAddress scratchAlign_ = 256; bool rayTracing() const override { return giPipeline_ != VK_NULL_HANDLE; } void destroyTlas(Tlas& t) { if (t.as) vkDestroyAccelerationStructureKHR(device_.device(), t.as, nullptr); for (vk::Buffer* b : {&t.buffer, &t.scratch, &t.instances, &t.info}) device_.destroyBuffer(*b); t = {}; } // Room for n instances (this slot's last frame is done with it). void ensureTlas(Tlas& t, uint32_t n) { if (t.capacity >= n) return; destroyTlas(t); uint32_t cap = 1024; while (cap < n) cap *= 2; VkDevice dev = device_.device(); t.instances = device_.createBuffer(cap * sizeof(VkAccelerationStructureInstanceKHR), VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, true); t.info = device_.createBuffer(cap * sizeof(RayInst), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true); VkAccelerationStructureGeometryKHR g{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR}; g.geometryType = VK_GEOMETRY_TYPE_INSTANCES_KHR; g.geometry.instances.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_INSTANCES_DATA_KHR; VkAccelerationStructureBuildGeometryInfoKHR bi{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR}; bi.type = VK_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL_KHR; bi.flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_BUILD_BIT_KHR; bi.geometryCount = 1; bi.pGeometries = &g; VkAccelerationStructureBuildSizesInfoKHR sz{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR}; vkGetAccelerationStructureBuildSizesKHR(dev, VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR, &bi, &cap, &sz); t.buffer = device_.createBuffer(sz.accelerationStructureSize, VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR, false); t.scratch = device_.createBuffer(sz.buildScratchSize + scratchAlign_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, false); VkAccelerationStructureCreateInfoKHR ci{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR}; ci.buffer = t.buffer.buffer; ci.size = sz.accelerationStructureSize; ci.type = VK_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL_KHR; VK_CHECK(vkCreateAccelerationStructureKHR(dev, &ci, nullptr, &t.as)); VkAccelerationStructureDeviceAddressInfoKHR ai{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR}; ai.accelerationStructure = t.as; t.address = vkGetAccelerationStructureDeviceAddressKHR(dev, &ai); t.capacity = cap; } uint32_t gpuCulled() const override { return gpuCulled_; } VkFormat swapFormat_ = VK_FORMAT_UNDEFINED; VkCommandBuffer cmd_ = VK_NULL_HANDLE; bool imgui_ = false; VkDescriptorSetLayout setLayout_ = VK_NULL_HANDLE; VkDescriptorPool pool_ = VK_NULL_HANDLE; VkDescriptorSet set_ = VK_NULL_HANDLE; VkSampler sampler_ = VK_NULL_HANDLE, shadowSampler_ = VK_NULL_HANDLE, clampSampler_ = VK_NULL_HANDLE; VkPipelineLayout layout_ = VK_NULL_HANDLE; // shared: bindless set + 128 bytes of push constants VkPipeline boxPipeline_ = VK_NULL_HANDLE, meshPipeline_ = VK_NULL_HANDLE, meshBlendPipeline_ = VK_NULL_HANDLE, skinnedPipeline_ = VK_NULL_HANDLE, skinnedBlendPipeline_ = VK_NULL_HANDLE, shadowMeshPipeline_ = VK_NULL_HANDLE, shadowSkinnedPipeline_ = VK_NULL_HANDLE, depthMeshPipeline_ = VK_NULL_HANDLE, depthSkinnedPipeline_ = VK_NULL_HANDLE, meshAfterDepthPipeline_ = VK_NULL_HANDLE, skinnedAfterDepthPipeline_ = VK_NULL_HANDLE, tonemapPipeline_ = VK_NULL_HANDLE, skyPipeline_ = VK_NULL_HANDLE, hzbPipeline_ = VK_NULL_HANDLE, ssaoPipeline_ = VK_NULL_HANDLE, ssaoBlurPipeline_ = VK_NULL_HANDLE, bloomDownPipeline_ = VK_NULL_HANDLE, bloomUpPipeline_ = VK_NULL_HANDLE, taaPipeline_ = VK_NULL_HANDLE, ssrPipeline_ = VK_NULL_HANDLE, adaptPipeline_ = VK_NULL_HANDLE, volumetricPipeline_ = VK_NULL_HANDLE, volumetricAddPipeline_ = VK_NULL_HANDLE, spritePipeline_ = VK_NULL_HANDLE, glowPipeline_ = VK_NULL_HANDLE; Texture adapt_[2]; uint32_t adaptCur_ = 0; bool adaptValid_ = false; // the targets were (re)made: no history yet Texture taa_[2]; uint32_t shadowMapSize_ = kShadowSize; // TAA history: written this frame / read from last frame, alternating uint32_t taaCur_ = 0; // Post-processing targets (sized with the screen). Texture ao_, aoBlur_, bloom_[kBloomLevels], gbuf_, ssr_; VkExtent2D aoExtent_{}, bloomExtent_[kBloomLevels]{}; // Occlusion grid: rendered, copied to a readback buffer per frame in flight, read when that // frame's fence has passed. Texture hzb_; vk::Buffer hzbReadback_[vk::Device::kFramesInFlight]; uint64_t hzbTags_[vk::Device::kFramesInFlight] = {}; HzbReadback hzbLatest_; uint64_t hzbTaken_ = 0; Texture shadowMap_, shadowAtlas_, scene_, depth_; // Mirrors: the reflected scene (and the G-buffer and depth its pass needs), the scene's size. Texture mirror_, mirrorGbuf_, mirrorDepth_; vk::Buffer mirrorFrames_[vk::Device::kFramesInFlight]; VkDeviceAddress drawFrame_ = 0; bool mirrorValid_ = false; // the mirror target's been drawn (else its layout's undefined) // the frame data drawMeshes gives the shaders (0: this frame's) bool mirrors() const override { return true; } VkExtent2D targetExtent_{}; vk::Buffer drawBuffers_[vk::Device::kFramesInFlight], frameBuffers_[vk::Device::kFramesInFlight], jointBuffers_[vk::Device::kFramesInFlight], lightBuffers_[vk::Device::kFramesInFlight]; std::vector textures_; // index = slot std::vector models_; std::vector> retired_; // freed after kFramesInFlight more frames std::vector> retiredTextures_; uint64_t frameNumber_ = 0; struct UiTexture { uint64_t set; VkImageView view; }; std::map uiTextures_; // ImGui descriptor sets, made on first use (again if the picture's replaced) std::vector> retiredUiSets_; }; void VulkanBackend::createBindless() { VkDevice dev = device_.device(); VkSamplerCreateInfo sci{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; sci.magFilter = sci.minFilter = VK_FILTER_LINEAR; sci.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; sci.addressModeU = sci.addressModeV = sci.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; sci.anisotropyEnable = VK_TRUE; sci.maxAnisotropy = 16.0f; sci.maxLod = VK_LOD_CLAMP_NONE; VK_CHECK(vkCreateSampler(dev, &sci, nullptr, &sampler_)); VkSamplerCreateInfo shci{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; shci.magFilter = shci.minFilter = VK_FILTER_LINEAR; shci.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; shci.addressModeU = shci.addressModeV = shci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; shci.compareEnable = VK_TRUE; shci.compareOp = VK_COMPARE_OP_LESS_OR_EQUAL; VK_CHECK(vkCreateSampler(dev, &shci, nullptr, &shadowSampler_)); // Screen-space passes read render targets: clamped, so their edges never wrap round to the // opposite side (dark bands along the top and bottom of the screen). VkSamplerCreateInfo cci = sci; cci.addressModeU = cci.addressModeV = cci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; cci.anisotropyEnable = VK_FALSE; VK_CHECK(vkCreateSampler(dev, &cci, nullptr, &clampSampler_)); VkDescriptorSetLayoutBinding bindings[4] = { {0, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, kMaxTextures, kAllStages, nullptr}, {1, VK_DESCRIPTOR_TYPE_SAMPLER, 1, kAllStages, &sampler_}, {2, VK_DESCRIPTOR_TYPE_SAMPLER, 1, kAllStages, &shadowSampler_}, {3, VK_DESCRIPTOR_TYPE_SAMPLER, 1, kAllStages, &clampSampler_}, }; VkDescriptorBindingFlags flags[4] = {VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT, 0, 0, 0}; VkDescriptorSetLayoutBindingFlagsCreateInfo fci{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO}; fci.bindingCount = 4; fci.pBindingFlags = flags; VkDescriptorSetLayoutCreateInfo lci{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; lci.pNext = &fci; lci.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT; lci.bindingCount = 4; lci.pBindings = bindings; VK_CHECK(vkCreateDescriptorSetLayout(dev, &lci, nullptr, &setLayout_)); VkDescriptorPoolSize sizes[2] = {{VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, kMaxTextures}, {VK_DESCRIPTOR_TYPE_SAMPLER, 3}}; VkDescriptorPoolCreateInfo pci{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; pci.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT; pci.maxSets = 1; pci.poolSizeCount = 2; pci.pPoolSizes = sizes; VK_CHECK(vkCreateDescriptorPool(dev, &pci, nullptr, &pool_)); VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; ai.descriptorPool = pool_; ai.descriptorSetCount = 1; ai.pSetLayouts = &setLayout_; VK_CHECK(vkAllocateDescriptorSets(dev, &ai, &set_)); VkPushConstantRange range{kAllStages, 0, 128}; VkPipelineLayoutCreateInfo plci{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; plci.setLayoutCount = 1; plci.pSetLayouts = &setLayout_; plci.pushConstantRangeCount = 1; plci.pPushConstantRanges = ⦥ VK_CHECK(vkCreatePipelineLayout(dev, &plci, nullptr, &layout_)); // Shadow maps for the sun and for dynamic lights, sampled through the table. constexpr VkImageUsageFlags depthUsage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; shadowMap_ = makeTarget(kDepthFormat, {kShadowSize, kShadowSize}, depthUsage, VK_IMAGE_ASPECT_DEPTH_BIT); shadowAtlas_ = makeTarget(kDepthFormat, {kAtlasSize, kAtlasSize}, depthUsage, VK_IMAGE_ASPECT_DEPTH_BIT); bind(kSunShadowSlot, shadowMap_.view); bind(kAtlasSlot, shadowAtlas_.view); hzb_ = makeTarget(VK_FORMAT_R32_SFLOAT, {kHzbWidth, kHzbHeight}, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT); bind(kHzbSlot, hzb_.view); for (uint32_t l = 1; l < kHzbLevels; ++l) { // the pyramid above it hzbMips_[l] = makeTarget(VK_FORMAT_R32_SFLOAT, {std::max(1u, kHzbWidth >> l), std::max(1u, kHzbHeight >> l)}, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT); bind(kHzbSlot + l, hzbMips_[l].view); } for (auto& b : hzbReadback_) b = device_.createBuffer(kHzbWidth * kHzbHeight * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, true, true); for (uint32_t i = 0; i < vk::Device::kFramesInFlight; ++i) { drawBuffers_[i] = device_.createBuffer(kMaxDraws * sizeof(gpu::DrawData), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true); frameBuffers_[i] = device_.createBuffer(sizeof(gpu::FrameData), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true); jointBuffers_[i] = device_.createBuffer(kMaxJointsPerFrame * sizeof(glm::mat4), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true); lightBuffers_[i] = device_.createBuffer(kLightBufferSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true); cullBoxes_[i] = device_.createBuffer(kMaxCulled * sizeof(CullBox), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true); cullCmds_[i] = device_.createBuffer(kMaxCulled * sizeof(VkDrawIndexedIndirectCommand), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, false); cullCounter_[i] = device_.createBuffer(16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true, true); mirrorFrames_[i] = device_.createBuffer(sizeof(gpu::FrameData), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true); smoke_[i] = device_.createBuffer(size_t(kSmokeDims.x) * kSmokeDims.y * kSmokeDims.z * 2, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true); } for (auto& b : froxelCells_) b = device_.createBuffer(kFroxelCount * 16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, false); froxelOut_ = device_.createBuffer(kFroxelCount * 8, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, false); smokeSky_ = device_.createBuffer(160 * 90 * 8, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, false); if (device_.rayQuery()) { VkPhysicalDeviceAccelerationStructurePropertiesKHR asp{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_PROPERTIES_KHR}; VkPhysicalDeviceProperties2 props{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2}; props.pNext = &asp; vkGetPhysicalDeviceProperties2(device_.physicalDevice(), &props); scratchAlign_ = std::max(asp.minAccelerationStructureScratchOffsetAlignment, 1); for (auto& b : giProbes_) { b = device_.createBuffer(kGiProbes * kGiProbeSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, false); device_.immediateSubmit([&](VkCommandBuffer cmd) { vkCmdFillBuffer(cmd, b.buffer, 0, VK_WHOLE_SIZE, 0); }); } } } void VulkanBackend::createPipelines() { VkDevice dev = device_.device(); // The scene pass has two attachments (colour, G-buffer); only opaque meshes write the second. auto scene = [](PipelineDesc d, int gbuf) { d.gbuf = gbuf; return d; }; boxPipeline_ = makePipeline(dev, layout_, scene({kBoxVert, sizeof kBoxVert, kBoxFrag, sizeof kBoxFrag, kSceneFormat, true}, 1)); meshPipeline_ = makePipeline(dev, layout_, scene({kMeshVert, sizeof kMeshVert, kMeshFrag, sizeof kMeshFrag, kSceneFormat, true}, 2)); meshBlendPipeline_ = makePipeline(dev, layout_, scene({kMeshVert, sizeof kMeshVert, kMeshFrag, sizeof kMeshFrag, kSceneFormat, true, false, true}, 1)); skinnedPipeline_ = makePipeline(dev, layout_, scene({kSkinnedVert, sizeof kSkinnedVert, kMeshFrag, sizeof kMeshFrag, kSceneFormat, true}, 2)); skinnedBlendPipeline_ = makePipeline(dev, layout_, scene({kSkinnedVert, sizeof kSkinnedVert, kMeshFrag, sizeof kMeshFrag, kSceneFormat, true, false, true}, 1)); { PipelineDesc add{kFullscreenVert, sizeof kFullscreenVert, kVolumetricFrag, sizeof kVolumetricFrag, kSceneFormat, false}; volumetricAddPipeline_ = makePipeline(dev, layout_, [&] { auto d = add; d.additive = true; return d; }()); volumetricPipeline_ = makePipeline(dev, layout_, add); } { // particles: in the scene pass, depth-tested, not written; blended or added on PipelineDesc sp{kSpriteVert, sizeof kSpriteVert, kSpriteFrag, sizeof kSpriteFrag, kSceneFormat, true, false, true}; sp.gbuf = 1; spritePipeline_ = makePipeline(dev, layout_, sp); sp.blend = false, sp.glow = true; glowPipeline_ = makePipeline(dev, layout_, sp); } adaptPipeline_ = makePipeline(dev, layout_, {kFullscreenVert, sizeof kFullscreenVert, kAdaptFrag, sizeof kAdaptFrag, kSceneFormat, false}); ssrPipeline_ = makePipeline(dev, layout_, {kFullscreenVert, sizeof kFullscreenVert, kSsrFrag, sizeof kSsrFrag, kSceneFormat, false}); shadowMeshPipeline_ = makePipeline(dev, layout_, {kMeshVert, sizeof kMeshVert, nullptr, 0, VK_FORMAT_UNDEFINED, true, true}); // Compute pipelines (GPU culling, the fog grid, ray-traced light), on the same layout. All of // them are extras: one a driver can't build is left out (and what it does is off). (The // Galaxy S22's driver says it has ray queries but can't build gi.comp or froxel_rt.comp.) auto compute = [&](const char* name, const uint32_t* code, size_t size, VkPipeline& out) { VkShaderModuleCreateInfo ci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; ci.codeSize = size; ci.pCode = code; VkShaderModule m; VK_CHECK(vkCreateShaderModule(dev, &ci, nullptr, &m)); VkComputePipelineCreateInfo pi{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pi.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, m, "main", nullptr}; pi.layout = layout_; if (const VkResult res = vkCreateComputePipelines(dev, VK_NULL_HANDLE, 1, &pi, nullptr, &out); res != VK_SUCCESS) { log::warn("The graphics driver couldn't build the {} shader (VkResult {}): it's off", name, int(res)); out = VK_NULL_HANDLE; } vkDestroyShaderModule(dev, m, nullptr); }; compute("GPU culling", kCullComp, sizeof kCullComp, cullPipeline_); compute("fog grid", kFroxelComp, sizeof kFroxelComp, froxelPipeline_); if (device_.rayQuery()) compute("ray-traced light", kGiComp, sizeof kGiComp, giPipeline_), compute("ray-traced fog", kFroxelRtComp, sizeof kFroxelRtComp, froxelRtPipeline_); // Depth pre-pass, and the scene's opaque meshes after it. depthMeshPipeline_ = makePipeline(dev, layout_, {kMeshVert, sizeof kMeshVert, kDepthFrag, sizeof kDepthFrag, VK_FORMAT_UNDEFINED, true}); depthSkinnedPipeline_ = makePipeline(dev, layout_, {kSkinnedVert, sizeof kSkinnedVert, kDepthFrag, sizeof kDepthFrag, VK_FORMAT_UNDEFINED, true}); { PipelineDesc m = scene({kMeshVert, sizeof kMeshVert, kMeshFrag, sizeof kMeshFrag, kSceneFormat, true}, 2); m.prepassed = true; meshAfterDepthPipeline_ = makePipeline(dev, layout_, m); m.vs = kSkinnedVert, m.vsSize = sizeof kSkinnedVert; skinnedAfterDepthPipeline_ = makePipeline(dev, layout_, m); } shadowSkinnedPipeline_ = makePipeline(dev, layout_, {kSkinnedVert, sizeof kSkinnedVert, nullptr, 0, VK_FORMAT_UNDEFINED, true, true}); { PipelineDesc sky{kSkyVert, sizeof kSkyVert, kSkyFrag, sizeof kSkyFrag, kSceneFormat, true}; sky.sky = true; sky.gbuf = 1; skyPipeline_ = makePipeline(dev, layout_, sky); sky.fs = kSmokeSkyFrag, sky.fsSize = sizeof kSmokeSkyFrag, sky.over = true; smokeSkyPipeline_ = makePipeline(dev, layout_, sky); } ssaoPipeline_ = makePipeline(dev, layout_, {kFullscreenVert, sizeof kFullscreenVert, kSsaoFrag, sizeof kSsaoFrag, VK_FORMAT_R8_UNORM, false}); ssaoBlurPipeline_ = makePipeline(dev, layout_, {kFullscreenVert, sizeof kFullscreenVert, kSsaoBlurFrag, sizeof kSsaoBlurFrag, VK_FORMAT_R8_UNORM, false}); taaPipeline_ = makePipeline(dev, layout_, {kFullscreenVert, sizeof kFullscreenVert, kTaaFrag, sizeof kTaaFrag, kSceneFormat, false}); bloomDownPipeline_ = makePipeline(dev, layout_, {kFullscreenVert, sizeof kFullscreenVert, kBloomDownFrag, sizeof kBloomDownFrag, kSceneFormat, false}); { PipelineDesc up{kFullscreenVert, sizeof kFullscreenVert, kBloomUpFrag, sizeof kBloomUpFrag, kSceneFormat, false}; up.additive = true; bloomUpPipeline_ = makePipeline(dev, layout_, up); } hzbPipeline_ = makePipeline(dev, layout_, {kFullscreenVert, sizeof kFullscreenVert, kHzbFrag, sizeof kHzbFrag, VK_FORMAT_R32_SFLOAT, false}); tonemapPipeline_ = makePipeline(dev, layout_, {kFullscreenVert, sizeof kFullscreenVert, kTonemapFrag, sizeof kTonemapFrag, swapFormat_, false}); } bool VulkanBackend::initImGui(const Window& window) { IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGui::GetIO().IniFilename = nullptr; ImGui::StyleColorsDark(); ImGui_ImplVulkan_InitInfo ii{}; ii.ApiVersion = VK_API_VERSION_1_3; ii.Instance = device_.instance(); ii.PhysicalDevice = device_.physicalDevice(); ii.Device = device_.device(); ii.QueueFamily = device_.queueFamily(); ii.Queue = device_.queue(); ii.DescriptorPoolSize = 2048; // (one per texture the UI shows; out of them, the font gets none: a crash) ii.MinImageCount = 2; ii.ImageCount = device_.imageCount(); ii.UseDynamicRendering = true; ii.PipelineInfoMain.PipelineRenderingCreateInfo = {VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO}; ii.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount = 1; ii.PipelineInfoMain.PipelineRenderingCreateInfo.pColorAttachmentFormats = &swapFormat_; imgui_ = ImGui_ImplSDL3_InitForVulkan(window.handle()) && ImGui_ImplVulkan_Init(&ii); return imgui_; } void VulkanBackend::ensureTargets(VkExtent2D extent) { if (scene_.image && targetExtent_.width == extent.width && targetExtent_.height == extent.height) return; device_.waitIdle(); destroyTexture(scene_); destroyTexture(depth_); scene_ = makeTarget(kSceneFormat, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT); depth_ = makeTarget(kDepthFormat, extent, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_DEPTH_BIT); targetExtent_ = extent; bind(kSceneSlot, scene_.view); bind(kDepthSlot, depth_.view); for (Texture* t : {&mirror_, &mirrorGbuf_, &mirrorDepth_}) destroyTexture(*t); mirror_ = makeTarget(kSceneFormat, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT); mirrorGbuf_ = makeTarget(kGbufFormat, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, VK_IMAGE_ASPECT_COLOR_BIT); mirrorDepth_ = makeTarget(kDepthFormat, extent, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_IMAGE_ASPECT_DEPTH_BIT); bind(kMirrorSlot, mirror_.view); mirrorValid_ = false; // Occlusion at half size; bloom from half down to 1/64. destroyTexture(ao_); destroyTexture(aoBlur_); for (Texture& t : bloom_) destroyTexture(t); for (Texture& t : taa_) destroyTexture(t); constexpr VkImageUsageFlags target = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; aoExtent_ = {std::max(1u, extent.width / 2), std::max(1u, extent.height / 2)}; ao_ = makeTarget(VK_FORMAT_R8_UNORM, aoExtent_, target, VK_IMAGE_ASPECT_COLOR_BIT); aoBlur_ = makeTarget(VK_FORMAT_R8_UNORM, aoExtent_, target, VK_IMAGE_ASPECT_COLOR_BIT); bind(kAoSlot, ao_.view); bind(kAoBlurSlot, aoBlur_.view); destroyTexture(gbuf_); destroyTexture(ssr_); gbuf_ = makeTarget(kGbufFormat, extent, target, VK_IMAGE_ASPECT_COLOR_BIT); ssr_ = makeTarget(kSceneFormat, aoExtent_, target, VK_IMAGE_ASPECT_COLOR_BIT); bind(kGbufSlot, gbuf_.view); bind(kSsrSlot, ssr_.view); for (uint32_t i = 0; i < 2; ++i) { destroyTexture(adapt_[i]); adapt_[i] = makeTarget(kSceneFormat, {1, 1}, target, VK_IMAGE_ASPECT_COLOR_BIT); adaptValid_ = false; bind(kAdaptSlot + i, adapt_[i].view); } for (uint32_t i = 0; i < 2; ++i) { taa_[i] = makeTarget(kSceneFormat, extent, target, VK_IMAGE_ASPECT_COLOR_BIT); bind(kTaaSlot + i, taa_[i].view); } for (uint32_t i = 0; i < kBloomLevels; ++i) { bloomExtent_[i] = {std::max(1u, extent.width >> (i + 1)), std::max(1u, extent.height >> (i + 1))}; bloom_[i] = makeTarget(kSceneFormat, bloomExtent_[i], target, VK_IMAGE_ASPECT_COLOR_BIT); bind(kBloomSlot + i, bloom_[i].view); } } void VulkanBackend::drawMeshes(VkCommandBuffer cmd, const std::vector& draws, const glm::mat4& viewProj, bool shadow, VkPipeline mesh, VkPipeline skinned, const vk::Buffer* indirect, bool skipLiquid) { const uint32_t fi = device_.frameIndex(); VkPipeline bound = VK_NULL_HANDLE; uint32_t boundModel = UINT32_MAX; ModelPush mp{}; mp.viewProj = viewProj; mp.frame = drawFrame_ ? drawFrame_ : frameBuffers_[fi].address; for (const MeshDraw& d : draws) { const Model& m = models_[d.model]; if (!m.indices.buffer || (skipLiquid && d.liquid)) continue; const bool isSkinned = d.firstJoint != UINT32_MAX; VkPipeline want = isSkinned ? skinned : mesh; if (want != bound) { vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, want); bound = want; } if (d.model != boundModel) { vkCmdBindIndexBuffer(cmd, m.indices.buffer, 0, VK_INDEX_TYPE_UINT32); boundModel = d.model; } mp.vertices = m.vertices.address; mp.skin = isSkinned ? m.skin.address : 0; mp.joints = isSkinned ? jointBuffers_[fi].address + d.firstJoint * sizeof(glm::mat4) : 0; mp.draw = drawBuffers_[fi].address + d.draw * sizeof(gpu::DrawData); mp.textures = d.textures; mp.tint = d.tint; mp.material = shadow ? 0 : d.material; vkCmdPushConstants(cmd, layout_, kAllStages, 0, sizeof mp, &mp); if (indirect) // GPU-culled: its command says whether it's drawn vkCmdDrawIndexedIndirect(cmd, indirect->buffer, (&d - draws.data()) * sizeof(VkDrawIndexedIndirectCommand), 1, sizeof(VkDrawIndexedIndirectCommand)); else vkCmdDrawIndexed(cmd, d.indexCount, 1, d.firstIndex, 0, 0); } } void VulkanBackend::shadowPasses(VkCommandBuffer cmd, Texture& target, uint32_t size, const std::vector& passes) { vk::transitionImage(cmd, target.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); VkRenderingAttachmentInfo att{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}; att.imageView = target.view; att.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; att.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; att.storeOp = VK_ATTACHMENT_STORE_OP_STORE; att.clearValue.depthStencil = {1.0f, 0}; VkRenderingInfo ri{VK_STRUCTURE_TYPE_RENDERING_INFO}; ri.renderArea = {{0, 0}, {size, size}}; ri.layerCount = 1; ri.pDepthAttachment = &att; vkCmdBeginRendering(cmd, &ri); for (const ShadowPass& pass : passes) { const VkViewport v{float(pass.rect.x), float(pass.rect.y), float(pass.rect.z), float(pass.rect.w), 0, 1}; const VkRect2D s{{pass.rect.x, pass.rect.y}, {uint32_t(pass.rect.z), uint32_t(pass.rect.w)}}; vkCmdSetViewport(cmd, 0, 1, &v); vkCmdSetScissor(cmd, 0, 1, &s); drawMeshes(cmd, *pass.draws, pass.viewProj, true, shadowMeshPipeline_, shadowSkinnedPipeline_); } vkCmdEndRendering(cmd); vk::transitionImage(cmd, target.image, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); } void VulkanBackend::render(FramePacket& p) { VkCommandBuffer cmd = cmd_; ensureTargets({p.width, p.height}); // the scene's size (render scale), not the window's const uint32_t fi = device_.frameIndex(); // the frame fence guarantees the GPU is done with these std::memcpy(drawBuffers_[fi].mapped, p.draws.data(), p.draws.size() * sizeof(gpu::DrawData)); std::memcpy(jointBuffers_[fi].mapped, p.joints.data(), p.joints.size() * sizeof(glm::mat4)); auto* lights = static_cast(lightBuffers_[fi].mapped); std::memcpy(lights + kLightsOffset, p.lights.data(), p.lights.size() * sizeof(gpu::Light)); std::memcpy(lights + kViewsOffset, p.views.data(), p.views.size() * sizeof(gpu::ShadowView)); std::memcpy(lights + kCellsOffset, p.clusters->cells.data(), p.clusters->cells.size() * sizeof(glm::uvec2)); std::memcpy(lights + kIndicesOffset, p.clusters->indices.data(), p.clusters->indices.size() * sizeof(uint32_t)); const VkDeviceAddress lb = lightBuffers_[fi].address; p.frame.lights = lb + kLightsOffset; p.frame.cells = lb + kCellsOffset; p.frame.lightIndices = lb + kIndicesOffset; p.frame.shadowViews = lb + kViewsOffset; p.frame.froxels = froxelOut_.address; // Real-time GI: on while there are dynamic lights (and a little after, while their light fades). giIdle_ = p.lights.empty() ? giIdle_ + 1 : 0; giOn_ = p.frame.gi.x > 0.5f && giPipeline_ && !p.rayInstances.empty() && giIdle_ < 120; if (giOn_) { const float spacing = p.frame.gi.y; const glm::vec3 cell = glm::floor(glm::vec3(p.frame.camPos) / spacing) - glm::vec3(12, 12, 6); p.frame.giOrigin = glm::vec4(cell, 0.0f); p.frame.giProbes = giProbes_[giFrame_ & 1u].address; // this frame's (written before the scene reads it) } else { p.frame.gi.x = 0.0f; giFresh_ = true; } std::memcpy(frameBuffers_[fi].mapped, &p.frame, sizeof p.frame); beginMarks(cmd); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout_, 0, 1, &set_, 0, nullptr); // ── GPU occlusion culling: the opaque draws' commands, before anything draws them ── cullFor_ = nullptr; hzbReady_ = hzbNext_, hzbNext_ = false; // last frame made the grid: it's there to test against if (p.gpuCull && !p.opaque.empty() && p.opaque.size() <= kMaxCulled) { gpuCulled_ = *static_cast(cullCounter_[fi].mapped); // this slot's last count (its fence has passed) *static_cast(cullCounter_[fi].mapped) = 0; auto* boxes = static_cast(cullBoxes_[fi].mapped); for (size_t i = 0; i < p.opaque.size(); ++i) { const MeshDraw& d = p.opaque[i]; boxes[i] = {glm::vec4(d.lo, 0), glm::vec4(d.hi, 0), glm::uvec4(d.indexCount, d.firstIndex, d.lo != d.hi ? 1u : 0u, 0)}; } vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, layout_, 0, 1, &set_, 0, nullptr); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, cullPipeline_); const CullPush cp{p.cullViewProj, cullBoxes_[fi].address, cullCmds_[fi].address, cullCounter_[fi].address, uint32_t(p.opaque.size()), kHzbSlot, p.cullOcclusion && hzbReady_ ? 1u : 0u, 0}; vkCmdPushConstants(cmd, layout_, kAllStages, 0, sizeof cp, &cp); vkCmdDispatch(cmd, (uint32_t(p.opaque.size()) + 63) / 64, 1, 1); VkMemoryBarrier2 mb{VK_STRUCTURE_TYPE_MEMORY_BARRIER_2}; mb.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; mb.srcAccessMask = VK_ACCESS_2_SHADER_WRITE_BIT; mb.dstStageMask = VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_2_HOST_BIT; mb.dstAccessMask = VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_2_HOST_READ_BIT; VkDependencyInfo dep{VK_STRUCTURE_TYPE_DEPENDENCY_INFO}; dep.memoryBarrierCount = 1; dep.pMemoryBarriers = &mb; vkCmdPipelineBarrier2(cmd, &dep); cullFor_ = &cullCmds_[fi]; mark(cmd, "gpu culling"); } if (p.shadowSize != shadowMapSize_) { // r_shadowres changed device_.waitIdle(); destroyTexture(shadowMap_); shadowMap_ = makeTarget(kDepthFormat, {p.shadowSize, p.shadowSize}, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_DEPTH_BIT); bind(kSunShadowSlot, shadowMap_.view); shadowMapSize_ = p.shadowSize; } shadowPasses(cmd, shadowMap_, p.shadowSize, p.sunPasses); // always: clears it when there's no sun if (!p.atlasPasses.empty()) shadowPasses(cmd, shadowAtlas_, kAtlasSize, p.atlasPasses); mark(cmd, "shadows"); // ── Real-time GI: this frame's scene for rays, then every probe traces it (the fog grid // traces it too: what sky each cell sees) ── const bool fogRays = p.frame.froxel.x > 0.5f && froxelRtPipeline_ && !p.rayInstances.empty(); VkDeviceAddress fogTlas = 0; if (giOn_ || fogRays) { Tlas& t = tlas_[fi]; const uint32_t n = uint32_t(std::min(p.rayInstances.size(), 1u << 20)); ensureTlas(t, n); auto* inst = static_cast(t.instances.mapped); auto* info = static_cast(t.info.mapped); uint32_t used = 0; for (uint32_t i = 0; i < n; ++i) { const RayInstance& r = p.rayInstances[i]; if (r.model >= models_.size()) continue; const Model& m = models_[r.model]; const Blas* b = nullptr; for (auto& [first, blas] : m.blas) if (first == r.firstIndex) b = &blas; if (!b) continue; VkAccelerationStructureInstanceKHR& o = inst[used]; for (int row = 0; row < 3; ++row) for (int col = 0; col < 4; ++col) o.transform.matrix[row][col] = r.transform[col][row]; o.instanceCustomIndex = used; o.mask = 0xFF; o.instanceShaderBindingTableRecordOffset = 0; o.flags = VK_GEOMETRY_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT_KHR | VK_GEOMETRY_INSTANCE_FORCE_OPAQUE_BIT_KHR; o.accelerationStructureReference = b->address; info[used] = {m.vertices.address, m.indices.address, r.firstIndex, r.textures, r.tint, 0}; ++used; } VkAccelerationStructureGeometryKHR g{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR}; g.geometryType = VK_GEOMETRY_TYPE_INSTANCES_KHR; g.geometry.instances.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_INSTANCES_DATA_KHR; g.geometry.instances.data.deviceAddress = t.instances.address; VkAccelerationStructureBuildGeometryInfoKHR bi{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR}; bi.type = VK_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL_KHR; bi.flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_BUILD_BIT_KHR; bi.mode = VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR; bi.geometryCount = 1; bi.pGeometries = &g; bi.dstAccelerationStructure = t.as; bi.scratchData.deviceAddress = alignUp(t.scratch.address, scratchAlign_); const VkAccelerationStructureBuildRangeInfoKHR range{used, 0, 0, 0}; const VkAccelerationStructureBuildRangeInfoKHR* ranges = ⦥ vkCmdBuildAccelerationStructuresKHR(cmd, 1, &bi, &ranges); VkMemoryBarrier2 mb{VK_STRUCTURE_TYPE_MEMORY_BARRIER_2}; mb.srcStageMask = VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; mb.srcAccessMask = VK_ACCESS_2_ACCELERATION_STRUCTURE_WRITE_BIT_KHR | VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT; mb.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; mb.dstAccessMask = VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR | VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT; VkDependencyInfo dep{VK_STRUCTURE_TYPE_DEPENDENCY_INFO}; dep.memoryBarrierCount = 1; dep.pMemoryBarriers = &mb; vkCmdPipelineBarrier2(cmd, &dep); if (fogRays) fogTlas = t.address; if (giOn_) { // A different turn of the ray pattern each frame (random unit quaternion), so the probes // see all round them over a few frames. uint32_t h = giFrame_ * 747796405u + 2891336453u; auto rnd = [&] { h = h * 747796405u + 2891336453u; uint32_t w = ((h >> ((h >> 28u) + 4u)) ^ h) * 277803737u; return float((w >> 22u) ^ w) / 4294967296.0f; }; const float u1 = rnd(), u2 = rnd() * 6.2831853f, u3 = rnd() * 6.2831853f; const glm::vec4 rot(std::sqrt(1 - u1) * std::sin(u2), std::sqrt(1 - u1) * std::cos(u2), std::sqrt(u1) * std::sin(u3), std::sqrt(u1) * std::cos(u3)); const uint32_t cur = giFrame_ & 1u; const GiPush gp{frameBuffers_[fi].address, giProbes_[cur].address, giProbes_[cur ^ 1u].address, t.info.address, t.address, giFresh_ ? 1u : 0u, 0.9f, rot}; vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, layout_, 0, 1, &set_, 0, nullptr); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, giPipeline_); vkCmdPushConstants(cmd, layout_, kAllStages, 0, sizeof gp, &gp); vkCmdDispatch(cmd, kGiProbes, 1, 1); mb.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; mb.srcAccessMask = VK_ACCESS_2_SHADER_WRITE_BIT; mb.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT; mb.dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT; vkCmdPipelineBarrier2(cmd, &dep); giFresh_ = false; ++giFrame_; mark(cmd, "gi probes"); } } // ── The fog grid: lit and shadowed cells, then each column added up (the scene reads it) ── if (p.frame.froxel.x > 0.5f) { auto barrier = [&](VkPipelineStageFlags2 src, VkAccessFlags2 srcAccess, VkPipelineStageFlags2 dst, VkAccessFlags2 dstAccess) { VkMemoryBarrier2 mb{VK_STRUCTURE_TYPE_MEMORY_BARRIER_2}; mb.srcStageMask = src, mb.srcAccessMask = srcAccess, mb.dstStageMask = dst, mb.dstAccessMask = dstAccess; VkDependencyInfo dep{VK_STRUCTURE_TYPE_DEPENDENCY_INFO}; dep.memoryBarrierCount = 1; dep.pMemoryBarriers = &mb; vkCmdPipelineBarrier2(cmd, &dep); }; // (Last frame's reads of the grid, and of the cells, are done before they're written.) barrier(VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT); const uint32_t cur = froxelFrame_ & 1u; // A jitter along the slice that walks the whole of it over a few frames (golden ratio). const float jitter = std::fmod(0.5f + float(froxelFrame_) * 0.618034f, 1.0f); FroxelPush fp{frameBuffers_[fi].address, froxelCells_[cur].address, froxelCells_[cur ^ 1u].address, froxelOut_.address, p.post.proj, froxelPrevPos_, glm::vec4(glm::vec3(froxelPrevFwd_), jitter), 0, froxelFrame_, fogTlas, 0, 0, p.smokeOrigin}; if (!p.smoke.empty()) { std::memcpy(smoke_[fi].mapped, p.smoke.data(), std::min(p.smoke.size(), size_t(kSmokeDims.x) * kSmokeDims.y * kSmokeDims.z) * 2); fp.smoke = smoke_[fi].address; fp.smokeSky = smokeSky_.address; } else fp.smokeOrigin.w = -1.0f; smokeSkyOn_ = !p.smoke.empty(); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, layout_, 0, 1, &set_, 0, nullptr); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, fogTlas ? froxelRtPipeline_ : froxelPipeline_); vkCmdPushConstants(cmd, layout_, kAllStages, 0, sizeof fp, &fp); vkCmdDispatch(cmd, 160 / 8, (90 + 7) / 8, 64); barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT); fp.mode = 1; vkCmdPushConstants(cmd, layout_, kAllStages, 0, sizeof fp, &fp); vkCmdDispatch(cmd, 160 / 8, (90 + 7) / 8, 1); barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT); froxelPrevPos_ = glm::vec4(glm::vec3(p.frame.camPos), 1.0f); froxelPrevFwd_ = glm::vec4(glm::vec3(p.frame.camFwd), 0.0f); ++froxelFrame_; mark(cmd, "fog grid"); } else { froxelPrevPos_.w = 0.0f; // (nothing to blend with when it comes back on) } // ── Mirror: the scene from the reflected eye, cut at the mirror (no pre-pass, no post) ── if (p.mirror) { gpu::FrameData mf = p.mirrorFrame; mf.lights = p.frame.lights, mf.cells = p.frame.cells, mf.lightIndices = p.frame.lightIndices, mf.shadowViews = p.frame.shadowViews; mf.froxels = p.frame.froxels, mf.giProbes = p.frame.giProbes; mf.gi = p.frame.gi, mf.giOrigin = p.frame.giOrigin; // as decided here (GI may be off this frame: no probes to read) std::memcpy(mirrorFrames_[fi].mapped, &mf, sizeof mf); const VkExtent2D me{p.width, p.height}; vk::transitionImage(cmd, mirror_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); vk::transitionImage(cmd, mirrorGbuf_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); vk::transitionImage(cmd, mirrorDepth_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); VkRenderingAttachmentInfo mc[2] = {{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}, {VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}}; mc[0].imageView = mirror_.view; mc[0].imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; mc[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; mc[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; mc[0].clearValue.color = {{p.clearColor.r, p.clearColor.g, p.clearColor.b, 1.0f}}; mc[1] = mc[0]; mc[1].imageView = mirrorGbuf_.view; mc[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; VkRenderingAttachmentInfo md{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}; md.imageView = mirrorDepth_.view; md.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; md.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; md.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; md.clearValue.depthStencil = {1.0f, 0}; VkRenderingInfo mri{VK_STRUCTURE_TYPE_RENDERING_INFO}; const VkRect2D ms{{p.mirrorRect.x, p.mirrorRect.y}, {uint32_t(p.mirrorRect.z), uint32_t(p.mirrorRect.w)}}; // only where the mirror is // The whole target (a smaller render area left it unwritten on NVIDIA); the scissor // keeps the drawing to the mirror's rectangle. mri.renderArea = {{0, 0}, me}; mri.layerCount = 1; mri.colorAttachmentCount = 2; mri.pColorAttachments = mc; mri.pDepthAttachment = &md; vkCmdBeginRendering(cmd, &mri); const VkViewport mv{0, 0, float(me.width), float(me.height), 0, 1}; vkCmdSetViewport(cmd, 0, 1, &mv); vkCmdSetScissor(cmd, 0, 1, &ms); drawFrame_ = mirrorFrames_[fi].address; drawMeshes(cmd, p.mirrorOpaque, p.mirrorViewProj, false, meshPipeline_, skinnedPipeline_); if (p.frame.sky.x > 0.5f) { vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skyPipeline_); const gpu::SkyPush sp{p.mirrorInvViewProj, p.frame.sunDir, p.frame.sunColor, p.frame.sky, glm::vec4(p.skyCloud, p.skyPhotoSunZ, p.frame.weather.z, p.frame.weather.w)}; vkCmdPushConstants(cmd, layout_, kAllStages, 0, sizeof sp, &sp); vkCmdDraw(cmd, 3, 1, 0, 0); } drawMeshes(cmd, p.mirrorTranslucent, p.mirrorViewProj, false, meshBlendPipeline_, skinnedBlendPipeline_); drawFrame_ = 0; vkCmdEndRendering(cmd); vk::transitionImage(cmd, mirror_.image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); mirrorValid_ = true; mark(cmd, "mirror"); } else if (!mirrorValid_ && mirror_.image) { // (bound in the table: keep it in a readable layout) vk::transitionImage(cmd, mirror_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); mirrorValid_ = true; } // ── Scene pass (HDR) ── const VkExtent2D extent{p.width, p.height}, outExtent{p.outWidth, p.outHeight}; const VkViewport outViewport{0, 0, float(p.outWidth), float(p.outHeight), 0, 1}; const VkRect2D outScissor{{0, 0}, outExtent}; const VkViewport viewport{0, 0, float(p.width), float(p.height), 0, 1}; const VkRect2D scissor{{0, 0}, extent}; vk::transitionImage(cmd, scene_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); vk::transitionImage(cmd, gbuf_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); vk::transitionImage(cmd, depth_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); VkRenderingAttachmentInfo colors[2] = {{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}, {VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}}; VkRenderingAttachmentInfo& color = colors[0]; color.imageView = scene_.view; color.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color.clearValue.color = {{p.clearColor.r, p.clearColor.g, p.clearColor.b, 1.0f}}; colors[1] = color; colors[1].imageView = gbuf_.view; colors[1].clearValue.color = {{0.5f, 0.5f, 1.0f, 0.0f}}; // nothing to reflect VkRenderingAttachmentInfo depth{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}; depth.imageView = depth_.view; depth.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; depth.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; depth.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; depth.clearValue.depthStencil = {1.0f, 0}; if (p.prepass) { // depth first (no liquids: they're cut at their surface), then the scene on top of it VkRenderingAttachmentInfo pd = depth; pd.storeOp = VK_ATTACHMENT_STORE_OP_STORE; VkRenderingInfo pri{VK_STRUCTURE_TYPE_RENDERING_INFO}; pri.renderArea = scissor; pri.layerCount = 1; pri.pDepthAttachment = &pd; vkCmdBeginRendering(cmd, &pri); vkCmdSetViewport(cmd, 0, 1, &viewport); vkCmdSetScissor(cmd, 0, 1, &scissor); drawMeshes(cmd, p.opaque, p.viewProj, false, depthMeshPipeline_, depthSkinnedPipeline_, cullFor_, true); vkCmdEndRendering(cmd); mark(cmd, "depth pre-pass"); depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; } VkRenderingInfo ri{VK_STRUCTURE_TYPE_RENDERING_INFO}; ri.renderArea = scissor; ri.layerCount = 1; ri.colorAttachmentCount = 2; ri.pColorAttachments = colors; ri.pDepthAttachment = &depth; vkCmdBeginRendering(cmd, &ri); vkCmdSetViewport(cmd, 0, 1, &viewport); vkCmdSetScissor(cmd, 0, 1, &scissor); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, boxPipeline_); for (const gpu::Box& b : p.boxes) { const BoxPush bp{p.viewProj, b}; vkCmdPushConstants(cmd, layout_, kAllStages, 0, sizeof bp, &bp); vkCmdDraw(cmd, 36, 1, 0, 0); } drawMeshes(cmd, p.opaque, p.viewProj, false, p.prepass ? meshAfterDepthPipeline_ : meshPipeline_, p.prepass ? skinnedAfterDepthPipeline_ : skinnedPipeline_, cullFor_); if (p.frame.sky.x > 0.5f) { // where nothing was drawn vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skyPipeline_); const gpu::SkyPush sp{p.invViewProj, p.frame.sunDir, p.frame.sunColor, p.frame.sky, glm::vec4(p.skyCloud, p.skyPhotoSunZ, p.frame.weather.z, p.frame.weather.w)}; vkCmdPushConstants(cmd, layout_, kAllStages, 0, sizeof sp, &sp); vkCmdDraw(cmd, 3, 1, 0, 0); } if (smokeSkyOn_ && p.frame.froxel.x > 0.5f) { // smoke in front of the sky vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, smokeSkyPipeline_); const SmokeSkyPush sp{frameBuffers_[device_.frameIndex()].address, smokeSky_.address}; vkCmdPushConstants(cmd, layout_, kAllStages, 0, sizeof sp, &sp); vkCmdDraw(cmd, 3, 1, 0, 0); } drawMeshes(cmd, p.translucent, p.viewProj, false, meshBlendPipeline_, skinnedBlendPipeline_); // back to front, blended for (auto [list, pipe] : {std::pair{&p.sprites, spritePipeline_}, std::pair{&p.glowSprites, glowPipeline_}}) { if (list->empty()) continue; vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe); for (const gpu::Sprite& sp : *list) { const SpritePush push{p.viewProj, sp}; vkCmdPushConstants(cmd, layout_, kAllStages, 0, sizeof push, &push); vkCmdDraw(cmd, 6, 1, 0, 0); } } vkCmdEndRendering(cmd); vk::transitionImage(cmd, gbuf_.image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); vk::transitionImage(cmd, depth_.image, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); vk::transitionImage(cmd, scene_.image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); mark(cmd, "scene"); // ── Occlusion grid: the depth (only opaque things write it) shrunk and copied for the CPU ── if (p.hzbTag) { const uint32_t fi = device_.frameIndex(); vk::transitionImage(cmd, hzb_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); VkRenderingAttachmentInfo grid{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}; grid.imageView = hzb_.view; grid.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; grid.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; grid.storeOp = VK_ATTACHMENT_STORE_OP_STORE; const VkRect2D area{{0, 0}, {kHzbWidth, kHzbHeight}}; VkRenderingInfo gi{VK_STRUCTURE_TYPE_RENDERING_INFO}; gi.renderArea = area; gi.layerCount = 1; gi.colorAttachmentCount = 1; gi.pColorAttachments = &grid; vkCmdBeginRendering(cmd, &gi); const VkViewport gv{0, 0, float(kHzbWidth), float(kHzbHeight), 0, 1}; vkCmdSetViewport(cmd, 0, 1, &gv); vkCmdSetScissor(cmd, 0, 1, &area); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, hzbPipeline_); const HzbPush hp{kDepthSlot, extent.width, extent.height, kHzbWidth, kHzbHeight}; vkCmdPushConstants(cmd, layout_, kAllStages, 0, sizeof hp, &hp); vkCmdDraw(cmd, 3, 1, 0, 0); vkCmdEndRendering(cmd); vk::transitionImage(cmd, hzb_.image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); VkBufferImageCopy region{}; region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; region.imageExtent = {kHzbWidth, kHzbHeight, 1}; vkCmdCopyImageToBuffer(cmd, hzb_.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, hzbReadback_[fi].buffer, 1, ®ion); vk::transitionImage(cmd, hzb_.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); // next frame's culling reads it // The pyramid: each level the farthest depth of the four (or so) under it. for (uint32_t l = 1; l < kHzbLevels; ++l) { const uint32_t sw = std::max(1u, kHzbWidth >> (l - 1)), sh = std::max(1u, kHzbHeight >> (l - 1)); const uint32_t dw = std::max(1u, kHzbWidth >> l), dh = std::max(1u, kHzbHeight >> l); vk::transitionImage(cmd, hzbMips_[l].image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); VkRenderingAttachmentInfo lv{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}; lv.imageView = hzbMips_[l].view; lv.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; lv.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; lv.storeOp = VK_ATTACHMENT_STORE_OP_STORE; const VkRect2D la{{0, 0}, {dw, dh}}; VkRenderingInfo li{VK_STRUCTURE_TYPE_RENDERING_INFO}; li.renderArea = la; li.layerCount = 1; li.colorAttachmentCount = 1; li.pColorAttachments = &lv; vkCmdBeginRendering(cmd, &li); const VkViewport lvp{0, 0, float(dw), float(dh), 0, 1}; vkCmdSetViewport(cmd, 0, 1, &lvp); vkCmdSetScissor(cmd, 0, 1, &la); const HzbPush lp{kHzbSlot + l - 1, sw, sh, dw, dh}; vkCmdPushConstants(cmd, layout_, kAllStages, 0, sizeof lp, &lp); vkCmdDraw(cmd, 3, 1, 0, 0); vkCmdEndRendering(cmd); vk::transitionImage(cmd, hzbMips_[l].image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); } hzbNext_ = true; VkMemoryBarrier2 toHost{VK_STRUCTURE_TYPE_MEMORY_BARRIER_2}; toHost.srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT; toHost.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; toHost.dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT; toHost.dstAccessMask = VK_ACCESS_2_HOST_READ_BIT; VkDependencyInfo dep{VK_STRUCTURE_TYPE_DEPENDENCY_INFO}; dep.memoryBarrierCount = 1; dep.pMemoryBarriers = &toHost; vkCmdPipelineBarrier2(cmd, &dep); hzbTags_[fi] = p.hzbTag; vkCmdSetViewport(cmd, 0, 1, &viewport); // back to the full screen for the tonemap vkCmdSetScissor(cmd, 0, 1, &scissor); } mark(cmd, "occlusion grid"); // ── Post: ambient occlusion, bloom ── // One full-screen pass into `target` (left ready to sample); load: blend onto what's there. auto pass = [&](Texture& t, VkExtent2D ext, VkFormat, VkPipeline pipe, const void* push, uint32_t size, bool load) { vk::transitionImage(cmd, t.image, load ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); VkRenderingAttachmentInfo att{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}; att.imageView = t.view; att.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; att.loadOp = load ? VK_ATTACHMENT_LOAD_OP_LOAD : VK_ATTACHMENT_LOAD_OP_DONT_CARE; att.storeOp = VK_ATTACHMENT_STORE_OP_STORE; const VkRect2D area{{0, 0}, ext}; VkRenderingInfo ri3{VK_STRUCTURE_TYPE_RENDERING_INFO}; ri3.renderArea = area; ri3.layerCount = 1; ri3.colorAttachmentCount = 1; ri3.pColorAttachments = &att; vkCmdBeginRendering(cmd, &ri3); const VkViewport vp{0, 0, float(ext.width), float(ext.height), 0, 1}; vkCmdSetViewport(cmd, 0, 1, &vp); vkCmdSetScissor(cmd, 0, 1, &area); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe); vkCmdPushConstants(cmd, layout_, kAllStages, 0, size, push); vkCmdDraw(cmd, 3, 1, 0, 0); vkCmdEndRendering(cmd); vk::transitionImage(cmd, t.image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); }; const bool ao = p.post.aoStrength > 0.0f, bloom = p.post.bloomStrength > 0.0f, reflections = p.post.ssr > 0.0f, volumetric = p.frame.volumetric.x > 0.0f && p.frame.froxel.x < 0.5f, // the sun and the dynamic lights (else in the fog grid) ssr = reflections || volumetric; // (the half-size target both go to) if (reflections) { // reflections from this frame's scene, added by TAA (or the tonemap) const SsrPush sp{kDepthSlot, kGbufSlot, kSceneSlot, extent.width, extent.height, aoExtent_.width, aoExtent_.height, p.post.ssr, p.post.proj}; pass(ssr_, aoExtent_, kSceneFormat, ssrPipeline_, &sp, sizeof sp, false); } if (volumetric) { // sunlight in the air, onto the same target const VolumetricPush vp{frameBuffers_[fi].address, kDepthSlot, extent.width, extent.height, aoExtent_.width, aoExtent_.height, {}, p.post.proj}; pass(ssr_, aoExtent_, kSceneFormat, reflections ? volumetricAddPipeline_ : volumetricPipeline_, &vp, sizeof vp, reflections); } if (ssr) mark(cmd, "reflections + volumetric"); // TAA first: what follows (bloom, tonemap) uses the resolved image. TextureId resolved = kSceneSlot; if (p.post.taa) { taaCur_ ^= 1u; const TaaPush tp{p.post.reproject, kSceneSlot, kTaaSlot + (taaCur_ ^ 1u), kDepthSlot, extent.width, extent.height, p.post.taaReset ? 1u : 0u, ssr ? kSsrSlot : 0u, 0}; pass(taa_[taaCur_], extent, kSceneFormat, taaPipeline_, &tp, sizeof tp, false); resolved = kTaaSlot + taaCur_; mark(cmd, "taa"); } if (ao) { const SsaoPush sp{kDepthSlot, extent.width, extent.height, aoExtent_.width, aoExtent_.height, p.post.aoRadius, p.post.aoStrength, 0, p.post.proj}; pass(ao_, aoExtent_, VK_FORMAT_R8_UNORM, ssaoPipeline_, &sp, sizeof sp, false); const SsaoBlurPush bp{kAoSlot, kDepthSlot, extent.width, extent.height, aoExtent_.width, aoExtent_.height, 0, 0, p.post.proj}; pass(aoBlur_, aoExtent_, VK_FORMAT_R8_UNORM, ssaoBlurPipeline_, &bp, sizeof bp, false); mark(cmd, "ssao"); } if (bloom) { for (uint32_t i = 0; i < kBloomLevels; ++i) { const VkExtent2D src = i == 0 ? extent : bloomExtent_[i - 1]; const BloomPush bp{i == 0 ? resolved : kBloomSlot + i - 1, src.width, src.height, bloomExtent_[i].width, bloomExtent_[i].height, i == 0 ? 1u : 0u}; pass(bloom_[i], bloomExtent_[i], kSceneFormat, bloomDownPipeline_, &bp, sizeof bp, false); } const float radius = 1.0f; uint32_t radiusBits; std::memcpy(&radiusBits, &radius, 4); for (uint32_t i = kBloomLevels - 1; i > 0; --i) { const BloomPush bp{kBloomSlot + i, bloomExtent_[i].width, bloomExtent_[i].height, bloomExtent_[i - 1].width, bloomExtent_[i - 1].height, radiusBits}; pass(bloom_[i - 1], bloomExtent_[i - 1], kSceneFormat, bloomUpPipeline_, &bp, sizeof bp, true); } } if (bloom) mark(cmd, "bloom"); if (p.post.adapt) { // eye adaptation from the smallest bloom level adaptCur_ ^= 1u; const AdaptPush ap{kBloomSlot + kBloomLevels - 1, p.post.adaptReset || !adaptValid_ ? 0u : kAdaptSlot + (adaptCur_ ^ 1u), p.post.adaptDt, 1.5f, p.post.adaptKey, 0.6f, 1.8f, 0}; pass(adapt_[adaptCur_], {1, 1}, kSceneFormat, adaptPipeline_, &ap, sizeof ap, false); adaptValid_ = true; } // ── Tonemap to the swapchain, then UI ── VkRenderingAttachmentInfo swap{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}; swap.imageView = device_.currentImageView(); swap.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; swap.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; swap.storeOp = VK_ATTACHMENT_STORE_OP_STORE; VkRenderingInfo ri2{VK_STRUCTURE_TYPE_RENDERING_INFO}; ri2.renderArea = outScissor; ri2.layerCount = 1; ri2.colorAttachmentCount = 1; ri2.pColorAttachments = &swap; vkCmdBeginRendering(cmd, &ri2); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, tonemapPipeline_); vkCmdSetViewport(cmd, 0, 1, &outViewport); vkCmdSetScissor(cmd, 0, 1, &outScissor); const TonemapPush tp{resolved, p.exposure, ao ? kAoBlurSlot : 0u, bloom ? kBloomSlot : 0u, p.post.bloomStrength, p.post.saturation, p.post.vignette, p.post.taa ? p.post.sharpen : 0.0f, kDepthSlot, {ssr && !p.post.taa ? kSsrSlot : 0u, p.post.adapt ? kAdaptSlot + adaptCur_ : 0u, 0}, p.post.shafts, p.post.sunColor}; vkCmdPushConstants(cmd, layout_, kAllStages, 0, sizeof tp, &tp); vkCmdDraw(cmd, 3, 1, 0, 0); ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), cmd); vkCmdEndRendering(cmd); mark(cmd, "tonemap + ui"); vk::Buffer shot; if (!p.screenshot.empty()) { shot = device_.createBuffer(VkDeviceSize(outExtent.width) * outExtent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, true, true); vk::transitionImage(cmd, device_.currentImage(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); VkBufferImageCopy region{}; region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; region.imageExtent = {outExtent.width, outExtent.height, 1}; vkCmdCopyImageToBuffer(cmd, device_.currentImage(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, shot.buffer, 1, ®ion); vk::transitionImage(cmd, device_.currentImage(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); } device_.endFrame(); if (shot.buffer) { device_.waitIdle(); vmaInvalidateAllocation(device_.allocator(), shot.alloc, 0, VK_WHOLE_SIZE); const size_t count = size_t(outExtent.width) * outExtent.height; std::vector px(static_cast(shot.mapped), static_cast(shot.mapped) + count * 4); device_.destroyBuffer(shot); if (swapFormat_ == VK_FORMAT_B8G8R8A8_UNORM || swapFormat_ == VK_FORMAT_B8G8R8A8_SRGB) for (size_t i = 0; i < count; ++i) std::swap(px[i * 4], px[i * 4 + 2]); saveScreenshot(p.screenshot, std::move(px), outExtent.width, outExtent.height); } } } // namespace std::unique_ptr createVulkanBackend() { return std::make_unique(); } } // namespace keel