载入中...
搜索中...
未找到
GraphicsGpuDriven.cpp
浏览该文件的文档.
1// Vulkan backend implementation — GPU-driven rendering (stages 0-3).
2//
3// Bindless resources, GPU tables, per-frame arena, CPU/GPU indirect draws,
4// HZB + frustum cull chain, visibility-buffer pass, fullscreen resolve and
5// VirtualGeometry hardware-raster integration. Kept in its own TU (matching
6// dev's split-graphics-backend layout) so the feature stays reviewable.
7
12#include "graphics/Light.h"
13#include "graphics/Material.h"
15#include "graphics/Shadow.h"
16
17#include <SDL2/SDL.h>
18
19#include <algorithm>
20#include <array>
21#include <cmath>
22#include <cstdio>
23#include <cstdlib>
24#include <cstring>
25#include <functional>
26#include <stdexcept>
27#include <string>
28#include <vector>
29
30#include "common/Exception.h"
32#include "zeroerr/assert.h"
33
34#include <glm/gtc/matrix_transform.hpp>
35
36#include "graphics/shaders/gpu_emit_comp_spv.inc"
37#include "graphics/shaders/vg_main_cull_comp_spv.inc"
38#include "graphics/shaders/hzb_build_comp_spv.inc"
39#include "graphics/shaders/gpu_cull_comp_spv.inc"
40#include "graphics/shaders/mesh3d_gpudriven_vert_spv.inc"
41#include "graphics/shaders/mesh3d_gpudriven_frag_spv.inc"
42#include "graphics/shaders/mesh3d_gbuffer_vis_vert_spv.inc"
43#include "graphics/shaders/mesh3d_gbuffer_vis_frag_spv.inc"
44#include "graphics/shaders/mesh3d_gbuffer_vgvis_vert_spv.inc"
45#include "graphics/shaders/mesh3d_gbuffer_vgvis_frag_spv.inc"
46#include "graphics/shaders/resolve_vis_vert_spv.inc"
47#include "graphics/shaders/resolve_vis_frag_spv.inc"
48
49namespace eve::graphics::vulkan {
50
52 return gpuDrivenEnabled_ && gpuDrivenCaps_.gpuDrivenCullAvailable() &&
53 renderControl_ && renderControl_->isEnabled("visResolve") &&
54 sceneColorSamples == vk::SampleCountFlagBits::e1;
55}
56
57vk::DescriptorSet Graphics::bindlessSetForFrame() const {
58 if (bindlessSets_.empty()) return nullptr;
59 return bindlessSets_[currentFrameSlot() % bindlessSets_.size()];
60}
61
62void Graphics::initGpuDrivenResources() {
63 if (!gpuDrivenCaps_.gpuDrivenAvailable()) return;
64 frameArenas_.resize(frameSlotCount());
65 for (auto &arena : frameArenas_) {
66 arena.ensure(device, 8u << 20,
67 vk::BufferUsageFlagBits::eStorageBuffer |
68 vk::BufferUsageFlagBits::eIndirectBuffer |
69 vk::BufferUsageFlagBits::eTransferDst);
70 }
71 createBindlessSet();
72 createMesh3DGpuDrivenPipeline();
73}
74
75void Graphics::createGpuDrivenVisResources(int width, int height) {
76 if (!gpuDrivenCaps_.gpuDrivenAvailable()) return;
77 if (gbufferVisRenderPass || gbufferSlots.empty()) return;
78 const uint32_t w = uint32_t(width);
79 const uint32_t h = uint32_t(height);
80 const vk::Format colorFmt = pickGBufferColorFormat(device);
81 const vk::Format depthFmt = vk::Format::eD32Sfloat;
82 const vk::Format visIDFmt = vk::Format::eR32G32Uint;
83 const vk::Format visBaryFmt = vk::Format::eR16G16Sfloat;
84
85 gbufferVisRenderPass =
86 device.createRenderPass()
87 .addSampledColorAttachment(colorFmt)
88 .addSampledColorAttachment(colorFmt)
89 .addSampledColorAttachment(colorFmt)
90 .addSampledColorAttachment(visIDFmt)
91 .addSampledColorAttachment(visBaryFmt)
92 .addSampledDepthAttachment(depthFmt)
93 .addSubpass(vkb::SubpassBuilder()
94 .addAttachmentRef(0, vk::ImageLayout::eColorAttachmentOptimal)
95 .addAttachmentRef(1, vk::ImageLayout::eColorAttachmentOptimal)
96 .addAttachmentRef(2, vk::ImageLayout::eColorAttachmentOptimal)
97 .addAttachmentRef(3, vk::ImageLayout::eColorAttachmentOptimal)
98 .addAttachmentRef(4, vk::ImageLayout::eColorAttachmentOptimal)
99 .setDepthStencilAttachment(
100 5, vk::ImageLayout::eDepthStencilAttachmentOptimal))
101 .addExternalShaderReadDependencies()
102 .build();
103 for (auto &slot : gbufferSlots) {
104 slot.visFramebuffer = gbufferVisRenderPass.createFramebuffer(
105 device, w, h,
106 {slot.normal.asAttachment(), slot.depthColor.asAttachment(), slot.albedo.asAttachment(),
107 slot.visID.asAttachment(), slot.visBary.asAttachment(), slot.depth.asAttachment()});
108 }
109
110 if (!mesh3dGpuDrivenPipelineLayout) return;
111 // Instance vis pass: no vertex input (fetches from the pooled buffers via
112 // gl_VertexIndex); draws non-indexed with the cull chain's NI commands.
113 {
114 std::vector<uint32_t> visVert(mesh3d_gbuffer_vis_vert_spv,
115 mesh3d_gbuffer_vis_vert_spv +
116 mesh3d_gbuffer_vis_vert_spv_count);
117 std::vector<uint32_t> visFrag(mesh3d_gbuffer_vis_frag_spv,
118 mesh3d_gbuffer_vis_frag_spv +
119 mesh3d_gbuffer_vis_frag_spv_count);
120 vk::ShaderModule visVertModule =
121 vkb::PipelineBuilder::createShaderModule(device.instance, visVert);
122 vk::ShaderModule visFragModule =
123 vkb::PipelineBuilder::createShaderModule(device.instance, visFrag);
124 gbufferVisPipeline =
125 device.createPipeline()
126 .useClassicPipeline(visVertModule, visFragModule)
127 .setPipelineLayout(mesh3dGpuDrivenPipelineLayout)
128 .setDynamicStatesViewportScissor()
129 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f,
130 vk::CullModeFlagBits::eNone, vk::FrontFace::eClockwise)
131 .setDepthStencil(true, true, vk::CompareOp::eLess)
132 .setColorAttachmentCount(5)
133 .build(gbufferVisRenderPass);
134 device->destroyShaderModule(visVertModule);
135 device->destroyShaderModule(visFragModule);
136 }
137 // VG variant: same vis attachments, cluster-stream fetch (no vertex input).
138 {
139 std::vector<uint32_t> vgVisVert(mesh3d_gbuffer_vgvis_vert_spv,
140 mesh3d_gbuffer_vgvis_vert_spv +
141 mesh3d_gbuffer_vgvis_vert_spv_count);
142 std::vector<uint32_t> vgVisFrag(mesh3d_gbuffer_vgvis_frag_spv,
143 mesh3d_gbuffer_vgvis_frag_spv +
144 mesh3d_gbuffer_vgvis_frag_spv_count);
145 vk::ShaderModule vgVisVertModule =
146 vkb::PipelineBuilder::createShaderModule(device.instance, vgVisVert);
147 vk::ShaderModule vgVisFragModule =
148 vkb::PipelineBuilder::createShaderModule(device.instance, vgVisFrag);
149 gbufferVgVisPipeline =
150 device.createPipeline()
151 .useClassicPipeline(vgVisVertModule, vgVisFragModule)
152 .setPipelineLayout(mesh3dGpuDrivenPipelineLayout)
153 .setDynamicStatesViewportScissor()
154 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f,
155 vk::CullModeFlagBits::eNone, vk::FrontFace::eClockwise)
156 .setDepthStencil(true, true, vk::CompareOp::eLess)
157 .setColorAttachmentCount(5)
158 .build(gbufferVisRenderPass);
159 device->destroyShaderModule(vgVisVertModule);
160 device->destroyShaderModule(vgVisFragModule);
161 }
162}
163
164// ---- GPU-driven (stage 0): bindless set + per-frame arena + tables ----
165
167 if (frameArenas_.empty()) {
168 static FrameArena fallback; // never used for recording; guards odd call order
169 return fallback;
170 }
171 return frameArenas_[currentFrameSlot() % frameArenas_.size()];
172}
173
174void Graphics::createBindlessSet() {
175 if (!gpuDrivenCaps_.gpuDrivenAvailable()) return;
176 if (!whiteTexture || !whiteTexture->gpuHandle) return;
177 if (!defaultBindlessCube || !defaultBindlessCube->gpuHandle) return;
178 auto *white = static_cast<GpuTexture *>(whiteTexture->gpuHandle);
179 auto *whiteCube = static_cast<GpuTexture *>(defaultBindlessCube->gpuHandle);
180
181 auto samplerBinding = [](uint32_t binding, uint32_t count, vk::ShaderStageFlags stages) {
182 vk::DescriptorSetLayoutBinding b{};
183 b.binding = binding;
184 b.descriptorType = vk::DescriptorType::eCombinedImageSampler;
185 b.descriptorCount = count;
186 b.stageFlags = stages;
187 return b;
188 };
189 auto storageBinding = [](uint32_t binding, vk::ShaderStageFlags stages) {
190 vk::DescriptorSetLayoutBinding b{};
191 b.binding = binding;
192 b.descriptorType = vk::DescriptorType::eStorageBuffer;
193 b.descriptorCount = 1;
194 b.stageFlags = stages;
195 return b;
196 };
197 auto uniformBinding = [](uint32_t binding, vk::ShaderStageFlags stages) {
198 vk::DescriptorSetLayoutBinding b{};
199 b.binding = binding;
200 b.descriptorType = vk::DescriptorType::eUniformBuffer;
201 b.descriptorCount = 1;
202 b.stageFlags = stages;
203 return b;
204 };
205 const vk::ShaderStageFlags allStages = vk::ShaderStageFlagBits::eVertex |
206 vk::ShaderStageFlagBits::eFragment |
207 vk::ShaderStageFlagBits::eCompute;
208 const vk::ShaderStageFlags computeOnly = vk::ShaderStageFlagBits::eCompute;
209 const vk::ShaderStageFlags vertFrag =
210 vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment;
211 std::vector<vk::DescriptorSetLayoutBinding> bindings{
212 samplerBinding(0, kMaxBindlessTextures, allStages),
213 samplerBinding(1, kMaxBindlessCubemaps, allStages),
214 storageBinding(2, allStages),
215 storageBinding(3, allStages),
216 storageBinding(4, allStages),
217 storageBinding(5, allStages), // unused by shaders; kept valid
218 uniformBinding(6, computeOnly),
219 storageBinding(7, computeOnly),
220 storageBinding(8, computeOnly),
221 storageBinding(9, computeOnly),
222 storageBinding(10, computeOnly),
223 samplerBinding(11, 1, computeOnly),
224 storageBinding(12, computeOnly),
225 storageBinding(13, computeOnly),
226 storageBinding(14, computeOnly),
227 samplerBinding(15, 1, vk::ShaderStageFlagBits::eFragment), // visID (resolve)
228 samplerBinding(16, 1, vk::ShaderStageFlagBits::eFragment), // visBary (resolve)
229 storageBinding(17, computeOnly),
230 storageBinding(18, vertFrag), // pooled vertex positions
231 storageBinding(19, vertFrag), // pooled vertex normals
232 storageBinding(20, vertFrag), // pooled vertex uvs
233 storageBinding(21, vertFrag), // pooled indices
234 storageBinding(22, allStages), // VG position stream (float xyz)
235 storageBinding(23, allStages), // VG triangle stream (u32)
236 storageBinding(24, allStages), // VG cluster table (uvec4 x 4)
237 storageBinding(25, allStages), // VG cluster -> asset id
238 storageBinding(26, allStages), // VG visible list (per slot)
239 storageBinding(27, allStages), // VG indirect commands (per slot)
240 storageBinding(28, vk::ShaderStageFlagBits::eFragment), // VG asset -> material
241 storageBinding(29, vk::ShaderStageFlagBits::eVertex |
242 vk::ShaderStageFlagBits::eFragment |
243 vk::ShaderStageFlagBits::eCompute), // VG asset models
244 storageBinding(30, computeOnly), // non-indexed indirect commands (vis pass)
245 };
246
247 vk::DescriptorSetLayoutCreateInfo layoutInfo{};
248 layoutInfo.bindingCount = uint32_t(bindings.size());
249 layoutInfo.pBindings = bindings.data();
250 bindlessSetLayoutUnique_ = device->createDescriptorSetLayoutUnique(layoutInfo);
251 bindlessSetLayout_ = *bindlessSetLayoutUnique_;
252
253 // kAsyncResourceCopies sets: one per frame-in-flight slot, so frame N+1 can
254 // rewrite its own set while frame N's pending command buffers still hold
255 // the previous contents of the other set (no UPDATE_AFTER_BIND required).
256 const uint32_t setCount = kAsyncResourceCopies;
257 std::array<vk::DescriptorPoolSize, 3> poolSizes{
258 vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler,
259 (kMaxBindlessTextures + kMaxBindlessCubemaps + 3) * setCount},
260 vk::DescriptorPoolSize{vk::DescriptorType::eStorageBuffer, 25 * setCount},
261 vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 1 * setCount},
262 };
263 vk::DescriptorPoolCreateInfo poolInfo{};
264 poolInfo.maxSets = setCount;
265 poolInfo.poolSizeCount = uint32_t(poolSizes.size());
266 poolInfo.pPoolSizes = poolSizes.data();
267 bindlessPool_ = device->createDescriptorPool(poolInfo);
268
269 {
270 std::vector<vk::DescriptorSetLayout> layouts(setCount, bindlessSetLayout_);
271 vk::DescriptorSetAllocateInfo alloc{};
272 alloc.descriptorPool = bindlessPool_;
273 alloc.descriptorSetCount = setCount;
274 alloc.pSetLayouts = layouts.data();
275 bindlessSets_ = device->allocateDescriptorSets(alloc);
276 }
277
278 bindlessTextures2D_.assign(kMaxBindlessTextures, white);
279 bindlessCubemaps_.assign(kMaxBindlessCubemaps, whiteCube);
280 bindlessFree2D_.clear();
281 bindlessFreeCube_.clear();
282 bindlessFree2D_.reserve(kMaxBindlessTextures);
283 bindlessFreeCube_.reserve(kMaxBindlessCubemaps);
284 for (uint32_t i = 0; i < kMaxBindlessTextures; ++i) bindlessFree2D_.push_back(i);
285 for (uint32_t i = 0; i < kMaxBindlessCubemaps; ++i) bindlessFreeCube_.push_back(i);
286
287 vk::DescriptorImageInfo white2D{white->sampler, white->imageView(),
288 vk::ImageLayout::eShaderReadOnlyOptimal};
289 vk::DescriptorImageInfo whiteCubeInfo{whiteCube->sampler, whiteCube->cubeImage.imageView(),
290 vk::ImageLayout::eShaderReadOnlyOptimal};
291 // Placeholder fill: single-element writes keep the per-call update tiny;
292 // ~1.1k single-element calls per set at init cost ~6 ms, which is fine.
293 constexpr uint32_t kDescriptorChunk = 1;
294 auto chunkFill = [&](vk::DescriptorSet set, uint32_t binding, uint32_t total,
295 const vk::DescriptorImageInfo &info) {
296 for (uint32_t base = 0; base < total; base += kDescriptorChunk) {
297 vk::WriteDescriptorSet w{};
298 w.dstSet = set;
299 w.dstBinding = binding;
300 w.dstArrayElement = base;
301 w.descriptorCount = std::min(kDescriptorChunk, total - base);
302 w.descriptorType = vk::DescriptorType::eCombinedImageSampler;
303 w.pImageInfo = &info;
304 device->updateDescriptorSets(1, &w, 0, nullptr);
305 }
306 };
307 for (vk::DescriptorSet set : bindlessSets_) {
308 chunkFill(set, 0, kMaxBindlessTextures, white2D);
309 chunkFill(set, 1, kMaxBindlessCubemaps, whiteCubeInfo);
310 }
311
312 // GPU resource tables (fixed capacity at startup; entries registered lazily).
313 constexpr uint32_t kMaxMeshRecords = 4096;
314 constexpr uint32_t kMaxMaterialRecords = 1024;
315 meshTableBuffer_ = vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer,
316 kMaxMeshRecords * sizeof(GpuMeshRecord),
317 kHostVisibleCoherent);
318 meshTableCapacity_ = kMaxMeshRecords;
319 meshTableRecords_.reserve(kMaxMeshRecords);
320 materialTableBuffer_ = vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer,
321 kMaxMaterialRecords * sizeof(GpuMaterialRecord),
322 kHostVisibleCoherent);
323 materialTableCapacity_ = kMaxMaterialRecords;
324 materialTableRecords_.reserve(kMaxMaterialRecords);
325
326 // Bind the resource tables into the bindless set (bindings 2-5). Binding 4
327 // (per-frame instances) is rewritten before each frame; these writes make
328 // every binding valid so the set is safe to bind at any time.
329 auto tableWrite = [&](vk::DescriptorSet set, uint32_t binding, vk::Buffer buffer) {
330 vk::DescriptorBufferInfo info{buffer, 0, VK_WHOLE_SIZE};
331 vk::WriteDescriptorSet w{};
332 w.dstSet = set;
333 w.dstBinding = binding;
334 w.descriptorCount = 1;
335 w.descriptorType = vk::DescriptorType::eStorageBuffer;
336 w.pBufferInfo = &info;
337 device->updateDescriptorSets(1, &w, 0, nullptr);
338 };
339 for (vk::DescriptorSet set : bindlessSets_) {
340 tableWrite(set, 2, meshTableBuffer_.buffer);
341 tableWrite(set, 3, materialTableBuffer_.buffer);
342 tableWrite(set, 4, meshTableBuffer_.buffer); // placeholder; rewritten per frame
343 tableWrite(set, 5, meshTableBuffer_.buffer); // unused by shaders
344 }
345 // Stage 2 placeholders: every binding must be valid before the set binds.
346 gpuDrivenCullParamsPlaceholder_ =
347 vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eUniformBuffer, 256,
348 kHostVisibleCoherent);
349 {
350 vk::DescriptorBufferInfo ubo{gpuDrivenCullParamsPlaceholder_.buffer, 0, 256};
351 for (vk::DescriptorSet set : bindlessSets_) {
352 vk::WriteDescriptorSet w{};
353 w.dstSet = set;
354 w.dstBinding = 6;
355 w.descriptorCount = 1;
356 w.descriptorType = vk::DescriptorType::eUniformBuffer;
357 w.pBufferInfo = &ubo;
358 device->updateDescriptorSets(1, &w, 0, nullptr);
359 }
360 }
361 for (uint32_t b : {7u, 8u, 9u, 10u, 12u, 13u, 14u, 17u})
362 for (vk::DescriptorSet set : bindlessSets_) tableWrite(set, b, meshTableBuffer_.buffer);
363 {
364 vk::DescriptorImageInfo depthInfo{white->sampler, white->imageView(),
365 vk::ImageLayout::eShaderReadOnlyOptimal};
366 for (vk::DescriptorSet set : bindlessSets_) {
367 vk::WriteDescriptorSet w{};
368 w.dstSet = set;
369 w.dstBinding = 11;
370 w.descriptorCount = 1;
371 w.descriptorType = vk::DescriptorType::eCombinedImageSampler;
372 w.pImageInfo = &depthInfo;
373 device->updateDescriptorSets(1, &w, 0, nullptr);
374 }
375 }
376 // Stage 3 placeholders: visID/visBary are rewritten per frame by the
377 // resolve; keep them valid so the set is safe to bind anywhere.
378 for (vk::DescriptorSet set : bindlessSets_) {
379 vk::DescriptorImageInfo visInfo{white->sampler, white->imageView(),
380 vk::ImageLayout::eShaderReadOnlyOptimal};
381 vk::WriteDescriptorSet w{};
382 w.dstSet = set;
383 w.dstBinding = 15;
384 w.descriptorCount = 1;
385 w.descriptorType = vk::DescriptorType::eCombinedImageSampler;
386 w.pImageInfo = &visInfo;
387 device->updateDescriptorSets(1, &w, 0, nullptr);
388 w.dstBinding = 16;
389 device->updateDescriptorSets(1, &w, 0, nullptr);
390 }
391 // Pooled vertex/index buffers for the vis resolve (grows lazily).
392 ensureGpuVertexPool();
393 bindGpuVertexPoolBindless();
394 // Shared virtual-geometry cluster pool (grows lazily on upload).
395 ensureVgBuffers();
396 bindVgPoolBindless();
397}
398
399void Graphics::ensureGpuVertexPool() {
400 if (gpuVertexPool_.positions.buffer) return;
401 constexpr uint32_t kInitialVertices = 256u << 10; // 256k verts (~11 MB total)
402 constexpr uint32_t kInitialIndices = 768u << 10; // 768k u32 indices (3 MB)
403 const auto hostMem = kHostVisibleCoherent;
404 gpuVertexPool_.positions = vkb::GenericBuffer(
405 device, vk::BufferUsageFlagBits::eStorageBuffer, kInitialVertices * sizeof(glm::vec4),
406 hostMem);
407 gpuVertexPool_.normals = vkb::GenericBuffer(
408 device, vk::BufferUsageFlagBits::eStorageBuffer, kInitialVertices * sizeof(glm::vec4),
409 hostMem);
410 gpuVertexPool_.uvs = vkb::GenericBuffer(
411 device, vk::BufferUsageFlagBits::eStorageBuffer, kInitialVertices * sizeof(glm::vec2),
412 hostMem);
413 gpuVertexPool_.indices = vkb::GenericBuffer(
414 device, vk::BufferUsageFlagBits::eStorageBuffer, kInitialIndices * sizeof(uint32_t),
415 hostMem);
416 gpuVertexPool_.vertexCount = 0;
417 gpuVertexPool_.indexCount = 0;
418}
419
420void Graphics::growGpuVertexPool(uint32_t needVertices, uint32_t needIndices) {
421 // Pool growth reallocates the buffers; a pending frame may still be reading
422 // them, so drain the GPU first (rare path: first-time mesh registration).
423 device->waitIdle();
424 const uint32_t oldVerts = gpuVertexPool_.vertexCount;
425 const uint32_t oldInds = gpuVertexPool_.indexCount;
426 const auto hostMem = kHostVisibleCoherent;
427 auto copyInto = [&](vkb::GenericBuffer &dst, vkb::GenericBuffer &src, vk::DeviceSize oldBytes,
428 vk::DeviceSize newBytes) {
429 vkb::GenericBuffer grown(device, vk::BufferUsageFlagBits::eStorageBuffer, newBytes,
430 hostMem);
431 if (oldBytes > 0) {
432 void *srcMap = src.map();
433 void *dstMap = grown.map();
434 std::memcpy(dstMap, srcMap, oldBytes);
435 grown.unmap();
436 src.unmap();
437 }
438 src.release();
439 dst = std::move(grown);
440 };
441 const uint32_t newVerts = std::max(needVertices, oldVerts * 2u);
442 const uint32_t newInds = std::max(needIndices, oldInds * 2u);
443 copyInto(gpuVertexPool_.positions, gpuVertexPool_.positions,
444 vk::DeviceSize(oldVerts) * sizeof(glm::vec4),
445 vk::DeviceSize(newVerts) * sizeof(glm::vec4));
446 copyInto(gpuVertexPool_.normals, gpuVertexPool_.normals,
447 vk::DeviceSize(oldVerts) * sizeof(glm::vec4),
448 vk::DeviceSize(newVerts) * sizeof(glm::vec4));
449 copyInto(gpuVertexPool_.uvs, gpuVertexPool_.uvs,
450 vk::DeviceSize(oldVerts) * sizeof(glm::vec2),
451 vk::DeviceSize(newVerts) * sizeof(glm::vec2));
452 copyInto(gpuVertexPool_.indices, gpuVertexPool_.indices,
453 vk::DeviceSize(oldInds) * sizeof(uint32_t),
454 vk::DeviceSize(newInds) * sizeof(uint32_t));
455 bindGpuVertexPoolBindless();
456}
457
458void Graphics::bindGpuVertexPoolBindless() {
459 if (bindlessSets_.empty() || !gpuVertexPool_.positions.buffer) return;
460 auto bufWrite = [&](vk::DescriptorSet set, uint32_t binding, vk::Buffer buffer,
461 vk::DeviceSize size) {
462 vk::DescriptorBufferInfo info{buffer, 0, size};
463 vk::WriteDescriptorSet w{};
464 w.dstSet = set;
465 w.dstBinding = binding;
466 w.descriptorCount = 1;
467 w.descriptorType = vk::DescriptorType::eStorageBuffer;
468 w.pBufferInfo = &info;
469 device->updateDescriptorSets(1, &w, 0, nullptr);
470 };
471 const uint32_t cap = [&]() {
472 const vk::DeviceSize bytes = gpuVertexPool_.positions.size;
473 return uint32_t(bytes / sizeof(glm::vec4));
474 }();
475 const uint32_t indCap = uint32_t(gpuVertexPool_.indices.size / sizeof(uint32_t));
476 for (vk::DescriptorSet set : bindlessSets_) {
477 bufWrite(set, 18, gpuVertexPool_.positions.buffer,
478 vk::DeviceSize(cap) * sizeof(glm::vec4));
479 bufWrite(set, 19, gpuVertexPool_.normals.buffer, vk::DeviceSize(cap) * sizeof(glm::vec4));
480 bufWrite(set, 20, gpuVertexPool_.uvs.buffer, vk::DeviceSize(cap) * sizeof(glm::vec2));
481 bufWrite(set, 21, gpuVertexPool_.indices.buffer,
482 vk::DeviceSize(indCap) * sizeof(uint32_t));
483 }
484}
485
486void Graphics::appendGpuMeshToPool(GpuMesh &gpu) {
487 if (!gpu.vertices.buffer || !gpu.indices.buffer) return;
488 ensureGpuVertexPool();
489 const uint32_t nVerts = gpu.record.vertexCount;
490 const uint32_t nInds = gpu.record.indexCount;
491 if (nVerts == 0 || nInds == 0) return;
492 const uint32_t newVerts = gpuVertexPool_.vertexCount + nVerts;
493 const uint32_t newInds = gpuVertexPool_.indexCount + nInds;
494 const uint32_t capVerts =
495 uint32_t(gpuVertexPool_.positions.size / sizeof(glm::vec4));
496 const uint32_t capInds = uint32_t(gpuVertexPool_.indices.size / sizeof(uint32_t));
497 if (newVerts > capVerts || newInds > capInds) {
498 growGpuVertexPool(newVerts, newInds);
499 }
500
501 void *vMap = gpu.vertices.map();
502 void *iMap = gpu.indices.map();
503 if (!vMap || !iMap) return;
504 auto *verts = static_cast<const MeshVertex *>(vMap);
505 auto *posDst = static_cast<glm::vec4 *>(gpuVertexPool_.positions.map());
506 auto *nrmDst = static_cast<glm::vec4 *>(gpuVertexPool_.normals.map());
507 auto *uvDst = static_cast<glm::vec2 *>(gpuVertexPool_.uvs.map());
508 auto *idxDst = static_cast<uint32_t *>(gpuVertexPool_.indices.map());
509 if (!posDst || !nrmDst || !uvDst || !idxDst) {
510 gpu.vertices.unmap();
511 gpu.indices.unmap();
512 return;
513 }
514 posDst += gpuVertexPool_.vertexCount;
515 nrmDst += gpuVertexPool_.vertexCount;
516 uvDst += gpuVertexPool_.vertexCount;
517 idxDst += gpuVertexPool_.indexCount;
518 for (uint32_t i = 0; i < nVerts; ++i) {
519 posDst[i] = glm::vec4(verts[i].pos, 0.f);
520 nrmDst[i] = glm::vec4(verts[i].normal, 0.f);
521 uvDst[i] = verts[i].uv;
522 }
523 if (gpu.indexType == vk::IndexType::eUint16) {
524 const auto *src16 = static_cast<const uint16_t *>(iMap);
525 for (uint32_t i = 0; i < nInds; ++i) idxDst[i] = uint32_t(src16[i]);
526 } else {
527 const auto *src32 = static_cast<const uint32_t *>(iMap);
528 for (uint32_t i = 0; i < nInds; ++i) idxDst[i] = src32[i];
529 }
530 const uint32_t firstIdx = static_cast<const uint32_t *>(iMap)[0];
531 const glm::vec3 firstPos = verts[0].pos;
532 gpuVertexPool_.positions.unmap();
533 gpuVertexPool_.normals.unmap();
534 gpuVertexPool_.uvs.unmap();
535 gpuVertexPool_.indices.unmap();
536 gpu.vertices.unmap();
537 gpu.indices.unmap();
538
539 // Pool offsets are vertex/index counts, resolved by the vis shaders.
540 gpu.record.vertexOffset = gpuVertexPool_.vertexCount;
541 gpu.record.indexOffset = gpuVertexPool_.indexCount;
542 gpu.record.firstIndex = 0; // vis pass draws non-indexed from the pool
543 gpu.record.vertexBase = 0;
544 gpuVertexPool_.vertexCount = newVerts;
545 gpuVertexPool_.indexCount = newInds;
546}
547
548uint32_t Graphics::registerBindlessTexture2D(GpuTexture *tex) {
549 if (!tex || bindlessSets_.empty()) return kInvalidBindlessSlot;
550 if (tex->bindlessIndex2D != kInvalidBindlessSlot) return tex->bindlessIndex2D;
551 if (bindlessFree2D_.empty()) return kInvalidBindlessSlot;
552 const uint32_t slot = bindlessFree2D_.front();
553 bindlessFree2D_.erase(bindlessFree2D_.begin());
554 bindlessTextures2D_[slot] = tex;
555 tex->bindlessIndex2D = slot;
556 vk::DescriptorImageInfo img{tex->sampler, tex->imageView(),
557 vk::ImageLayout::eShaderReadOnlyOptimal};
558 for (vk::DescriptorSet set : bindlessSets_) {
559 vk::WriteDescriptorSet write{};
560 write.dstSet = set;
561 write.dstBinding = 0;
562 write.dstArrayElement = slot;
563 write.descriptorCount = 1;
564 write.descriptorType = vk::DescriptorType::eCombinedImageSampler;
565 write.pImageInfo = &img;
566 device->updateDescriptorSets(1, &write, 0, nullptr);
567 }
568 return slot;
569}
570
571uint32_t Graphics::registerBindlessTextureCube(GpuTexture *tex) {
572 if (!tex || bindlessSets_.empty()) return kInvalidBindlessSlot;
573 if (tex->bindlessIndexCube != kInvalidBindlessSlot) return tex->bindlessIndexCube;
574 if (bindlessFreeCube_.empty()) return kInvalidBindlessSlot;
575 const uint32_t slot = bindlessFreeCube_.front();
576 bindlessFreeCube_.erase(bindlessFreeCube_.begin());
577 bindlessCubemaps_[slot] = tex;
578 tex->bindlessIndexCube = slot;
579 vk::DescriptorImageInfo img{tex->sampler, tex->cubeImage.imageView(),
580 vk::ImageLayout::eShaderReadOnlyOptimal};
581 for (vk::DescriptorSet set : bindlessSets_) {
582 vk::WriteDescriptorSet write{};
583 write.dstSet = set;
584 write.dstBinding = 1;
585 write.dstArrayElement = slot;
586 write.descriptorCount = 1;
587 write.descriptorType = vk::DescriptorType::eCombinedImageSampler;
588 write.pImageInfo = &img;
589 device->updateDescriptorSets(1, &write, 0, nullptr);
590 }
591 return slot;
592}
593
594void Graphics::unregisterBindlessTexture(GpuTexture *tex) {
595 if (!tex || bindlessSets_.empty()) return;
596 auto *white = static_cast<GpuTexture *>(whiteTexture->gpuHandle);
597 GpuTexture *whiteCube = white;
598 if (defaultBindlessCube && defaultBindlessCube->gpuHandle)
599 whiteCube = static_cast<GpuTexture *>(defaultBindlessCube->gpuHandle);
600
601 auto restore = [&](uint32_t binding, uint32_t slot, GpuTexture *placeholder,
602 bool cube) {
603 vk::DescriptorImageInfo img{
604 placeholder->sampler,
605 cube ? placeholder->cubeImage.imageView() : placeholder->imageView(),
606 vk::ImageLayout::eShaderReadOnlyOptimal};
607 for (vk::DescriptorSet set : bindlessSets_) {
608 vk::WriteDescriptorSet write{};
609 write.dstSet = set;
610 write.dstBinding = binding;
611 write.dstArrayElement = slot;
612 write.descriptorCount = 1;
613 write.descriptorType = vk::DescriptorType::eCombinedImageSampler;
614 write.pImageInfo = &img;
615 device->updateDescriptorSets(1, &write, 0, nullptr);
616 }
617 };
618
619 if (tex->bindlessIndex2D != kInvalidBindlessSlot) {
620 const uint32_t slot = tex->bindlessIndex2D;
621 bindlessTextures2D_[slot] = white;
622 bindlessFree2D_.push_back(slot);
623 restore(0, slot, white, false);
624 tex->bindlessIndex2D = kInvalidBindlessSlot;
625 }
626 if (tex->bindlessIndexCube != kInvalidBindlessSlot) {
627 const uint32_t slot = tex->bindlessIndexCube;
628 bindlessCubemaps_[slot] = whiteCube;
629 bindlessFreeCube_.push_back(slot);
630 restore(1, slot, whiteCube, true);
631 tex->bindlessIndexCube = kInvalidBindlessSlot;
632 }
633}
634
635void Graphics::ensureVgBuffers() {
636 if (vgGpu_.positions.buffer) return;
637 const auto hostMem = kHostVisibleCoherent;
638 constexpr uint32_t kInitClusters = 16384;
639 constexpr uint32_t kInitVertFloats = 1u << 20; // ~1M floats (~4MB)
640 constexpr uint32_t kInitTriangles = 2u << 20; // ~2M u32 indices
641 vgGpu_.positions = vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer,
642 kInitVertFloats * sizeof(float), hostMem);
643 vgGpu_.triangles = vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer,
644 kInitTriangles * sizeof(uint32_t), hostMem);
645 vgGpu_.clusters = vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer,
646 kInitClusters * sizeof(GpuVgCluster), hostMem);
647 vgGpu_.clusterAssets =
648 vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer,
649 kInitClusters * sizeof(uint32_t), hostMem);
650 constexpr uint32_t kVisBytes = (kMaxVgClusters + 1) * sizeof(uint32_t);
651 constexpr uint32_t kIndBytes = kMaxVgClusters * sizeof(glm::uvec4);
652 vgGpu_.visible = vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer |
653 vk::BufferUsageFlagBits::eIndirectBuffer,
654 kVisBytes * kAsyncResourceCopies, hostMem);
655 vgGpu_.indirect = vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer |
656 vk::BufferUsageFlagBits::eIndirectBuffer,
657 kIndBytes * kAsyncResourceCopies, hostMem);
658 vgGpu_.assetMaterials =
659 vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer,
660 kMaxVgAssets * sizeof(uint32_t) * kAsyncResourceCopies, hostMem);
661 vgGpu_.assetModels =
662 vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer,
663 kMaxVgAssets * sizeof(glm::mat4) * kAsyncResourceCopies, hostMem);
664 {
665 void *m = vgGpu_.visible.map();
666 std::memset(m, 0, kVisBytes * kAsyncResourceCopies);
667 vgGpu_.visible.unmap();
668 m = vgGpu_.assetMaterials.map();
669 std::memset(m, 0, kMaxVgAssets * sizeof(uint32_t) * kAsyncResourceCopies);
670 vgGpu_.assetMaterials.unmap();
671 m = vgGpu_.assetModels.map();
672 for (size_t s = 0; s < kAsyncResourceCopies; ++s)
673 for (uint32_t a = 0; a < kMaxVgAssets; ++a)
674 static_cast<glm::mat4 *>(m)[s * kMaxVgAssets + a] = glm::mat4(1.f);
675 vgGpu_.assetModels.unmap();
676 }
677 bindVgPoolBindless();
678}
679
680void Graphics::growVgBuffers(uint32_t needClusters, uint32_t needVertices,
681 uint32_t needTriangles) {
682 device->waitIdle();
683 const auto hostMem = kHostVisibleCoherent;
684 auto growBuffer = [&](vkb::GenericBuffer &dst, uint32_t elementSize, uint32_t oldElems,
685 uint32_t newElems) {
686 if (newElems <= oldElems) return;
687 vkb::GenericBuffer grown(device, vk::BufferUsageFlagBits::eStorageBuffer,
688 vk::DeviceSize(newElems) * elementSize, hostMem);
689 if (oldElems > 0) {
690 void *srcMap = dst.map();
691 void *dstMap = grown.map();
692 std::memcpy(dstMap, srcMap, vk::DeviceSize(oldElems) * elementSize);
693 grown.unmap();
694 dst.unmap();
695 }
696 dst.release();
697 dst = std::move(grown);
698 };
699 const uint32_t curClusters = uint32_t(vgGpu_.clusters.size / sizeof(GpuVgCluster));
700 const uint32_t curVertFloats = uint32_t(vgGpu_.positions.size / sizeof(float));
701 const uint32_t curTriangles = uint32_t(vgGpu_.triangles.size / sizeof(uint32_t));
702 growBuffer(vgGpu_.clusters, uint32_t(sizeof(GpuVgCluster)), curClusters,
703 std::max(needClusters, curClusters * 2u));
704 growBuffer(vgGpu_.clusterAssets, uint32_t(sizeof(uint32_t)), curClusters,
705 std::max(needClusters, curClusters * 2u));
706 growBuffer(vgGpu_.positions, uint32_t(sizeof(float)), curVertFloats,
707 std::max(needVertices, curVertFloats * 2u));
708 growBuffer(vgGpu_.triangles, uint32_t(sizeof(uint32_t)), curTriangles,
709 std::max(needTriangles, curTriangles * 2u));
710 bindVgPoolBindless();
711}
712
713void Graphics::bindVgPoolBindless() {
714 if (bindlessSets_.empty() || !vgGpu_.positions.buffer) return;
715 auto bufWrite = [&](vk::DescriptorSet set, uint32_t binding, vk::Buffer buffer,
716 vk::DeviceSize offset, vk::DeviceSize size) {
717 vk::DescriptorBufferInfo info{buffer, offset, size};
718 vk::WriteDescriptorSet w{};
719 w.dstSet = set;
720 w.dstBinding = binding;
721 w.descriptorCount = 1;
722 w.descriptorType = vk::DescriptorType::eStorageBuffer;
723 w.pBufferInfo = &info;
724 device->updateDescriptorSets(1, &w, 0, nullptr);
725 };
726 const vk::DeviceSize posBytes = vgGpu_.positions.size;
727 const vk::DeviceSize triBytes = vgGpu_.triangles.size;
728 const vk::DeviceSize clBytes = vgGpu_.clusters.size;
729 const vk::DeviceSize claBytes = vgGpu_.clusterAssets.size;
730 for (vk::DescriptorSet set : bindlessSets_) {
731 bufWrite(set, 22, vgGpu_.positions.buffer, 0, posBytes);
732 bufWrite(set, 23, vgGpu_.triangles.buffer, 0, triBytes);
733 bufWrite(set, 24, vgGpu_.clusters.buffer, 0, clBytes);
734 bufWrite(set, 25, vgGpu_.clusterAssets.buffer, 0, claBytes);
735 bufWrite(set, 28, vgGpu_.assetMaterials.buffer, 0, vgGpu_.assetMaterials.size);
736 bufWrite(set, 29, vgGpu_.assetModels.buffer, 0, vgGpu_.assetModels.size);
737 bufWrite(set, 26, vgGpu_.visible.buffer, 0, vgGpu_.visible.size);
738 bufWrite(set, 27, vgGpu_.indirect.buffer, 0, vgGpu_.indirect.size);
739 }
740}
741
742void Graphics::bindVgFrameBindless(vk::DescriptorSet bindless, size_t slot) {
743 if (!bindless || !vgGpu_.visible.buffer) return;
744 const vk::DeviceSize slotVis = (vk::DeviceSize(kMaxVgClusters) + 1) * sizeof(uint32_t);
745 const vk::DeviceSize slotInd = vk::DeviceSize(kMaxVgClusters) * sizeof(glm::uvec4);
746 const vk::DeviceSize slotMat = vk::DeviceSize(kMaxVgAssets) * sizeof(uint32_t);
747 const vk::DeviceSize slotModel = vk::DeviceSize(kMaxVgAssets) * sizeof(glm::mat4);
748 const vk::DeviceSize visOff = slotVis * slot;
749 const vk::DeviceSize indOff = slotInd * slot;
750 const vk::DeviceSize matOff = slotMat * slot;
751 const vk::DeviceSize modelOff = slotModel * slot;
752 vk::DescriptorBufferInfo infos[4]{
753 {vgGpu_.visible.buffer, visOff, slotVis},
754 {vgGpu_.indirect.buffer, indOff, slotInd},
755 {vgGpu_.assetMaterials.buffer, matOff, slotMat},
756 {vgGpu_.assetModels.buffer, modelOff, slotModel},
757 };
758 vk::WriteDescriptorSet w[4]{};
759 const uint32_t bindings[4] = {26, 27, 28, 29};
760 for (int i = 0; i < 4; ++i) {
761 w[i].dstSet = bindless;
762 w[i].dstBinding = bindings[i];
763 w[i].descriptorCount = 1;
764 w[i].descriptorType = vk::DescriptorType::eStorageBuffer;
765 w[i].pBufferInfo = &infos[i];
766 }
767 device->updateDescriptorSets(4, w, 0, nullptr);
768}
769
771 if (!asset.positions || asset.vertexCount <= 0 || !asset.triangles ||
772 asset.triangleCount <= 0 || !asset.clusters || asset.clusterCount <= 0)
774 if (vgAssetCount_ >= kMaxVgAssets) return kInvalidBindlessSlot;
775 ensureVgBuffers();
776
777 const uint32_t assetId = vgAssetCount_;
778 const uint32_t vertBase = vgVertexCount_; // global vertex index base
779 const uint32_t triBase = vgTriangleCount_; // global triangle (u32) base
780 const uint32_t clusterBase = vgClusterCount_;
781 const uint32_t needClusters = clusterBase + uint32_t(asset.clusterCount);
782 const uint32_t needVerts = uint32_t(vertBase + uint32_t(asset.vertexCount)) * 3;
783 const uint32_t needTris = triBase + uint32_t(asset.triangleCount);
784 const uint32_t curClusters = uint32_t(vgGpu_.clusters.size / sizeof(GpuVgCluster));
785 const uint32_t curVerts = uint32_t(vgGpu_.positions.size / sizeof(float));
786 const uint32_t curTris = uint32_t(vgGpu_.triangles.size / sizeof(uint32_t));
787 if (needClusters > curClusters || needVerts > curVerts || needTris > curTris)
788 growVgBuffers(needClusters, needVerts, needTris);
789
790 void *posMap = vgGpu_.positions.map();
791 void *triMap = vgGpu_.triangles.map();
792 void *clMap = vgGpu_.clusters.map();
793 void *claMap = vgGpu_.clusterAssets.map();
794 if (!posMap || !triMap || !clMap || !claMap) {
795 if (posMap) vgGpu_.positions.unmap();
796 if (triMap) vgGpu_.triangles.unmap();
797 if (clMap) vgGpu_.clusters.unmap();
798 if (claMap) vgGpu_.clusterAssets.unmap();
800 }
801 std::memcpy(static_cast<char *>(posMap) + size_t(vertBase) * 3 * sizeof(float), asset.positions,
802 size_t(asset.vertexCount) * 3 * sizeof(float));
803 {
804 auto *dst = static_cast<uint32_t *>(triMap) + triBase;
805 for (int i = 0; i < asset.triangleCount; ++i) dst[i] = asset.triangles[i] + vertBase;
806 }
807 std::memcpy(static_cast<char *>(clMap) + vgClusterCount_ * sizeof(GpuVgCluster),
808 asset.clusters, size_t(asset.clusterCount) * sizeof(GpuVgCluster));
809 {
810 auto *dst = static_cast<GpuVgCluster *>(clMap) + vgClusterCount_;
811 for (int i = 0; i < asset.clusterCount; ++i) {
812 dst[i].u1[0] += triBase; // triStart -> global triangle stream
813 }
814 }
815 auto *cla = static_cast<uint32_t *>(claMap);
816 for (int i = 0; i < asset.clusterCount; ++i) cla[vgClusterCount_ + uint32_t(i)] = assetId;
817 vgGpu_.positions.unmap();
818 vgGpu_.triangles.unmap();
819 vgGpu_.clusters.unmap();
820 vgGpu_.clusterAssets.unmap();
821
822 vgClusterCount_ = needClusters;
823 vgVertexCount_ = vertBase + uint32_t(asset.vertexCount);
824 vgTriangleCount_ = needTris;
825 vgAssetCount_ = assetId + 1;
826 // Default identity model per slot; vgSetInstance overwrites the current slot.
827 {
828 void *m = vgGpu_.assetModels.map();
829 auto *models = static_cast<glm::mat4 *>(m);
830 for (size_t s = 0; s < kAsyncResourceCopies; ++s)
831 models[s * kMaxVgAssets + assetId] = glm::mat4(1.f);
832 vgGpu_.assetModels.unmap();
833 }
834 return assetId;
835}
836
838 if (!mesh || !mesh->gpuHandle) return kInvalidBindlessSlot;
839 const auto *gpu = static_cast<const GpuMesh *>(mesh->gpuHandle);
840 return gpu->record.vgAssetId;
841}
842
843bool Graphics::gpuDrivenVgAttachToMesh(Mesh *mesh, uint32_t vgAssetId) {
844 if (!mesh || !mesh->gpuHandle) return false;
845 if (vgAssetId >= vgAssetCount_ || vgAssetId >= kMaxVgAssets) return false;
846 auto *gpu = static_cast<GpuMesh *>(mesh->gpuHandle);
847 gpu->record.vgAssetId = vgAssetId;
848 if (gpu->gpuRecordIndex != kInvalidBindlessSlot &&
849 gpu->gpuRecordIndex < meshTableRecords_.size()) {
850 meshTableRecords_[gpu->gpuRecordIndex].vgAssetId = vgAssetId;
851 syncMeshTable();
852 }
853 return true;
854}
855
856bool Graphics::gpuDrivenVgSetInstance(uint32_t vgAssetId, const glm::mat4 &model,
857 uint32_t materialId) {
858 if (vgAssetId >= vgAssetCount_ || vgAssetId >= kMaxVgAssets) return false;
859 if (vgGpu_.assetModels.buffer) {
860 const size_t slot = currentFrameSlot() % kAsyncResourceCopies;
861 void *m = vgGpu_.assetModels.map();
862 static_cast<glm::mat4 *>(m)[slot * kMaxVgAssets + vgAssetId] = model;
863 vgGpu_.assetModels.unmap();
864 void *am = vgGpu_.assetMaterials.map();
865 static_cast<uint32_t *>(am)[slot * kMaxVgAssets + vgAssetId] = materialId;
866 vgGpu_.assetMaterials.unmap();
867 }
868 vgAnyThisFrame_ = true;
869 return true;
870}
871
873 if (!vgGpu_.visible.buffer || vgAssetCount_ == 0) return 0;
874 void *map = vgGpu_.visible.map();
875 if (!map) return 0;
876 const size_t slot = vgLastVisible_ % kAsyncResourceCopies;
877 const uint32_t count = static_cast<const uint32_t *>(map)[slot * (kMaxVgClusters + 1)];
878 vgGpu_.visible.unmap();
879 return count;
880}
881
882uint32_t Graphics::registerMeshRecord(GpuMesh *gpu) {
883 if (!gpu) return kInvalidBindlessSlot;
884 if (gpu->gpuRecordIndex != kInvalidBindlessSlot) return gpu->gpuRecordIndex;
885 if (meshTableRecords_.size() >= meshTableCapacity_) return kInvalidBindlessSlot;
886 // dev's mesh factories do not populate GpuMeshRecord; build it on first
887 // registration from the host-visible buffers (bounds + ranges).
888 if (gpu->record.vertexCount == 0 && gpu->vertices.buffer) {
889 const uint32_t vertexCount = uint32_t(gpu->vertices.size / sizeof(MeshVertex));
890 gpu->record.vertexCount = vertexCount;
891 gpu->record.indexCount = gpu->indexCount;
892 gpu->record.indexType = gpu->indexType == vk::IndexType::eUint16 ? 0u : 1u;
893 void *map = gpu->vertices.map();
894 if (map && vertexCount > 0) {
895 const auto *verts = static_cast<const MeshVertex *>(map);
896 glm::vec3 minv(1e30f), maxv(-1e30f);
897 for (uint32_t i = 0; i < vertexCount; ++i) {
898 minv = glm::min(minv, verts[i].pos);
899 maxv = glm::max(maxv, verts[i].pos);
900 }
901 const glm::vec3 center = (minv + maxv) * 0.5f;
902 float radius = 0.f;
903 for (uint32_t i = 0; i < vertexCount; ++i)
904 radius = std::max(radius, glm::length(verts[i].pos - center));
905 gpu->record.boundsCenterRadius = glm::vec4(center, radius);
906 }
907 if (map) gpu->vertices.unmap();
908 }
909 // Stage 3: lazily pool the mesh's vertices/indices so the vis resolve can
910 // fetch attributes by (pool offset + triangle + barycentric).
911 appendGpuMeshToPool(*gpu);
912 const uint32_t idx = uint32_t(meshTableRecords_.size());
913 meshTableRecords_.push_back(gpu->record);
914 meshRecordOwners_.push_back(gpu);
915 gpu->gpuRecordIndex = idx;
916 syncMeshTable();
917 return idx;
918}
919
920void Graphics::syncMeshTable() {
921 if (!meshTableBuffer_.buffer || meshTableRecords_.empty()) return;
922 meshTableBuffer_.updateLocal(vkb::FrameSlot::gpuIdle(), meshTableRecords_.data(),
923 meshTableRecords_.size() * sizeof(GpuMeshRecord));
924}
925
926GpuMaterialRecord Graphics::buildMaterialRecord(Material *material) {
927 GpuMaterialRecord rec{};
928 if (!material) return rec;
929 rec.tint = glm::vec4(material->getTintR(), material->getTintG(), material->getTintB(),
930 material->getTintA());
931 rec.pbr = glm::vec4(material->getMetallic(), material->getRoughness(),
932 material->getReceiveShadow() ? 1.f : 0.f,
933 material->getReceiveLight() ? 1.f : 0.f);
934 rec.texBomb = glm::vec4(material->getTexCellBombScale(), material->getTexCellBombStrength(),
935 material->getTexCellBombRotation(), 0.f);
936 rec.parallax = glm::vec4(material->getParallaxScale(), material->getParallaxMinLayers(),
937 material->getParallaxMaxLayers(), 0.f);
938 auto slotOf = [](Texture *t) {
939 if (!t || !t->gpuHandle) return kInvalidBindlessSlot;
940 return static_cast<GpuTexture *>(t->gpuHandle)->bindlessIndex2D;
941 };
942 rec.textureSlots[0] = slotOf(material->getAlbedoTexture());
943 rec.textureSlots[1] = slotOf(material->getNormalTexture());
944 rec.textureSlots[2] = slotOf(material->getHeightTexture());
945 rec.textureSlots[3] = kInvalidBindlessSlot; // env is camera state, not material state
946 const std::string model = material->getShadingModel();
947 rec.shadingModel = (model == "unlit") ? 1u : (model == "hair") ? 2u
948 : (model == "custom") ? 3u
949 : 0u;
950 if (material->getCastShadow()) rec.flags |= 1u;
951 if (material->getCastOcclusion()) rec.flags |= 2u;
952 return rec;
953}
954
956 if (!material) return kInvalidBindlessSlot;
957 auto it = materialTableIndex_.find(material);
958 if (it != materialTableIndex_.end()) {
959 materialTableRecords_[it->second] = buildMaterialRecord(material);
961 return it->second;
962 }
963 if (materialTableRecords_.size() >= materialTableCapacity_) return kInvalidBindlessSlot;
964 const uint32_t idx = uint32_t(materialTableRecords_.size());
965 materialTableIndex_.emplace(material, idx);
966 materialTableRecords_.push_back(buildMaterialRecord(material));
968 return idx;
969}
970
972 if (!materialTableBuffer_.buffer || materialTableRecords_.empty()) return;
973 materialTableBuffer_.updateLocal(vkb::FrameSlot::gpuIdle(), materialTableRecords_.data(),
974 materialTableRecords_.size() *
975 sizeof(GpuMaterialRecord));
976}
977
979 if (!mesh || !mesh->gpuHandle) return kInvalidBindlessSlot;
980 auto *gpu = static_cast<GpuMesh *>(mesh->gpuHandle);
981 if (gpu->gpuRecordIndex == kInvalidBindlessSlot) registerMeshRecord(gpu);
982 return gpu->gpuRecordIndex;
983}
984
986 const uint32_t id = materialTableGetOrCreate(material);
987 // Any material with a GPU table record is representable by the bindless
988 // path; descriptor-array indexing handles arbitrary slots.
989 return id != kInvalidBindlessSlot;
990}
991
992bool Graphics::gpuDrivenSubmitOpaque(const GpuInstance *instances, uint32_t instanceCount) {
993 if (!gpuDrivenEnabled() || !gpuDrivenCaps_.gpuDrivenAvailable()) return false;
994 if (!mesh3dGpuDrivenPipeline || bindlessSets_.empty() || !meshTableBuffer_.buffer) return false;
995 if (!instances || instanceCount == 0) return false;
996
997 // Sort by (material, mesh) and merge buckets using the GPU mesh table.
999 for (uint32_t i = 0; i < instanceCount; ++i) {
1000 builder.add(i, instances[i].meshId, instances[i].materialId, 0);
1001 }
1002 const uint32_t drawCount = builder.build(meshTableRecords_);
1003 if (drawCount == 0) return false;
1004 lastGpuDrivenDrawCount_ = drawCount;
1005 const auto &cmds = builder.commands();
1006 const auto &order = builder.sortedInstanceOrder();
1007
1008 // Upload instances in the sorted order so each bucket is a contiguous range.
1009 std::vector<GpuInstance> sorted(instanceCount);
1010 for (uint32_t i = 0; i < instanceCount; ++i) sorted[i] = instances[order[i]];
1011
1012 auto &arena = currentFrameArena();
1013 FrameArena::Alloc instAlloc = arena.alloc(instanceCount * sizeof(GpuInstance), 16);
1014 FrameArena::Alloc cmdAlloc = arena.alloc(drawCount * sizeof(GpuIndirectCommand), 16);
1015 if (!instAlloc.mapped || !cmdAlloc.mapped) return false; // arena overflow: caller falls back
1016 std::memcpy(instAlloc.mapped, sorted.data(), instanceCount * sizeof(GpuInstance));
1017 std::memcpy(cmdAlloc.mapped, cmds.data(), drawCount * sizeof(GpuIndirectCommand));
1018
1019 // Bind the arena instance buffer as bindless binding 4 (update before bind,
1020 // on the current frame slot's set only; the other slot's pending command
1021 // buffers keep pointing at their own frame's arena).
1022 const vk::DescriptorSet bindless = bindlessSetForFrame();
1023 if (!bindless) return false;
1024 vk::DescriptorBufferInfo instInfo{arena.buffer(), instAlloc.offset, instAlloc.size};
1025 vk::WriteDescriptorSet instWrite{};
1026 instWrite.dstSet = bindless;
1027 instWrite.dstBinding = 4;
1028 instWrite.descriptorCount = 1;
1029 instWrite.descriptorType = vk::DescriptorType::eStorageBuffer;
1030 instWrite.pBufferInfo = &instInfo;
1031 device->updateDescriptorSets(1, &instWrite, 0, nullptr);
1032
1033 // Per-frame set0 through the shared ring (dynamic UBO offsets).
1034 const Graphics::GpuDrivenFrameSet0 s0 = gpuDrivenFrameSet0();
1035 if (!s0.set) return false;
1036 const uint32_t dynOffsets[2] = {s0.uboOffset, s0.shadowOffset};
1037
1038 auto &cb = currentPresentCb();
1039 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, mesh3dGpuDrivenPipeline);
1040 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, mesh3dGpuDrivenPipelineLayout, 0, 1,
1041 &s0.set, 2, dynOffsets);
1042 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, mesh3dGpuDrivenPipelineLayout, 1, 1,
1043 &bindless, 0, nullptr);
1044 const vk::DeviceSize stride = sizeof(GpuIndirectCommand);
1045 // Stage 1 keeps one host buffer pair per mesh (no pool yet), so each draw
1046 // group must bind the owning mesh's vertex/index buffers. The builder sorts
1047 // by (pipeline, material, mesh), so commands sharing a mesh are contiguous;
1048 // bind only when the mesh changes.
1049 GpuMesh *boundMesh = nullptr;
1050 for (uint32_t i = 0; i < drawCount; ++i) {
1051 const GpuInstance &first = sorted[cmds[i].firstInstance];
1052 if (first.meshId >= meshRecordOwners_.size()) {
1053 EV_ASSERT(false, "indirect draw references an unregistered mesh record");
1054 continue;
1055 }
1056 GpuMesh *mesh = meshRecordOwners_[first.meshId];
1057 if (mesh != boundMesh) {
1058 const vk::DeviceSize vbOffset = 0;
1059 cb.bindVertexBuffers(0, 1, mesh->vertices, &vbOffset);
1060 cb.bindIndexBuffer(mesh->indices.buffer, 0, mesh->indexType);
1061 boundMesh = mesh;
1062 }
1063 cb.drawIndexedIndirect(arena.buffer(), cmdAlloc.offset + vk::DeviceSize(i) * stride, 1,
1064 stride);
1065 }
1066 return true;
1067}
1068
1069// --- Stage 2: HZB + GPU cull ------------------------------------------------
1070
1071namespace {
1072
1074void gpuDrivenFrustumPlanes(const glm::mat4 &m, glm::vec4 planes[6]) {
1075 // glm is column-major: row i is (m[0][i], m[1][i], m[2][i], m[3][i]).
1076 const glm::vec4 row0(m[0][0], m[1][0], m[2][0], m[3][0]);
1077 const glm::vec4 row1(m[0][1], m[1][1], m[2][1], m[3][1]);
1078 const glm::vec4 row2(m[0][2], m[1][2], m[2][2], m[3][2]);
1079 const glm::vec4 row3(m[0][3], m[1][3], m[2][3], m[3][3]);
1080 planes[0] = row3 + row0; // left
1081 planes[1] = row3 - row0; // right
1082 planes[2] = row3 + row1; // bottom
1083 planes[3] = row3 - row1; // top
1084 planes[4] = row3 + row2; // near
1085 planes[5] = row3 - row2; // far
1086 for (int i = 0; i < 6; ++i) {
1087 const float len = glm::length(glm::vec3(planes[i]));
1088 if (len > 1e-8f) planes[i] /= len;
1089 }
1090}
1091
1092} // namespace
1093
1094Graphics::GpuDrivenCullSlot &Graphics::gpuDrivenCullSlot(uint32_t frameSlot) {
1095 // Callers guard with gpuDrivenCullReady_; this is a debug-only invariant.
1096 EV_ASSERT(!gpuDrivenCullSlots_.empty(), "gpuDriven cull slot accessed before resources");
1097 return gpuDrivenCullSlots_[frameSlot % gpuDrivenCullSlots_.size()];
1098}
1099
1100Graphics::GpuDrivenCullSlot &Graphics::currentGpuDrivenCullSlot() {
1101 return gpuDrivenCullSlot(currentFrameSlot());
1102}
1103
1104void Graphics::ensureGpuDrivenCullResources(int width, int height) {
1105 if (!gpuDrivenCaps_.gpuDrivenCullAvailable() || width <= 0 || height <= 0) return;
1106 if (gpuDrivenCullReady_ && gpuDrivenCullWidth == width && gpuDrivenCullHeight == height)
1107 return;
1108 destroyGpuDrivenCullResources();
1109
1110 gpuDrivenCullWidth = width;
1111 gpuDrivenCullHeight = height;
1112
1113 // HZB word layout per slot: [16 header uints][mip0][mip1]...
1114 uint32_t mipOffsets[kMaxHzbMips] = {};
1115 uint32_t totalWords = kHZBHeaderWords;
1116 uint32_t maxMip = 0;
1117 {
1118 uint32_t w = uint32_t(width);
1119 uint32_t h = uint32_t(height);
1120 for (uint32_t m = 0; m < kMaxHzbMips; ++m) {
1121 mipOffsets[m] = totalWords;
1122 const uint32_t mw = std::max(w >> m, 1u);
1123 const uint32_t mh = std::max(h >> m, 1u);
1124 totalWords += mw * mh;
1125 maxMip = m;
1126 if (mw == 1 && mh == 1) break;
1127 }
1128 }
1129 gpuDrivenCullMaxMip = maxMip;
1130
1131 const vk::DeviceSize flagBytes = kMaxGpuDrivenInstances * sizeof(uint32_t);
1132 const vk::DeviceSize compactBytes = kMaxGpuDrivenInstances * sizeof(GpuInstance);
1133 const vk::DeviceSize indirectBytes = kMaxGpuDrivenBuckets * sizeof(GpuIndirectCommand);
1134 const vk::DeviceSize indirectNIBytes = kMaxGpuDrivenBuckets * sizeof(glm::uvec4);
1135 const vk::DeviceSize counterBytes = kMaxGpuDrivenBuckets * sizeof(uint32_t);
1136 const vk::DeviceSize hzbBytes = totalWords * sizeof(uint32_t);
1137
1138 gpuDrivenCullSlots_.resize(frameSlotCount());
1139 for (auto &slot : gpuDrivenCullSlots_) {
1140 slot.visibleFlags =
1141 vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer, flagBytes,
1142 kHostVisibleCoherent);
1143 slot.compacted = vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer,
1144 compactBytes, kHostVisibleCoherent);
1145 slot.indirect = vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer |
1146 vk::BufferUsageFlagBits::eIndirectBuffer,
1147 indirectBytes, kHostVisibleCoherent);
1148 slot.indirectNI = vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer |
1149 vk::BufferUsageFlagBits::eIndirectBuffer,
1150 indirectNIBytes, kHostVisibleCoherent);
1151 slot.bucketCounters =
1152 vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer, counterBytes,
1153 kHostVisibleCoherent);
1154 slot.hzb = vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eStorageBuffer, hzbBytes,
1155 kHostVisibleCoherent);
1156 slot.cullParams =
1157 vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eUniformBuffer,
1158 sizeof(GpuCullParams), kHostVisibleCoherent);
1159 // Header offsets + zero depth (0 = near plane -> everything visible).
1160 void *hzbMap = slot.hzb.map();
1161 auto *u32 = static_cast<uint32_t *>(hzbMap);
1162 for (uint32_t m = 0; m <= maxMip; ++m) u32[m] = mipOffsets[m];
1163 for (uint32_t m = maxMip + 1; m < kHZBHeaderWords; ++m) u32[m] = 0;
1164 std::memset(static_cast<char *>(hzbMap) + kHZBHeaderWords * sizeof(uint32_t), 0,
1165 (totalWords - kHZBHeaderWords) * sizeof(uint32_t));
1166 slot.hzb.unmap();
1167 }
1168
1169 // Compute pipeline layout: set 1 = bindless (set 0 = empty placeholder so
1170 // the shaders' `layout(set = 1, ...)` bindings resolve to index 1).
1171 vk::DescriptorSetLayoutCreateInfo emptyInfo{};
1172 gpuDrivenComputeEmptyLayout_ = device->createDescriptorSetLayout(emptyInfo);
1173 vk::PushConstantRange pcr{vk::ShaderStageFlagBits::eCompute, 0, 20};
1174 vk::PipelineLayoutCreateInfo pli{};
1175 std::array<vk::DescriptorSetLayout, 2> computeLayouts{gpuDrivenComputeEmptyLayout_,
1176 bindlessSetLayout_};
1177 pli.setLayoutCount = uint32_t(computeLayouts.size());
1178 pli.pSetLayouts = computeLayouts.data();
1179 pli.pushConstantRangeCount = 1;
1180 pli.pPushConstantRanges = &pcr;
1181 gpuDrivenComputeLayout = device->createPipelineLayout(pli);
1182
1183 const auto spvOf = [](const uint32_t *p, size_t n) {
1184 return std::vector<uint32_t>(p, p + n);
1185 };
1186 hzbBuildPass_.create(device, gpuDrivenComputeLayout,
1187 spvOf(hzb_build_comp_spv, hzb_build_comp_spv_count));
1188 cullPass_.create(device, gpuDrivenComputeLayout,
1189 spvOf(gpu_cull_comp_spv, gpu_cull_comp_spv_count));
1190 emitPass_.create(device, gpuDrivenComputeLayout,
1191 spvOf(gpu_emit_comp_spv, gpu_emit_comp_spv_count));
1192 vgCullPass_.create(device, gpuDrivenComputeLayout,
1193 spvOf(vg_main_cull_comp_spv, vg_main_cull_comp_spv_count));
1194 gpuDrivenCullReady_ =
1195 hzbBuildPass_.pipeline() && cullPass_.pipeline() && emitPass_.pipeline();
1196}
1197
1198void Graphics::destroyGpuDrivenCullResources() {
1199 hzbBuildPass_ = ComputePass{};
1200 cullPass_ = ComputePass{};
1201 emitPass_ = ComputePass{};
1202 vgCullPass_ = ComputePass{};
1203 if (gpuDrivenComputeLayout) {
1204 device->destroyPipelineLayout(gpuDrivenComputeLayout);
1205 gpuDrivenComputeLayout = nullptr;
1206 }
1207 if (gpuDrivenComputeEmptyLayout_) {
1208 device->destroyDescriptorSetLayout(gpuDrivenComputeEmptyLayout_);
1209 gpuDrivenComputeEmptyLayout_ = nullptr;
1210 }
1211 for (auto &slot : gpuDrivenCullSlots_) {
1212 slot.visibleFlags.release();
1213 slot.compacted.release();
1214 slot.indirect.release();
1215 slot.indirectNI.release();
1216 slot.bucketCounters.release();
1217 slot.hzb.release();
1218 slot.cullParams.release();
1219 }
1220 gpuDrivenCullSlots_.clear();
1221 gpuDrivenCullReady_ = false;
1222 gpuDrivenCullWidth = 0;
1223 gpuDrivenCullHeight = 0;
1224}
1225
1226void Graphics::recordGpuDrivenHzbBuild() {
1227 if (!gpuDrivenCullReady_) return;
1228 auto *slot = currentGBufferSlot();
1229 if (!slot || !slot->depthGpu.sampler || !slot->depthGpu.imageView()) return;
1230 auto &cull = currentGpuDrivenCullSlot();
1231 auto &cb = currentPresentCb();
1232
1233 // Depth attachment write -> compute shader read.
1234 vk::ImageMemoryBarrier imb{};
1235 imb.image = slot->depth.image();
1236 imb.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
1237 imb.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
1238 imb.srcAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentWrite;
1239 imb.dstAccessMask = vk::AccessFlagBits::eShaderRead;
1240 imb.subresourceRange = {vk::ImageAspectFlagBits::eDepth, 0, 1, 0, 1};
1241 cb.pipelineBarrier(vk::PipelineStageFlagBits::eEarlyFragmentTests |
1242 vk::PipelineStageFlagBits::eLateFragmentTests,
1243 vk::PipelineStageFlagBits::eComputeShader, {}, 0, nullptr, 0, nullptr, 1,
1244 &imb);
1245
1246 uint32_t mipOffsets[kMaxHzbMips] = {};
1247 uint32_t totalWords = kHZBHeaderWords;
1248 {
1249 uint32_t w = uint32_t(gpuDrivenCullWidth);
1250 uint32_t h = uint32_t(gpuDrivenCullHeight);
1251 for (uint32_t m = 0; m <= gpuDrivenCullMaxMip; ++m) {
1252 mipOffsets[m] = totalWords;
1253 const uint32_t mw = std::max(w >> m, 1u);
1254 const uint32_t mh = std::max(h >> m, 1u);
1255 totalWords += mw * mh;
1256 }
1257 }
1258 for (uint32_t m = 0; m <= gpuDrivenCullMaxMip; ++m) {
1259 if (m > 0) {
1260 // Previous mip write -> this mip read.
1261 vk::BufferMemoryBarrier bmb{};
1262 bmb.buffer = cull.hzb.buffer;
1263 bmb.size = VK_WHOLE_SIZE;
1264 bmb.srcAccessMask = vk::AccessFlagBits::eShaderWrite;
1265 bmb.dstAccessMask = vk::AccessFlagBits::eShaderRead;
1266 cb.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader,
1267 vk::PipelineStageFlagBits::eComputeShader, {}, 0, nullptr, 1, &bmb,
1268 0, nullptr);
1269 }
1270 const uint32_t mw = std::max(uint32_t(gpuDrivenCullWidth) >> m, 1u);
1271 const uint32_t mh = std::max(uint32_t(gpuDrivenCullHeight) >> m, 1u);
1272 struct HzbPush {
1273 uint32_t mip;
1274 uint32_t width;
1275 uint32_t height;
1276 uint32_t slotBase;
1277 uint32_t prevOffset;
1278 } push{m, mw, mh, 0u, mipOffsets[m > 0 ? m - 1 : 0]};
1279 cb.pushConstants(gpuDrivenComputeLayout, vk::ShaderStageFlagBits::eCompute, 0,
1280 sizeof(push), &push);
1281 hzbBuildPass_.record(cb, (mw + 7) / 8, (mh + 7) / 8, 1);
1282 }
1283}
1284
1285bool Graphics::gpuDrivenCullBegin(const GpuInstance *instances, uint32_t instanceCount) {
1286 if (!gpuDrivenCullEnabled() || !gpuDrivenCullReady_) return false;
1287 if (!instances || instanceCount == 0) return false;
1288 instanceCount = std::min(instanceCount, kMaxGpuDrivenInstances);
1289
1291 for (uint32_t i = 0; i < instanceCount; ++i) {
1292 builder.add(i, instances[i].meshId, instances[i].materialId, 0);
1293 }
1294 const uint32_t bucketCount = builder.build(meshTableRecords_);
1295 if (bucketCount == 0 || bucketCount > kMaxGpuDrivenBuckets) return false;
1296 const auto &cmds = builder.commands();
1297 const auto &order = builder.sortedInstanceOrder();
1298
1299 std::vector<GpuInstance> sorted(instanceCount);
1300 for (uint32_t i = 0; i < instanceCount; ++i) sorted[i] = instances[order[i]];
1301
1302 gpuDrivenBucketIds_.assign(instanceCount, 0);
1303 gpuDrivenBucketOffsets_.resize(bucketCount);
1304 gpuDrivenBucketMeshIds_.resize(bucketCount);
1305 for (uint32_t b = 0; b < bucketCount; ++b) {
1306 gpuDrivenBucketOffsets_[b] = cmds[b].firstInstance;
1307 gpuDrivenBucketMeshIds_[b] = sorted[cmds[b].firstInstance].meshId;
1308 }
1309 {
1310 uint32_t b = 0;
1311 for (uint32_t j = 0; j < instanceCount; ++j) {
1312 while (b + 1 < bucketCount && cmds[b + 1].firstInstance <= j) ++b;
1313 gpuDrivenBucketIds_[j] = b;
1314 }
1315 }
1316
1317 auto &arena = currentFrameArena();
1318 // Storage-buffer descriptor offsets must honor minStorageBufferOffsetAlignment
1319 // (64 on this Intel driver); align conservatively to 64.
1320 gpuDrivenInstAlloc_ = arena.alloc(instanceCount * sizeof(GpuInstance), 64);
1321 gpuDrivenBucketIdAlloc_ = arena.alloc(instanceCount * sizeof(uint32_t), 64);
1322 gpuDrivenBucketOffAlloc_ = arena.alloc(bucketCount * sizeof(uint32_t), 64);
1323 if (!gpuDrivenInstAlloc_.mapped || !gpuDrivenBucketIdAlloc_.mapped ||
1324 !gpuDrivenBucketOffAlloc_.mapped) {
1325 gpuDrivenCullInstanceCount_ = 0;
1326 return false;
1327 }
1328 std::memcpy(gpuDrivenInstAlloc_.mapped, sorted.data(),
1329 instanceCount * sizeof(GpuInstance));
1330 std::memcpy(gpuDrivenBucketIdAlloc_.mapped, gpuDrivenBucketIds_.data(),
1331 instanceCount * sizeof(uint32_t));
1332 std::memcpy(gpuDrivenBucketOffAlloc_.mapped, gpuDrivenBucketOffsets_.data(),
1333 bucketCount * sizeof(uint32_t));
1334
1335 gpuDrivenBucketCount_ = bucketCount;
1336 gpuDrivenCullInstanceCount_ = instanceCount;
1337 lastGpuDrivenDrawCount_ = bucketCount; // debug counter: bucket draws the cull path emits
1338
1339 // Cull source buffers: sorted instances (17), bucket ids (12), offsets (13).
1340 const vk::DescriptorSet bindless = bindlessSetForFrame();
1341 if (!bindless) {
1342 gpuDrivenCullInstanceCount_ = 0;
1343 return false;
1344 }
1345 auto bufWrite = [&](uint32_t binding, vk::Buffer buffer, vk::DeviceSize offset,
1346 vk::DeviceSize size) {
1347 vk::DescriptorBufferInfo info{buffer, offset, size};
1348 vk::WriteDescriptorSet w{};
1349 w.dstSet = bindless;
1350 w.dstBinding = binding;
1351 w.descriptorCount = 1;
1352 w.descriptorType = vk::DescriptorType::eStorageBuffer;
1353 w.pBufferInfo = &info;
1354 device->updateDescriptorSets(1, &w, 0, nullptr);
1355 };
1356 bufWrite(17, arena.buffer(), gpuDrivenInstAlloc_.offset, gpuDrivenInstAlloc_.size);
1357 bufWrite(12, arena.buffer(), gpuDrivenBucketIdAlloc_.offset, gpuDrivenBucketIdAlloc_.size);
1358 bufWrite(13, arena.buffer(), gpuDrivenBucketOffAlloc_.offset, gpuDrivenBucketOffAlloc_.size);
1359 return true;
1360}
1361
1362void Graphics::gpuDrivenRecordComputeSection(const glm::mat4 &viewProj, const glm::vec3 &eye,
1363 float fovYDeg, float nearZ, float farZ) {
1364 if (!gpuDrivenCullReady_) return;
1365 auto &slot = currentGpuDrivenCullSlot();
1366 auto &cb = currentPresentCb();
1367 const vk::DescriptorSet bindless = bindlessSetForFrame();
1368 if (!bindless) return;
1369 // Lazily-created defaults register into the bindless set (bindings 0/1);
1370 // do it BEFORE the set is bound to the recording command buffer.
1371 ensureFlatNormalTexture3D();
1372 ensureFlatHeightTexture3D();
1373 ensureDefaultEnvCubemap();
1374 // Per-frame bindless updates must happen BEFORE the set is bound to the
1375 // recording command buffer (no UPDATE_AFTER_BIND): updating it afterwards
1376 // invalidates the command buffer (VUID-vkCmdBindPipeline-commandBuffer-recording).
1377 createGBufferResources(gbufferWidth > 0 ? gbufferWidth : int(swapchain.extent.width),
1378 gbufferHeight > 0 ? gbufferHeight : int(swapchain.extent.height));
1379 {
1380 auto *gbSlot = currentGBufferSlot();
1381 if (gbSlot && gbSlot->visIDGpu.sampler && gbSlot->visBaryGpu.sampler) {
1382 vk::DescriptorImageInfo visIDInfo{gbSlot->visIDGpu.sampler, gbSlot->visIDGpu.imageView(),
1383 vk::ImageLayout::eShaderReadOnlyOptimal};
1384 vk::DescriptorImageInfo visBaryInfo{gbSlot->visBaryGpu.sampler,
1385 gbSlot->visBaryGpu.imageView(),
1386 vk::ImageLayout::eShaderReadOnlyOptimal};
1387 vk::WriteDescriptorSet w[2]{};
1388 w[0].dstSet = bindless;
1389 w[0].dstBinding = 15;
1390 w[0].descriptorCount = 1;
1391 w[0].descriptorType = vk::DescriptorType::eCombinedImageSampler;
1392 w[0].pImageInfo = &visIDInfo;
1393 w[1].dstSet = bindless;
1394 w[1].dstBinding = 16;
1395 w[1].descriptorCount = 1;
1396 w[1].descriptorType = vk::DescriptorType::eCombinedImageSampler;
1397 w[1].pImageInfo = &visBaryInfo;
1398 device->updateDescriptorSets(2, w, 0, nullptr);
1399 }
1400 }
1401 bindVgFrameBindless(bindless, currentFrameSlot() % kAsyncResourceCopies);
1402
1403 GpuCullParams params{};
1404 params.viewProj = viewProj;
1405 gpuDrivenFrustumPlanes(viewProj, params.frustumPlanes);
1406 params.cameraPos = glm::vec4(eye, 0.f);
1407 const int w = gpuDrivenCullWidth;
1408 const int h = gpuDrivenCullHeight;
1409 params.screen = glm::vec4(float(w), float(h), 1.f / float(w), 1.f / float(h));
1410 const float fovRad = fovYDeg * 0.017453292519943295f;
1411 params.clipNearFar =
1412 glm::vec4(nearZ, farZ, float(h) * 0.5f / std::tan(fovRad * 0.5f), 1.f);
1413 params.hzbInfo =
1414 glm::vec4(float(gpuDrivenCullMaxMip), 0.f, float(w), float(h));
1415 params.counts = glm::uvec4(gpuDrivenCullInstanceCount_, gpuDrivenBucketCount_,
1417 {
1418 void *pMap = slot.cullParams.map();
1419 std::memcpy(pMap, &params, sizeof(params));
1420 slot.cullParams.unmap();
1421 }
1422
1423 auto bufWrite = [&](uint32_t binding, vk::Buffer buffer, vk::DeviceSize offset,
1424 vk::DeviceSize size, vk::DescriptorType type) {
1425 vk::DescriptorBufferInfo info{buffer, offset, size};
1426 vk::WriteDescriptorSet w{};
1427 w.dstSet = bindless;
1428 w.dstBinding = binding;
1429 w.descriptorCount = 1;
1430 w.descriptorType = type;
1431 w.pBufferInfo = &info;
1432 device->updateDescriptorSets(1, &w, 0, nullptr);
1433 };
1434 bufWrite(6, slot.cullParams.buffer, 0, sizeof(GpuCullParams),
1435 vk::DescriptorType::eUniformBuffer);
1436 bufWrite(7, slot.visibleFlags.buffer, 0, VK_WHOLE_SIZE,
1437 vk::DescriptorType::eStorageBuffer);
1438 bufWrite(8, slot.compacted.buffer, 0, VK_WHOLE_SIZE, vk::DescriptorType::eStorageBuffer);
1439 bufWrite(9, slot.indirect.buffer, 0, VK_WHOLE_SIZE, vk::DescriptorType::eStorageBuffer);
1440 bufWrite(30, slot.indirectNI.buffer, 0, VK_WHOLE_SIZE, vk::DescriptorType::eStorageBuffer);
1441 bufWrite(14, slot.bucketCounters.buffer, 0, VK_WHOLE_SIZE,
1442 vk::DescriptorType::eStorageBuffer);
1443 // The draw consumes the compacted buffer as its instance source (binding 4).
1444 bufWrite(4, slot.compacted.buffer, 0, VK_WHOLE_SIZE, vk::DescriptorType::eStorageBuffer);
1445 // HZB buffer + GBuffer depth sampler for the build pass.
1446 bufWrite(10, slot.hzb.buffer, 0, VK_WHOLE_SIZE, vk::DescriptorType::eStorageBuffer);
1447 {
1448 auto *gbSlot = currentGBufferSlot();
1449 if (gbSlot && gbSlot->depthGpu.sampler && gbSlot->depthGpu.imageView()) {
1450 vk::DescriptorImageInfo depthInfo{gbSlot->depthGpu.sampler, gbSlot->depthGpu.imageView(),
1451 vk::ImageLayout::eShaderReadOnlyOptimal};
1452 vk::WriteDescriptorSet wd{};
1453 wd.dstSet = bindless;
1454 wd.dstBinding = 11;
1455 wd.descriptorCount = 1;
1456 wd.descriptorType = vk::DescriptorType::eCombinedImageSampler;
1457 wd.pImageInfo = &depthInfo;
1458 device->updateDescriptorSets(1, &wd, 0, nullptr);
1459 }
1460 }
1461
1462 // All descriptor updates are done; bind the bindless set ONCE for the
1463 // whole compute section (the set is not UPDATE_AFTER_BIND, so updating it
1464 // while bound to a recording command buffer would invalidate the buffer).
1465 cb.bindDescriptorSets(vk::PipelineBindPoint::eCompute, gpuDrivenComputeLayout, 1, 1,
1466 &bindless, 0, nullptr);
1467
1468 recordGpuDrivenHzbBuild();
1469
1470 // HZB build writes (storage buffer) -> cull reads.
1471 {
1472 vk::BufferMemoryBarrier bmb{};
1473 bmb.buffer = slot.hzb.buffer;
1474 bmb.size = VK_WHOLE_SIZE;
1475 bmb.srcAccessMask = vk::AccessFlagBits::eShaderWrite;
1476 bmb.dstAccessMask = vk::AccessFlagBits::eShaderRead;
1477 cb.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader,
1478 vk::PipelineStageFlagBits::eComputeShader, {}, 0, nullptr, 1, &bmb, 0,
1479 nullptr);
1480 }
1481}
1482
1483void Graphics::gpuDrivenVgComputeSection(const glm::mat4 &viewProj, const glm::vec3 &eye,
1484 float fovYDeg, float nearZ, float farZ) {
1485 gpuDrivenRecordComputeSection(viewProj, eye, fovYDeg, nearZ, farZ);
1486}
1487
1488void Graphics::gpuDrivenCullEmit(const glm::mat4 &viewProj, const glm::vec3 &eye, float fovYDeg,
1489 float nearZ, float farZ) {
1490 if (!gpuDrivenCullReady_ || gpuDrivenCullInstanceCount_ == 0) return;
1491 gpuDrivenLastCullSlot_ = uint32_t(currentFrameSlot());
1492 auto &slot = currentGpuDrivenCullSlot();
1493 auto &cb = currentPresentCb();
1494 // This frame's own bindless set: the previous frame that owned this slot
1495 // completed at acquireForFrame()'s fence wait, so rewriting it is safe.
1496 const vk::DescriptorSet bindless = bindlessSetForFrame();
1497 if (!bindless) return;
1498
1499 // CPU reset of GPU-owned per-slot state (slot's previous frame is complete).
1500 {
1501 void *flagsMap = slot.visibleFlags.map();
1502 std::memset(flagsMap, 0, kMaxGpuDrivenInstances * sizeof(uint32_t));
1503 slot.visibleFlags.unmap();
1504 void *counterMap = slot.bucketCounters.map();
1505 std::memset(counterMap, 0, kMaxGpuDrivenBuckets * sizeof(uint32_t));
1506 slot.bucketCounters.unmap();
1507 // The emit pass only writes buckets that survived culling; clear the
1508 // commands so a stale instanceCount from an earlier frame cannot make
1509 // a culled bucket draw again (readbacks / validation read this too).
1510 void *cmdMap = slot.indirect.map();
1511 std::memset(cmdMap, 0, kMaxGpuDrivenBuckets * sizeof(GpuIndirectCommand));
1512 slot.indirect.unmap();
1513 }
1514
1515 gpuDrivenRecordComputeSection(viewProj, eye, fovYDeg, nearZ, farZ);
1516
1517 const uint32_t groups = (gpuDrivenCullInstanceCount_ + 63u) / 64u;
1518 // Arena host writes + previous-frame HZB shader writes visible to cull.
1519 {
1520 vk::BufferMemoryBarrier bmb{};
1521 bmb.buffer = currentFrameArena().buffer();
1522 bmb.size = VK_WHOLE_SIZE;
1523 bmb.srcAccessMask = vk::AccessFlagBits::eHostWrite | vk::AccessFlagBits::eShaderWrite;
1524 bmb.dstAccessMask = vk::AccessFlagBits::eShaderRead;
1525 cb.pipelineBarrier(vk::PipelineStageFlagBits::eHost |
1526 vk::PipelineStageFlagBits::eComputeShader,
1527 vk::PipelineStageFlagBits::eComputeShader, {}, 0, nullptr, 1, &bmb, 0,
1528 nullptr);
1529 }
1530 cullPass_.record(cb, groups, 1, 1);
1531
1532 // Cull flags write -> emit read.
1533 {
1534 vk::BufferMemoryBarrier bmb{};
1535 bmb.buffer = slot.visibleFlags.buffer;
1536 bmb.size = VK_WHOLE_SIZE;
1537 bmb.srcAccessMask = vk::AccessFlagBits::eShaderWrite;
1538 bmb.dstAccessMask = vk::AccessFlagBits::eShaderRead;
1539 cb.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader,
1540 vk::PipelineStageFlagBits::eComputeShader, {}, 0, nullptr, 1, &bmb, 0,
1541 nullptr);
1542 }
1543 emitPass_.record(cb, groups, 1, 1);
1544
1545 // Emit writes (compacted + commands) -> vertex read + indirect read.
1546 {
1547 vk::BufferMemoryBarrier bmb[2]{};
1548 bmb[0].buffer = slot.compacted.buffer;
1549 bmb[0].size = VK_WHOLE_SIZE;
1550 bmb[0].srcAccessMask = vk::AccessFlagBits::eShaderWrite;
1551 bmb[0].dstAccessMask = vk::AccessFlagBits::eShaderRead;
1552 bmb[1].buffer = slot.indirect.buffer;
1553 bmb[1].size = VK_WHOLE_SIZE;
1554 bmb[1].srcAccessMask = vk::AccessFlagBits::eShaderWrite;
1555 bmb[1].dstAccessMask = vk::AccessFlagBits::eIndirectCommandRead;
1556 cb.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader,
1557 vk::PipelineStageFlagBits::eVertexShader |
1558 vk::PipelineStageFlagBits::eDrawIndirect,
1559 {}, 0, nullptr, 2, bmb, 0, nullptr);
1560 }
1561}
1562
1564 if (!gpuDrivenScenePassPending_) return;
1565 gpuDrivenScenePassPending_ = false;
1566 if (beginSceneColorRenderPass()) {
1567 ensureScenePassPipelines(activeScenePass(), activeSceneSamples());
1568 } else {
1569 ensureScenePassPipelines(renderpass, vk::SampleCountFlagBits::e1);
1570 beginSwapchainColorPass();
1571 }
1572 swapchainPassOpen = true;
1573}
1574
1575Graphics::GpuDrivenFrameSet0 Graphics::gpuDrivenFrameSet0() {
1576 // Per-frame set0 + UBO (same shading state as the stage-1 path).
1577 ensureFlatNormalTexture3D();
1578 ensureFlatHeightTexture3D();
1579 ensureDefaultEnvCubemap();
1580 Texture *tex = whiteTexture;
1581 auto *gpuTex = static_cast<GpuTexture *>(tex->gpuHandle);
1582 auto *gpuNormal = static_cast<GpuTexture *>(mesh3dNormalTexture ? mesh3dNormalTexture->gpuHandle
1583 : flatNormalTexture3D->gpuHandle);
1584 auto *gpuHeight = static_cast<GpuTexture *>(mesh3dHeightTexture ? mesh3dHeightTexture->gpuHandle
1585 : flatHeightTexture3D->gpuHandle);
1586 Texture *envTex = mesh3dEnvTexture ? mesh3dEnvTexture : defaultEnvCubemap;
1587 auto *gpuEnv = static_cast<GpuTexture *>(envTex->gpuHandle);
1588 auto *gpuDepth = static_cast<GpuTexture *>(whiteTexture->gpuHandle);
1589
1590 Mesh3DUBO ubo = mesh3dFrameUbo;
1591 ubo.model = glm::mat4(1.f);
1592 ubo.tint = glm::vec4(1.f);
1593 const int lightCount = std::max(0, std::min(mesh3dLighting.count, Lighting3DPack::kMaxLights));
1594 ubo.lightDir.w = float(lightCount);
1595 ubo.cameraPos.w = mesh3dRoughness;
1596 ubo.lightColor.w = mesh3dEnvIntensity;
1597 ubo.ambient = glm::vec4(glm::vec3(mesh3dLighting.ambient), mesh3dMetallic);
1598 for (int i = 0; i < lightCount; ++i) ubo.lights[i] = mesh3dLighting.lights[i];
1599 int dirI = -1;
1600 for (int i = 0; i < lightCount; ++i) {
1601 if (mesh3dLighting.lights[i].posRadius.w <= 0.f) {
1602 dirI = i;
1603 break;
1604 }
1605 }
1606 if (dirI >= 0) {
1607 glm::vec3 d(mesh3dLighting.lights[dirI].posRadius);
1608 if (glm::length(d) < 1e-6f) d = glm::vec3(0.f, 1.f, 0.f);
1609 else d = glm::normalize(d);
1610 ubo.lightDir = glm::vec4(d, float(lightCount));
1611 ubo.lightColor =
1612 glm::vec4(glm::vec3(mesh3dLighting.lights[dirI].color), mesh3dEnvIntensity);
1613 } else {
1614 ubo.lightDir = glm::vec4(0.f, 1.f, 0.f, float(lightCount));
1615 ubo.lightColor = glm::vec4(0.f, 0.f, 0.f, mesh3dEnvIntensity);
1616 }
1617 auto &fslots = currentMesh3dFrameSlots();
1618 if (fslots.drawIndex >= fslots.capacity) {
1619 std::fprintf(stderr, "[vulkan] mesh3d UBO ring exhausted; gpu-driven draw skipped\n");
1620 return {};
1621 }
1622 const size_t slot = fslots.drawIndex++;
1623 ensureMesh3dStrides();
1624 const uint32_t uboOffset = uint32_t(slot) * mesh3dUboStride;
1625 const uint32_t shadowOffset = uint32_t(slot) * shadowUboStride;
1626 updateRingLocal(fslots.uboRing, uboOffset, &ubo, sizeof(ubo));
1627 ShadowUBO shadowUbo = mesh3dShadows.ubo;
1628 if (!mesh3dShadows.active) {
1629 shadowUbo.bias.y = 0.f;
1630 shadowUbo.splits.w = 0.f;
1631 }
1632 shadowUbo.bias.z = mesh3dShadowReceive ? 1.f : 0.f;
1633 updateRingLocal(fslots.shadowRing, shadowOffset, &shadowUbo, sizeof(shadowUbo));
1634 vk::DescriptorSet set = mesh3dSetFor(gpuTex, gpuNormal, gpuEnv, gpuHeight, gpuDepth, fslots);
1635 return {set, uboOffset, shadowOffset};
1636}
1637
1639 if (!gpuDrivenCullReady_ || gpuDrivenBucketCount_ == 0) return;
1640 if (!swapchainPassOpen && !sceneColorPassOpen) return;
1641 auto &slot = currentGpuDrivenCullSlot();
1642
1643 const Graphics::GpuDrivenFrameSet0 s0 = gpuDrivenFrameSet0();
1644 if (!s0.set) return;
1645 const uint32_t dynOffsets[2] = {s0.uboOffset, s0.shadowOffset};
1646
1647 auto &cb = currentPresentCb();
1648 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, mesh3dGpuDrivenPipeline);
1649 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, mesh3dGpuDrivenPipelineLayout, 0, 1,
1650 &s0.set, 2, dynOffsets);
1651 const vk::DescriptorSet bindless = bindlessSetForFrame();
1652 if (!bindless) return;
1653 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, mesh3dGpuDrivenPipelineLayout, 1, 1,
1654 &bindless, 0, nullptr);
1655
1656 const vk::DeviceSize stride = sizeof(GpuIndirectCommand);
1657 GpuMesh *boundMesh = nullptr;
1658 for (uint32_t b = 0; b < gpuDrivenBucketCount_; ++b) {
1659 const uint32_t meshId = gpuDrivenBucketMeshIds_[b];
1660 if (meshId >= meshRecordOwners_.size()) continue;
1661 GpuMesh *mesh = meshRecordOwners_[meshId];
1662 if (mesh != boundMesh) {
1663 const vk::DeviceSize vbOffset = 0;
1664 cb.bindVertexBuffers(0, 1, mesh->vertices, &vbOffset);
1665 cb.bindIndexBuffer(mesh->indices.buffer, 0, mesh->indexType);
1666 boundMesh = mesh;
1667 }
1668 cb.drawIndexedIndirect(slot.indirect.buffer, vk::DeviceSize(b) * stride, 1, stride);
1669 }
1670}
1671
1673 if (!gpuDrivenCullReady_) return;
1674 if (gpuDrivenBucketCount_ == 0 && !vgAnyThisFrame_) return;
1675 // The vis attachments live in the GBuffer slot set; make sure it exists
1676 // even when the AO/gbuffer feature is off (resolve needs it anyway).
1677 createGBufferResources(gbufferWidth > 0 ? gbufferWidth : int(swapchain.extent.width),
1678 gbufferHeight > 0 ? gbufferHeight : int(swapchain.extent.height));
1679 auto *slot = currentGBufferSlot();
1680 if (!slot || !gbufferVisPipeline || !gbufferVisRenderPass || !slot->visFramebuffer) return;
1681 auto &cull = currentGpuDrivenCullSlot();
1682 auto &cb = currentPresentCb();
1683 // Stage 3 VG: cluster cull (frustum + HZB) runs right before the vis pass.
1684 recordVgCull();
1685
1686 const uint32_t w = uint32_t(gbufferWidth);
1687 const uint32_t h = uint32_t(gbufferHeight);
1688 std::array<vk::ClearValue, 6> clears{};
1689 clears[0].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
1690 clears[1].color = vk::ClearColorValue(std::array<float, 4>{1, 1, 1, 1});
1691 clears[2].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
1692 clears[3].color = vk::ClearColorValue(std::array<uint32_t, 4>{0xFFFFFFFFu, 0u, 0u, 0u});
1693 clears[4].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
1694 clears[5].depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
1695 vk::RenderPassBeginInfo rpBegin{};
1696 rpBegin.renderPass = gbufferVisRenderPass;
1697 rpBegin.framebuffer = slot->visFramebuffer;
1698 rpBegin.renderArea = vk::Rect2D{{0, 0}, {w, h}};
1699 rpBegin.clearValueCount = uint32_t(clears.size());
1700 rpBegin.pClearValues = clears.data();
1701 slot->normal.beginColorAttachment();
1702 slot->depthColor.beginColorAttachment();
1703 slot->albedo.beginColorAttachment();
1704 slot->visID.beginColorAttachment();
1705 slot->visBary.beginColorAttachment();
1706 slot->depth.beginDepthAttachment();
1707 cb.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
1708 setViewportAndScissor(cb, w, h);
1709
1710 const Graphics::GpuDrivenFrameSet0 s0 = gpuDrivenFrameSet0();
1711 const vk::DescriptorSet bindless = bindlessSetForFrame();
1712 if (s0.set && bindless) {
1713 const uint32_t dynOffsets[2] = {s0.uboOffset, s0.shadowOffset};
1714 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, gbufferVisPipeline);
1715 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, mesh3dGpuDrivenPipelineLayout, 0,
1716 1, &s0.set, 2, dynOffsets);
1717 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, mesh3dGpuDrivenPipelineLayout, 1,
1718 1, &bindless, 0, nullptr);
1719 const vk::DeviceSize stride = sizeof(glm::uvec4);
1720 for (uint32_t b = 0; b < gpuDrivenBucketCount_; ++b) {
1721 cb.drawIndirect(cull.indirectNI.buffer, vk::DeviceSize(b) * stride, 1, stride);
1722 }
1723 if (vgAnyThisFrame_) drawVgClusters(cb);
1724 }
1725 cb.endRenderPass();
1726 slot->normal.endSampledLayout();
1727 slot->depthColor.endSampledLayout();
1728 slot->albedo.endSampledLayout();
1729 slot->visID.endSampledLayout();
1730 slot->visBary.endSampledLayout();
1731 slot->depth.endSampledLayout();
1732}
1733
1735 if (!gpuDrivenCullReady_) return;
1736 if (gpuDrivenBucketCount_ == 0 && !vgAnyThisFrame_) return;
1737 if (!swapchainPassOpen && !sceneColorPassOpen) return;
1738 if (!resolveVisPipeline || !mesh3dGpuDrivenPipelineLayout) return;
1739 auto *slot = currentGBufferSlot();
1740 if (!slot || !slot->visIDGpu.sampler || !slot->visBaryGpu.sampler) return;
1741 auto &cb = currentPresentCb();
1742 const vk::DescriptorSet bindless = bindlessSetForFrame();
1743 if (!bindless) return;
1744
1745 const Graphics::GpuDrivenFrameSet0 s0 = gpuDrivenFrameSet0();
1746 if (!s0.set) return;
1747 const uint32_t dynOffsets[2] = {s0.uboOffset, s0.shadowOffset};
1748 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, resolveVisPipeline);
1749 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, mesh3dGpuDrivenPipelineLayout, 0, 1,
1750 &s0.set, 2, dynOffsets);
1751 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, mesh3dGpuDrivenPipelineLayout, 1, 1,
1752 &bindless, 0, nullptr);
1753 const uint32_t w =
1754 uint32_t(sceneColorPassOpen ? sceneColorWidth : int(swapchain.extent.width));
1755 const uint32_t h =
1756 uint32_t(sceneColorPassOpen ? sceneColorHeight : int(swapchain.extent.height));
1757 setViewportAndScissor(cb, w, h);
1758 cb.draw(3, 1, 0, 0);
1759}
1760
1761void Graphics::createResolveVisPipeline(const vkb::BuiltRenderPass &rp,
1762 vk::SampleCountFlagBits samples) {
1763 destroyPipeline(device, resolveVisPipeline);
1764 if (!mesh3dGpuDrivenPipelineLayout) return;
1765 std::vector<uint32_t> vert(resolve_vis_vert_spv, resolve_vis_vert_spv +
1766 resolve_vis_vert_spv_count);
1767 std::vector<uint32_t> frag(resolve_vis_frag_spv, resolve_vis_frag_spv +
1768 resolve_vis_frag_spv_count);
1769 vk::ShaderModule vertModule = vkb::PipelineBuilder::createShaderModule(device.instance, vert);
1770 vk::ShaderModule fragModule = vkb::PipelineBuilder::createShaderModule(device.instance, frag);
1771 resolveVisPipeline =
1772 device.createPipeline()
1773 .useClassicPipeline(vertModule, fragModule)
1774 .setPipelineLayout(mesh3dGpuDrivenPipelineLayout)
1775 .setDynamicStatesViewportScissor()
1776 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f,
1777 vk::CullModeFlagBits::eNone, vk::FrontFace::eClockwise)
1778 .setMultisampler(false, samples)
1779 .setDepthStencil(true, true, vk::CompareOp::eLessOrEqual)
1780 .setColorAttachmentCount(1)
1781 .build(rp);
1782 device->destroyShaderModule(vertModule);
1783 device->destroyShaderModule(fragModule);
1784}
1785
1786void Graphics::recordVgCull() {
1787 if (!gpuDrivenCullReady_ || vgAssetCount_ == 0 || vgClusterCount_ == 0) return;
1788 if (!vgCullPass_.pipeline() || !vgGpu_.visible.buffer) return;
1789 auto &cb = currentPresentCb();
1790 const vk::DescriptorSet bindless = bindlessSetForFrame();
1791 if (!bindless) return;
1792
1793 const size_t slot = currentFrameSlot() % kAsyncResourceCopies;
1794 vgLastVisible_ = uint32_t(slot);
1795
1796 // Reset this slot's visible counter (the slot's fence was waited at frame
1797 // begin, so the previous use of this slot's buffers has completed).
1798 {
1799 const vk::DeviceSize slotVis = (vk::DeviceSize(kMaxVgClusters) + 1) * sizeof(uint32_t);
1800 void *map = vgGpu_.visible.map();
1801 if (!map) return;
1802 std::memset(static_cast<char *>(map) + slotVis * slot, 0, sizeof(uint32_t));
1803 vgGpu_.visible.unmap();
1804 }
1805
1806 cb.bindDescriptorSets(vk::PipelineBindPoint::eCompute, gpuDrivenComputeLayout, 1, 1,
1807 &bindless, 0, nullptr);
1808 const uint32_t push[4]{vgClusterCount_, 0u, 0u, 0u};
1809 cb.pushConstants(gpuDrivenComputeLayout, vk::ShaderStageFlagBits::eCompute, 0, sizeof(push),
1810 push);
1811 vgCullPass_.record(cb, (vgClusterCount_ + 63u) / 64u);
1812
1813 // VG cull writes (visible + indirect) -> vertex read + indirect read.
1814 {
1815 vk::BufferMemoryBarrier bmb[2]{};
1816 bmb[0].buffer = vgGpu_.visible.buffer;
1817 bmb[0].size = VK_WHOLE_SIZE;
1818 bmb[0].srcAccessMask = vk::AccessFlagBits::eShaderWrite;
1819 bmb[0].dstAccessMask = vk::AccessFlagBits::eShaderRead;
1820 bmb[1].buffer = vgGpu_.indirect.buffer;
1821 bmb[1].size = VK_WHOLE_SIZE;
1822 bmb[1].srcAccessMask = vk::AccessFlagBits::eShaderWrite;
1823 bmb[1].dstAccessMask = vk::AccessFlagBits::eIndirectCommandRead;
1824 cb.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader,
1825 vk::PipelineStageFlagBits::eVertexShader |
1826 vk::PipelineStageFlagBits::eDrawIndirect,
1827 {}, 0, nullptr, 2, bmb, 0, nullptr);
1828 }
1829}
1830
1831void Graphics::drawVgClusters(vk::CommandBuffer cb) {
1832 if (!gbufferVgVisPipeline || vgAssetCount_ == 0 || vgClusterCount_ == 0) return;
1833 const vk::DescriptorSet bindless = bindlessSetForFrame();
1834 if (!bindless || !mesh3dGpuDrivenPipelineLayout) return;
1835 if (!gpuDrivenCaps_.drawIndirectCount) return;
1836
1837 const size_t slot = currentFrameSlot() % kAsyncResourceCopies;
1838 const vk::DeviceSize slotInd = vk::DeviceSize(kMaxVgClusters) * sizeof(glm::uvec4);
1839 const vk::DeviceSize slotVis = (vk::DeviceSize(kMaxVgClusters) + 1) * sizeof(uint32_t);
1840 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, gbufferVgVisPipeline);
1841 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, mesh3dGpuDrivenPipelineLayout, 1, 1,
1842 &bindless, 0, nullptr);
1843 cb.drawIndirectCount(vgGpu_.indirect.buffer, slotInd * slot, vgGpu_.visible.buffer,
1844 slotVis * slot, kMaxVgClusters, sizeof(glm::uvec4));
1845}
1846
1848 if (!gpuDrivenCullReady_ || gpuDrivenCullSlots_.empty()) return 0;
1849 auto &slot = gpuDrivenCullSlots_[gpuDrivenLastCullSlot_ % gpuDrivenCullSlots_.size()];
1850 uint32_t count = 0;
1851 void *map = slot.visibleFlags.map();
1852 if (!map) return 0;
1853 const uint32_t n = std::min(gpuDrivenCullInstanceCount_, kMaxGpuDrivenInstances);
1854 const auto *flags = static_cast<const uint32_t *>(map);
1855 for (uint32_t i = 0; i < n; ++i) count += flags[i] != 0 ? 1u : 0u;
1856 slot.visibleFlags.unmap();
1857 return count;
1858}
1859
1861 if (!gpuDrivenCullReady_ || gpuDrivenCullSlots_.empty()) return 0;
1862 auto &slot = gpuDrivenCullSlots_[gpuDrivenLastCullSlot_ % gpuDrivenCullSlots_.size()];
1863 uint32_t count = 0;
1864 void *map = slot.indirect.map();
1865 if (!map) return 0;
1866 const auto *cmds = static_cast<const GpuIndirectCommand *>(map);
1867 const uint32_t n = std::min(gpuDrivenBucketCount_, kMaxGpuDrivenBuckets);
1868 for (uint32_t b = 0; b < n; ++b) count += cmds[b].instanceCount > 0 ? 1u : 0u;
1869 slot.indirect.unmap();
1870 return count;
1871}
1872
1874 if (!tex || !tex->gpuHandle) return kInvalidBindlessSlot;
1875 return static_cast<GpuTexture *>(tex->gpuHandle)->bindlessIndex2D;
1876}
1877
1879 if (!mesh || !mesh->gpuHandle) return kInvalidBindlessSlot;
1880 return static_cast<GpuMesh *>(mesh->gpuHandle)->gpuRecordIndex;
1881}
1882
1883void Graphics::createMesh3DGpuDrivenPipeline() {
1884 if (mesh3dGpuDrivenPipeline) return;
1885 if (!mesh3dSetLayout || !bindlessSetLayout_ || !gpuDrivenCaps_.gpuDrivenAvailable()) return;
1886
1887 // Pipeline layout: set0 = per-frame (legacy mesh3d layout), set1 = bindless.
1888 // No push constants: instance indexing relies on gl_InstanceIndex, which
1889 // already includes the command's firstInstance per the Vulkan spec.
1890 std::array<vk::DescriptorSetLayout, 2> setLayouts{mesh3dSetLayout, bindlessSetLayout_};
1891 vk::PipelineLayoutCreateInfo pli{};
1892 pli.setLayoutCount = uint32_t(setLayouts.size());
1893 pli.pSetLayouts = setLayouts.data();
1894 mesh3dGpuDrivenPipelineLayout = device->createPipelineLayout(pli);
1895 mesh3dGpuDrivenPipeline =
1896 createMesh3DStylePipeline(embeddedSpirv(mesh3d_gpudriven_vert_spv),
1897 embeddedSpirv(mesh3d_gpudriven_frag_spv),
1898 mesh3dGpuDrivenPipelineLayout, renderpass,
1899 vk::SampleCountFlagBits::e1);
1900}
1901} // namespace eve::graphics::vulkan
#define EV_ASSERT(cond,...)
Assert an internal engine invariant (state that must always hold).
Definition Assert.h:37
std::vector< std::uint32_t > verts
Definition Builder.cpp:27
std::string type
vk::ShaderModule vert
vk::ShaderModule frag
float height
Definition Grass.cpp:235
float u
Definition Grass.cpp:234
glm::vec3 n
Definition Grass.cpp:64
int h
int w
uint32_t a
uint32_t b
Texture * normal
int width
int idx
glm::vec3 eye
glm::vec4 p[6]
float fovRad
Mesh * mesh
glm::mat4 viewProj
glm::mat4 model
Material * material
int d
float m[16]
uint32_t s
Definition Weather.cpp:28
std::unique_ptr< RenderControl > renderControl_
Definition Graphics.h:1126
CPU-side builder for GPU-driven indirect draws (stage 1).
uint32_t build(const std::vector< GpuMeshRecord > &meshTable)
Sort + merge. meshTable supplies indexCount/firstIndex/vertexBase.
void add(uint32_t seq, uint32_t meshId, uint32_t materialId, uint32_t pipelineId)
seq = monotonic instance sequence (stable order tiebreak).
const std::vector< GpuIndirectCommand > & commands() const
const std::vector< uint32_t > & sortedInstanceOrder() const
Instance indices in the sorted order the caller must upload.
Packages shading method + surface parameters into one attachable asset.
Definition Material.h:26
GPU mesh handle (+ optional CPU morph targets).
Definition Mesh.h:18
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:17
bool create(vkb::Device &device, vk::PipelineLayout layout, const std::vector< uint32_t > &spv)
Create from embedded SPIR-V words; layout must outlive the pass.
vk::Pipeline pipeline() const
Definition ComputePass.h:37
void record(vk::CommandBuffer cb, uint32_t groupsX, uint32_t groupsY=1, uint32_t groupsZ=1) const
Record one dispatch (local size baked into the shader).
Per-frame GPU allocation arena (host-visible coherent).
Definition FrameArena.h:18
bool gpuDrivenCullEnabled() const
Stage 2 cull is live for this frame (GPU-written commands).
Definition Graphics.h:252
uint32_t gpuDrivenMeshRecord(Mesh *mesh) override
GPU mesh-table slot for a mesh (kInvalidGpuDrivenSlot when not uploaded).
uint32_t gpuDrivenVgAssetId(Mesh *mesh) const override
VG asset id attached to a mesh (kInvalidGpuDrivenSlot when none).
uint32_t debugMeshRecordIndex(Mesh *mesh) const
bool gpuDrivenVgSetInstance(uint32_t vgAssetId, const glm::mat4 &model, uint32_t materialId) override
void gpuDrivenVgComputeSection(const glm::mat4 &viewProj, const glm::vec3 &eye, float fovYDeg, float nearZ, float farZ) override
Record the HZB build + cull-params section (VG-only frames).
bool gpuDrivenMaterialUsable(Material *material) override
Whether a material can be shaded by the GPU-driven opaque path. Backends/drivers with descriptor-inde...
void syncMaterialTable()
Upload all registered material records to the GPU table.
uint32_t debugGpuDrivenVgVisibleCount() const
Debug readback: visible VG clusters from the last cull.
uint32_t materialTableGetOrCreate(eve::graphics::Material *material)
Get (or lazily create) the GPU material-table slot for a material.
bool gpuDrivenCullBegin(const GpuInstance *instances, uint32_t instanceCount)
Upload sorted instances + bucket metadata for the cull chain.
void gpuDrivenResolve() override
Record the fullscreen vis resolve inside the open scene color pass.
bool gpuDrivenResolveWanted() const override
Stage 3: vis+resolve live for this frame (opt-in, 1x scene pass).
void gpuDrivenDrawOpaque()
Draw the opaque geometry with GPU-written indirect commands.
bool gpuDrivenEnabled() const override
Whether the GPU-driven opaque path is currently enabled.
Definition Graphics.h:234
uint32_t gpuDrivenVgUpload(const GpuVgAssetUpload &asset) override
Upload a virtual-geometry asset into the shared cluster pool.
void gpuDrivenOpenScenePass()
Open the scene color pass that begin3DFrame deferred (cull path).
uint32_t debugBindlessIndex(Texture *tex) const
Test/debug helpers (valid when the GPU-driven path is live).
uint32_t debugGpuDrivenVisibleCount() const
Debug readback: visible instances / non-empty buckets from the last cull.
void gpuDrivenRecordVisPass() override
Record the GBuffer vis pass (opaque indirect draws write visID/visBary).
bool gpuDrivenVgAttachToMesh(Mesh *mesh, uint32_t vgAssetId) override
bool gpuDrivenSubmitOpaque(const GpuInstance *instances, uint32_t instanceCount) override
Upload + record GPU-driven opaque draws (call inside the open 3D frame). The backend sorts instances ...
void gpuDrivenCullEmit(const glm::mat4 &viewProj, const glm::vec3 &eye, float fovYDeg, float nearZ, float farZ)
Record the cull + emit compute dispatches for the current frame.
FrameArena & currentFrameArena()
Per-frame arena for the current swapchain frame slot.
constexpr uint32_t kMaxGpuDrivenBuckets
Definition GpuDriven.h:24
eve::graphics::GpuIndirectCommand GpuIndirectCommand
Definition GpuDriven.h:18
constexpr uint32_t kMaxHzbMips
Definition GpuDriven.h:25
constexpr uint32_t kMaxBindlessCubemaps
Definition GpuDriven.h:20
constexpr uint32_t kMaxGpuDrivenInstances
Definition GpuDriven.h:23
constexpr uint32_t kInvalidBindlessSlot
Definition GpuDriven.h:22
constexpr uint32_t kHZBHeaderWords
Definition GpuDriven.h:26
constexpr uint32_t kMaxBindlessTextures
Definition GpuDriven.h:19
eve::graphics::GpuMeshRecord GpuMeshRecord
GPU-driven rendering shared constants + std430 GPU layouts.
Definition GpuDriven.h:15
eve::graphics::GpuMaterialRecord GpuMaterialRecord
Definition GpuDriven.h:16
eve::graphics::GpuInstance GpuInstance
Definition GpuDriven.h:17
Indirect draw command; layout identical to VkDrawIndexedIndirectCommand.
Per-instance GPU record (std430). Mirrors GLSL GpuInstance.
GPU material table record (std430). Mirrors GLSL GpuMaterialRecord.
Neutral GPU upload for one virtual-geometry asset. Raw arrays so the graphics module does not depend ...
const std::uint32_t * triangles
const GpuVgCluster * clusters
GPU-packed cluster node (std430, 4 x uvec4). Mirrors the virtualgeometry module's VgGpuCluster layout...
static constexpr int kMaxLights
Definition Light.h:91
Light3DGpu lights[kMaxLights]
Definition Light.h:93
bool gpuDrivenCullAvailable() const
Stage 2: GPU frustum/HZB cull + GPU-written indirect commands.
Definition GpuDriven.h:54
vkb::HostVertexBuffer vertices
Definition Graphics.h:155
uint32_t gpuRecordIndex
Index into the GPU mesh table (GpuMeshRecord).
Definition Graphics.h:168
GpuMeshRecord record
CPU-side record for this mesh (bounds/ranges); uploaded by registerMeshRecord.
Definition Graphics.h:170
Light3DGpu lights[Lighting3DPack::kMaxLights]
Definition Graphics.h:98