载入中...
搜索中...
未找到
Graphics3D.cpp
浏览该文件的文档.
1// Vulkan backend implementation — 3D frame, shadow and G-buffer passes.
2//
3// Re-split from the merged dev single-TU Graphics.cpp (pure move;
4// dev changes preserved). Shared helpers live in GraphicsInternal.h.
5
8#include "graphics/Light.h"
12#include "graphics/Outline.h"
14
15#include <SDL2/SDL.h>
16#include <SDL2/SDL_vulkan.h>
17
18#include <algorithm>
19#include <array>
20#include <cmath>
21#include <cstdio>
22#include <cstdlib>
23#include <cstring>
24#include <functional>
25#include <stdexcept>
26#include <string>
27#include <vector>
28#if !defined(_WIN32)
29#include <unistd.h>
30#endif
31
32#include "common/Exception.h"
34#include "common/config.h"
36#include "image/Image.h"
37#include "image/ImageData.h"
38#include "zeroerr/assert.h"
39
40#include <memory>
41
42
43#include <assimp/mesh.h>
44#include <assimp/matrix3x3.h>
45#include <assimp/matrix4x4.h>
46#include <assimp/vector3.h>
47#include <glm/gtc/matrix_transform.hpp>
48
49#include "graphics/shaders/mesh3d_vert_spv.inc"
50#include "graphics/shaders/mesh3d_frag_spv.inc"
51#include "graphics/shaders/voxel_rect_vert_spv.inc"
52#include "graphics/shaders/voxel_rect_frag_spv.inc"
55#include "thread/Thread.h"
56
57namespace eve::graphics::vulkan {
58
60 ASSERT(initialized);
61 if (!initialized) throw Exception("begin3DFrame: graphics not initialized");
62 if (isCanvasActive()) throw Exception("begin3DFrame: cannot start 3D while a Canvas is active");
63 if (swapchainPassOpen) {
64 // Previous eve_render opened 3D then threw before present().
65 try {
66 flushToSwapchain();
67 } catch (...) {
68 abortOpen3DFrame();
69 }
70 }
71 // Soft-fail like flushToSwapchain: on Android/iOS the surface may still be
72 // settling after orientation change; throwing would abort the whole script.
73 if (!beginPresentCommandBuffer())
74 return;
75 recordDeferredFrameGraph();
76
77 // 3D pass clears with backgroundColor (setBackgroundColor), not a stale 2D clear.
78 clearColor = backgroundColor;
79 createSceneColorResources(int(swapchain.extent.width), int(swapchain.extent.height));
80 if (beginSceneColorRenderPass()) {
81 // Rebuild scene-pass pipelines to match the active scene pass (MSAA).
82 // Must happen AFTER the pass opens: if the MSAA scene pass is unavailable
83 // we fall back to the swapchain (e1) render pass below, and rasterizing
84 // with an Nx pipeline into an e1 pass is UB that can hang the GPU (TDR).
85 ensureScenePassPipelines(activeScenePass(), activeSceneSamples());
86 } else {
87 // Scene pass unavailable — draw 3D straight into the swapchain. Scene-pass
88 // pipelines must be 1x / swapchain-compatible to avoid the samples mismatch.
89 ensureScenePassPipelines(renderpass, vk::SampleCountFlagBits::e1);
90 beginSwapchainColorPass();
91 }
92
93 auto &cb = currentPresentCb();
94 setViewportAndScissor(cb, swapchain.extent.width, swapchain.extent.height);
95
96 {
97 auto &fslots = currentMesh3dFrameSlots();
98 fslots.lastDrawCount = fslots.drawIndex;
99 fslots.drawIndex = 0;
100 ensureMesh3dRing(fslots);
101 auto &cslots = currentMesh3dClusteredFrameSlots();
102 cslots.lastDrawCount = cslots.drawIndex;
103 cslots.drawIndex = 0;
104 ensureMesh3dClusteredRing(cslots);
105 }
106 lastMesh3dPipeline = nullptr;
107 lastMesh3dClusteredPipeline = nullptr;
108 currentVoxelInstanceFrame().drawIndex = 0;
109 swapchainPassOpen = true;
110 frameHad3D = true;
111 hasPendingClear = false;
112}
113
114void Graphics::ensureOffscreen3DResources() {
115 auto &device = getDevice();
116 if (!offscreen3DRenderPass) {
117 offscreen3DRenderPass =
118 device.createRenderPass()
119 .addSampledColorAttachment(vk::Format::eR8G8B8A8Unorm)
120 .addDepthAttachment(depthFormat, vk::AttachmentLoadOp::eClear,
121 vk::AttachmentStoreOp::eDontCare)
122 .addSubpass(vkb::SubpassBuilder()
123 .addAttachmentRef(0, vk::ImageLayout::eColorAttachmentOptimal)
124 .setDepthStencilAttachment(
125 1, vk::ImageLayout::eDepthStencilAttachmentOptimal))
126 .addExternalShaderReadDependencies()
127 .build();
128 offscreen3DMeshPipeline = createMesh3DStylePipeline(
129 embeddedSpirv(mesh3d_vert_spv), embeddedSpirv(mesh3d_frag_spv), mesh3dPipelineLayout,
130 offscreen3DRenderPass, vk::SampleCountFlagBits::e1);
131 }
132 if (!offscreen3DPool) {
133 vk::CommandPoolCreateInfo poolInfo{};
134 poolInfo.flags = vk::CommandPoolCreateFlagBits::eTransient;
135 offscreen3DPool = device->createCommandPool(poolInfo);
136 vk::CommandBufferAllocateInfo allocInfo{};
137 allocInfo.commandPool = offscreen3DPool;
138 allocInfo.level = vk::CommandBufferLevel::ePrimary;
139 allocInfo.commandBufferCount = 1;
140 auto bufs = device->allocateCommandBuffers(allocInfo);
141 offscreen3DCB = bufs[0];
142 vk::FenceCreateInfo fenceInfo{};
143 fenceInfo.flags = vk::FenceCreateFlagBits::eSignaled; // first wait passes immediately
144 offscreen3DFence = device->createFence(fenceInfo);
145 }
146}
147
148void Graphics::destroyOffscreen3DResources() {
149 auto &device = getDevice();
150 if (offscreen3DFence) {
151 device->destroyFence(offscreen3DFence);
152 offscreen3DFence = nullptr;
153 }
154 if (offscreen3DPool) {
155 device->destroyCommandPool(offscreen3DPool);
156 offscreen3DPool = nullptr;
157 }
158 offscreen3DCB = nullptr;
159 if (offscreen3DMeshPipeline) {
160 device->destroyPipeline(offscreen3DMeshPipeline);
161 offscreen3DMeshPipeline = nullptr;
162 }
163 if (offscreen3DRenderPass) {
164 device->destroyRenderPass(offscreen3DRenderPass);
165 offscreen3DRenderPass = {};
166 }
167}
168
170 ASSERT(initialized);
171 if (!initialized) throw Exception("begin3DFrameToCanvas: graphics not initialized");
172 if (!canvas) throw Exception("begin3DFrameToCanvas: null canvas");
173 auto *oc = dynamic_cast<OffscreenCanvas *>(canvas);
174 if (!oc) throw Exception("begin3DFrameToCanvas: not an offscreen canvas");
175 if (offscreen3DPassOpen) throw Exception("begin3DFrameToCanvas: already open");
176 if (swapchainPassOpen) {
177 try {
178 flushToSwapchain();
179 } catch (...) {
180 abortOpen3DFrame();
181 }
182 }
183 clearColor = backgroundColor;
184 ensureOffscreen3DResources();
185 oc->ensure3D();
186 offscreen3DCanvas = oc;
187
188 if (offscreen3DFence) (void)device->waitForFences(1, &offscreen3DFence, VK_TRUE, UINT64_MAX);
189 vk::CommandBufferBeginInfo beginInfo{};
190 offscreen3DCB.begin(beginInfo);
191 std::array<vk::ClearValue, 2> clears{};
192 clears[0].color =
193 vk::ClearColorValue(std::array<float, 4>{clearColor.r, clearColor.g, clearColor.b, 1.f});
194 clears[1].depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
195 vk::RenderPassBeginInfo rpBegin{};
196 rpBegin.renderPass = offscreen3DRenderPass;
197 rpBegin.framebuffer = oc->framebuffer3D();
198 rpBegin.renderArea =
199 vk::Rect2D{{0, 0}, {uint32_t(oc->getWidth()), uint32_t(oc->getHeight())}};
200 rpBegin.clearValueCount = uint32_t(clears.size());
201 rpBegin.pClearValues = clears.data();
202 oc->colorImage().beginColorAttachment();
203 oc->depthImage().beginDepthAttachment();
204 offscreen3DCB.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
205 setViewportAndScissor(offscreen3DCB, oc->getWidth(), oc->getHeight());
206
207 {
208 auto &fslots = currentMesh3dFrameSlots();
209 fslots.lastDrawCount = fslots.drawIndex;
210 fslots.drawIndex = 0;
211 ensureMesh3dRing(fslots);
212 auto &cslots = currentMesh3dClusteredFrameSlots();
213 cslots.lastDrawCount = cslots.drawIndex;
214 cslots.drawIndex = 0;
215 ensureMesh3dClusteredRing(cslots);
216 }
217 lastMesh3dPipeline = nullptr;
218 lastMesh3dClusteredPipeline = nullptr;
219 offscreen3DPassOpen = true;
220 frameHad3D = true;
221 hasPendingClear = false;
222}
223
225 if (!offscreen3DPassOpen || !offscreen3DCB) return;
226 offscreen3DCB.endRenderPass();
227 if (offscreen3DCanvas) {
228 offscreen3DCanvas->colorImage().setLayout(offscreen3DCB,
229 vk::ImageLayout::eShaderReadOnlyOptimal);
230 }
231 offscreen3DCB.end();
232
233 // Submit the offscreen render directly to the graphics queue (no present),
234 // then wait for it so it is fully drained before any subsequent render or
235 // teardown.
236 vk::SubmitInfo submitInfo{};
237 submitInfo.commandBufferCount = 1;
238 submitInfo.pCommandBuffers = &offscreen3DCB;
239 if (offscreen3DFence) (void)device->resetFences(1, &offscreen3DFence);
240 (void)device.getQueue(vkb::QueueType::graphics).submit(1, &submitInfo, offscreen3DFence);
241 if (offscreen3DFence)
242 (void)device->waitForFences(1, &offscreen3DFence, VK_TRUE, UINT64_MAX);
243
244 offscreen3DPassOpen = false;
245 offscreen3DCanvas = nullptr;
246}
247
248void Graphics::setMesh3DLight(const glm::vec3 &dir, const glm::vec3 &color) {
249 glm::vec3 d = glm::normalize(dir);
250 if (glm::length(d) < 1e-6f) d = glm::vec3(0.f, 1.f, 0.f);
251 mesh3dFrameUbo.lightDir = glm::vec4(d, mesh3dFrameUbo.lightDir.w);
252 // Preserve .w (envIntensity) — filled per-draw in drawMeshShader.
253 mesh3dFrameUbo.lightColor = glm::vec4(color, mesh3dFrameUbo.lightColor.w);
254}
255
256void Graphics::setMesh3DCameraPos(const glm::vec3 &eye) {
257 mesh3dFrameUbo.cameraPos = glm::vec4(eye, mesh3dFrameUbo.cameraPos.w);
258}
259
260void Graphics::destroyVoxelRectResources() {
261 for (auto &frame : voxelInstanceFrames) {
262 for (auto &slot : frame.slots) {
263 slot.buffer.release();
264 slot.capacityBytes = 0;
265 }
266 frame.slots.clear();
267 frame.drawIndex = 0;
268 }
269 voxelInstanceFrames.clear();
270 voxelRectSets.clear();
271 auto destroyBuf = [&](vkb::GenericBuffer &b) { b.release(); };
272 if (voxelUnitQuadReady) {
273 destroyBuf(voxelUnitQuadVerts);
274 destroyBuf(voxelUnitQuadIndices);
275 voxelUnitQuadReady = false;
276 }
277 if (voxelRectPipeline) {
278 device->destroyPipeline(voxelRectPipeline);
279 voxelRectPipeline = vk::Pipeline{};
280 }
281 if (voxelRectPipelineLayout) {
282 device->destroyPipelineLayout(voxelRectPipelineLayout);
283 voxelRectPipelineLayout = vk::PipelineLayout{};
284 }
285 voxelRectSetLayoutUnique.reset();
286 voxelRectSetLayout = vk::DescriptorSetLayout{};
287}
288
289void Graphics::createVoxelRectPipeline() {
290 if (voxelRectPipeline) return;
291
292 if (!voxelRectPipelineLayout) {
293 vkb::DescriptorSetLayoutBuilder layoutBuilder;
294 voxelRectSetLayoutUnique =
295 layoutBuilder
296 .image(0, vk::DescriptorType::eCombinedImageSampler,
297 vk::ShaderStageFlagBits::eFragment, 1)
298 .createUnique(device.instance);
299 voxelRectSetLayout = *voxelRectSetLayoutUnique;
300
301 const auto pcr = pushConstantRange(vk::ShaderStageFlagBits::eVertex, sizeof(VoxelRectPC));
302 voxelRectPipelineLayout = createPipelineLayout(device, voxelRectSetLayout, &pcr);
303 }
304
305 voxelRectPipeline = buildVoxelRectPipeline(renderpass, vk::SampleCountFlagBits::e1);
306 ensureVoxelUnitQuad();
307}
308
309vk::Pipeline Graphics::buildVoxelRectPipeline(const vkb::BuiltRenderPass &rp,
310 vk::SampleCountFlagBits samples) {
311 auto vert = embeddedSpirv(voxel_rect_vert_spv);
312 auto frag = embeddedSpirv(voxel_rect_frag_spv);
313 ShaderModulePair modules(device, vert, frag);
314
315 vk::VertexInputBindingDescription bindings[3]{};
316 bindings[0].binding = 0;
317 bindings[0].stride = sizeof(glm::vec2);
318 bindings[0].inputRate = vk::VertexInputRate::eVertex;
319 bindings[1].binding = 1;
320 bindings[1].stride = sizeof(uint32_t);
321 bindings[1].inputRate = vk::VertexInputRate::eInstance;
322 bindings[2].binding = 2;
323 bindings[2].stride = sizeof(uint32_t);
324 bindings[2].inputRate = vk::VertexInputRate::eInstance;
325
326 vk::VertexInputAttributeDescription attrs[3]{};
327 attrs[0].location = 0;
328 attrs[0].binding = 0;
329 attrs[0].format = vk::Format::eR32G32Sfloat;
330 attrs[0].offset = 0;
331 attrs[1].location = 1;
332 attrs[1].binding = 1;
333 attrs[1].format = vk::Format::eR32Uint;
334 attrs[1].offset = 0;
335 attrs[2].location = 2;
336 attrs[2].binding = 2;
337 attrs[2].format = vk::Format::eR32Uint;
338 attrs[2].offset = 0;
339
340 vk::PipelineVertexInputStateCreateInfo vi{};
341 vi.vertexBindingDescriptionCount = 3;
342 vi.pVertexBindingDescriptions = bindings;
343 vi.vertexAttributeDescriptionCount = 3;
344 vi.pVertexAttributeDescriptions = attrs;
345
346 // Unit-quad indices 0-2-1 / 0-3-2 are object-space CCW for outward faces.
347 // perspectiveVulkanRH_ZO flips clip Y, so those faces are CCW in the
348 // framebuffer — CounterClockwise frontFace keeps them. Clockwise + Back
349 // (the mesh-pipeline convention) culls every submitted face after the Y
350 // flip; CPU 6-dir cull already dropped the true back faces, so the frame
351 // collapses to a 1px silhouette.
352 return device.createPipeline()
353 .useClassicPipeline(modules.vert, modules.frag)
354 .setPipelineLayout(voxelRectPipelineLayout)
355 .setVertexInputState(vi)
356 .setDynamicStatesViewportScissor()
357 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f, vk::CullModeFlagBits::eBack,
358 vk::FrontFace::eCounterClockwise)
359 .setMultisampler(false, samples)
360 .setDepthStencil(true, true, vk::CompareOp::eLess)
361 .setColorAttachmentCount(1)
362 .build(rp);
363}
364
365void Graphics::ensureVoxelUnitQuad() {
366 if (voxelUnitQuadReady) return;
367 const float corners[8] = {0.f, 0.f, 1.f, 0.f, 1.f, 1.f, 0.f, 1.f};
368 voxelUnitQuadVerts.allocate(frameToken(), device, vk::BufferUsageFlagBits::eVertexBuffer, sizeof(corners),
369 vk::MemoryPropertyFlagBits::eHostVisible |
370 vk::MemoryPropertyFlagBits::eHostCoherent);
371 voxelUnitQuadVerts.updateLocal(frameToken(), corners, sizeof(corners));
372 const uint32_t indices[6] = {0, 2, 1, 0, 3, 2};
373 voxelUnitQuadIndices.allocate(frameToken(), device, vk::BufferUsageFlagBits::eIndexBuffer, sizeof(indices),
374 vk::MemoryPropertyFlagBits::eHostVisible |
375 vk::MemoryPropertyFlagBits::eHostCoherent);
376 voxelUnitQuadIndices.updateLocal(frameToken(), indices, sizeof(indices));
377 voxelUnitQuadReady = true;
378}
379
380vkb::BoundSet Graphics::voxelRectSetFor(GpuTexture *gpuTex) {
381 ASSERT(gpuTex != nullptr);
382 auto it = voxelRectSets.find(gpuTex);
383 if (it != voxelRectSets.end()) return it->second;
384
385 vk::DescriptorSetAllocateInfo alloc{};
386 alloc.descriptorPool = descriptorPool;
387 alloc.descriptorSetCount = 1;
388 alloc.pSetLayouts = &voxelRectSetLayout;
389 vkb::UnboundSet unbound{device->allocateDescriptorSets(alloc).front()};
390
391 vkb::DescriptorSetUpdater updater(4, 4, 0);
392 updater.beginDescriptorSet(unbound)
393 .beginImages(0, 0, vk::DescriptorType::eCombinedImageSampler)
394 .image(vkb::SampledImage::forLaterSample(gpuTex->sampler, gpuTex->imageView()))
395 .update(device.instance);
396
397 vkb::BoundSet bound = std::move(unbound).publish();
398 voxelRectSets.emplace(gpuTex, bound);
399 return bound;
400}
401
402void Graphics::drawVoxelFaceInstances(const uint32_t *packed, int count, float originX,
403 float originY, float originZ, const std::string &faceDir,
404 Texture *atlas, int tilesPerRow, const uint32_t *ao) {
405 ASSERT(initialized);
406 if (!initialized) throw Exception("drawVoxelFaceInstances: graphics not initialized");
407 if (!swapchainPassOpen) throw Exception("drawVoxelFaceInstances: call begin3DFrame first");
408 if (!voxelRectPipeline) createVoxelRectPipeline();
409 if (!packed || count <= 0) return;
410
411 int face = -1;
412 if (faceDir == "posX" || faceDir == "+x")
413 face = 0;
414 else if (faceDir == "negX" || faceDir == "-x")
415 face = 1;
416 else if (faceDir == "posY" || faceDir == "+y")
417 face = 2;
418 else if (faceDir == "negY" || faceDir == "-y")
419 face = 3;
420 else if (faceDir == "posZ" || faceDir == "+z")
421 face = 4;
422 else if (faceDir == "negZ" || faceDir == "-z")
423 face = 5;
424 else
425 throw Exception("drawVoxelFaceInstances: unknown faceDir '%s'", faceDir.c_str());
426
427 Texture *tex = atlas ? atlas : whiteTexture;
428 if (!tex || !tex->gpuHandle) throw Exception("drawVoxelFaceInstances: missing texture");
429 auto *gpuTex = static_cast<GpuTexture *>(tex->gpuHandle);
430
431 ensureVoxelUnitQuad();
432
433 const vk::DeviceSize bytes = vk::DeviceSize(count) * sizeof(uint32_t);
434 auto &vframe = currentVoxelInstanceFrame();
435 const size_t slotIndex = vframe.drawIndex++;
436 while (vframe.slots.size() <= slotIndex) vframe.slots.emplace_back();
437 auto &slot = vframe.slots[slotIndex];
438 if (slot.capacityBytes < size_t(bytes) || !slot.buffer.buffer) {
439 const size_t alloc = std::max(size_t(bytes), slot.capacityBytes ? slot.capacityBytes * 2 : size_t(bytes));
440 slot.buffer.allocate(frameToken(), device, vk::BufferUsageFlagBits::eVertexBuffer, vk::DeviceSize(alloc),
441 vk::MemoryPropertyFlagBits::eHostVisible |
442 vk::MemoryPropertyFlagBits::eHostCoherent);
443 slot.capacityBytes = alloc;
444 }
445 slot.buffer.updateLocal(frameToken(), packed, size_t(bytes));
446
447 // Ambient-occlusion words (null → full bright). Kept parallel to packed.
448 const uint32_t defaultAO = 0xFFu; // all four corners AO=3
449 std::vector<uint32_t> aoDefaults;
450 if (!ao) {
451 aoDefaults.assign(size_t(count), defaultAO);
452 ao = aoDefaults.data();
453 }
454 const vk::DeviceSize aoBytes = vk::DeviceSize(count) * sizeof(uint32_t);
455 if (slot.aoCapacityBytes < size_t(aoBytes) || !slot.aoBuffer.buffer) {
456 const size_t alloc = std::max(size_t(aoBytes),
457 slot.aoCapacityBytes ? slot.aoCapacityBytes * 2
458 : size_t(aoBytes));
459 slot.aoBuffer.allocate(frameToken(), device, vk::BufferUsageFlagBits::eVertexBuffer,
460 vk::DeviceSize(alloc),
461 vk::MemoryPropertyFlagBits::eHostVisible |
462 vk::MemoryPropertyFlagBits::eHostCoherent);
463 slot.aoCapacityBytes = alloc;
464 }
465 slot.aoBuffer.updateLocal(frameToken(), ao, size_t(aoBytes));
466
467 VoxelRectPC pc{};
468 pc.viewProj = mesh3dFrameUbo.mvp;
469 pc.chunkOrigin = glm::vec4(originX, originY, originZ, float(face));
470 pc.atlasInfo = glm::vec4(float(std::max(1, tilesPerRow)), 0.f, 0.f, 0.f);
471 pc.tint = mesh3dFrameUbo.tint;
472
473 auto &cb = currentPresentCb();
474 vk::DescriptorSet set = voxelRectSetFor(gpuTex);
475 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, voxelRectPipeline);
476 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, voxelRectPipelineLayout, 0, 1, &set, 0,
477 nullptr);
478 cb.pushConstants(voxelRectPipelineLayout, vk::ShaderStageFlagBits::eVertex, 0, sizeof(VoxelRectPC),
479 &pc);
480
481 vk::Buffer vbufs[3] = {voxelUnitQuadVerts.buffer, slot.buffer.buffer, slot.aoBuffer.buffer};
482 vk::DeviceSize offsets[3] = {0, 0, 0};
483 cb.bindVertexBuffers(0, 3, vbufs, offsets);
484 cb.bindIndexBuffer(voxelUnitQuadIndices.buffer, 0, vk::IndexType::eUint32);
485 cb.drawIndexed(6, uint32_t(count), 0, 0, 0);
486}
487
488void Graphics::setMesh3DNormalTexture(Texture *normal) { mesh3dNormalTexture = normal; }
489
490void Graphics::setMesh3DHeightTexture(Texture *height) { mesh3dHeightTexture = height; }
491
492void Graphics::setMesh3DSceneDepth(Texture *depth) { mesh3dSceneDepthTexture = depth; }
493
494void Graphics::setMesh3DEnv(Texture *cube, float intensity) {
495 mesh3dEnvTexture = cube;
496 mesh3dEnvIntensity = intensity < 0.f ? 0.f : intensity;
497 if (!cube) mesh3dEnvIntensity = 0.f;
498}
499
500void Graphics::setMesh3DShadows(const ShadowUpload &upload) { mesh3dShadows = upload; }
501
502void Graphics::setMesh3DShadowReceive(bool receive) { mesh3dShadowReceive = receive; }
503
504void Graphics::beginShadowPass(int cascadeIndex) {
505 ASSERT(initialized);
506 if (!shadowPipeline) createShadowResources();
507 if (cascadeIndex < 0 || cascadeIndex >= ShadowConfig::kCascades) {
508 throw Exception("beginShadowPass: cascadeIndex out of range");
509 }
510 shadowPassCascade = cascadeIndex;
511 shadowPassDraws.clear();
512}
513
514void Graphics::drawMeshShadow(Mesh *mesh, const glm::mat4 &lightMVP) {
515 if (shadowPassCascade < 0) throw Exception("drawMeshShadow: call beginShadowPass first");
516 if (!mesh || !mesh->gpuHandle) throw Exception("drawMeshShadow: null mesh");
517 shadowPassDraws.push_back(ShadowDraw{mesh, lightMVP});
518}
519
520void Graphics::drawMeshShadowAlpha(Mesh *mesh, const glm::mat4 &lightMVP, Texture *albedo) {
521 if (shadowPassCascade < 0) throw Exception("drawMeshShadowAlpha: call beginShadowPass first");
522 if (!mesh || !mesh->gpuHandle) throw Exception("drawMeshShadowAlpha: null mesh");
523 ShadowDraw d;
524 d.mesh = mesh;
525 d.mvp = lightMVP;
526 d.albedo = albedo;
527 d.alphaTest = true;
528 shadowPassDraws.push_back(d);
529}
530
532 if (shadowPassCascade < 0) throw Exception("endShadowPass: no active shadow pass");
533 if (!shadowPipeline || !shadowRenderPass) {
534 shadowPassCascade = -1;
535 shadowPassDraws.clear();
536 return;
537 }
538 const int cascade = shadowPassCascade;
539 shadowCascadeDraws[cascade] = std::move(shadowPassDraws);
540 shadowPassDraws.clear();
541 shadowPassCascade = -1;
542 shadowPendingMask |= (1u << cascade);
543 // GPU work is recorded into the swapchain command buffer in
544 // beginSwapchainRenderPass() so it shares the frame's submit and uses the
545 // ping-pong copy for this frame slot.
546}
547
549 ASSERT(initialized);
550 if (!initialized) throw Exception("beginGBufferPass: graphics not initialized");
551 if (width <= 0 || height <= 0) throw Exception("beginGBufferPass: invalid size");
552 if (!texSetLayout || !descriptorPool)
553 throw Exception("beginGBufferPass: textured descriptor layout not ready");
554 createGBufferResources(width, height);
555 gbufferPassActive = true;
556 gbufferPassDraws.clear();
557}
558
559void Graphics::drawMeshGBuffer(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model, float nearZ,
560 float farZ, Texture *albedo, float tintR, float tintG, float tintB) {
561 if (!gbufferPassActive) throw Exception("drawMeshGBuffer: call beginGBufferPass first");
562 if (!mesh || !mesh->gpuHandle) throw Exception("drawMeshGBuffer: null mesh");
563 GBufferDraw d{};
564 d.mesh = mesh;
565 d.albedo = albedo;
566 d.push.mvp = mvp;
567 d.push.modelR0 = glm::vec4(model[0][0], model[1][0], model[2][0], model[3][0]);
568 d.push.modelR1 = glm::vec4(model[0][1], model[1][1], model[2][1], model[3][1]);
569 d.push.modelR2 = glm::vec4(model[0][2], model[1][2], model[2][2], model[3][2]);
570 auto u8 = [](float x) -> uint32_t {
571 return uint32_t(std::lround(std::clamp(x, 0.f, 1.f) * 255.f));
572 };
573 const uint32_t packedTint = u8(tintR) | (u8(tintG) << 8) | (u8(tintB) << 16) | (255u << 24);
574 d.push.clip = glm::vec4(nearZ, farZ, glm::uintBitsToFloat(packedTint), 0.f);
575 gbufferPassDraws.push_back(d);
576}
577
578void Graphics::drawMeshGBufferAlpha(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model,
579 float nearZ, float farZ, Texture *albedo, float tintR,
580 float tintG, float tintB) {
581 if (!gbufferPassActive) throw Exception("drawMeshGBufferAlpha: call beginGBufferPass first");
582 if (!mesh || !mesh->gpuHandle) throw Exception("drawMeshGBufferAlpha: null mesh");
583 GBufferDraw d{};
584 d.mesh = mesh;
585 d.albedo = albedo;
586 d.alphaTest = true;
587 d.push.mvp = mvp;
588 d.push.modelR0 = glm::vec4(model[0][0], model[1][0], model[2][0], model[3][0]);
589 d.push.modelR1 = glm::vec4(model[0][1], model[1][1], model[2][1], model[3][1]);
590 d.push.modelR2 = glm::vec4(model[0][2], model[1][2], model[2][2], model[3][2]);
591 auto u8 = [](float x) -> uint32_t {
592 return uint32_t(std::lround(std::clamp(x, 0.f, 1.f) * 255.f));
593 };
594 const uint32_t packedTint = u8(tintR) | (u8(tintG) << 8) | (u8(tintB) << 16) | (255u << 24);
595 d.push.clip = glm::vec4(nearZ, farZ, glm::uintBitsToFloat(packedTint), 0.f);
596 gbufferPassDraws.push_back(d);
597}
598
600 if (!gbufferPassActive) throw Exception("endGBufferPass: no active gbuffer pass");
601 gbufferPassActive = false;
602
603 auto *slot = currentGBufferSlot();
604 if (!gbufferPipeline || !gbufferRenderPass || !slot || !slot->framebuffer) {
605 gbufferPassDraws.clear();
606 gbufferPending = false;
607 if (renderControl_) renderControl_->getGBuffer()->clear();
608 return;
609 }
610
611 gbufferPending = true;
612 if (renderControl_) {
613 Texture *albedo = &slot->albedoTex;
614 renderControl_->getGBuffer()->setTargets(gbufferWidth, gbufferHeight, &slot->depthColorTex,
615 &slot->normalTex, albedo, &slot->depthTex);
616 }
617}
618
619void Graphics::ensureDefaultEnvCubemap() {
620 if (defaultEnvCubemap) return;
621 const uint8_t black[4] = {0, 0, 0, 255};
622 std::vector<uint8_t> faces(6u * 4u);
623 for (int i = 0; i < 6; ++i) {
624 faces[size_t(i) * 4u + 0] = black[0];
625 faces[size_t(i) * 4u + 1] = black[1];
626 faces[size_t(i) * 4u + 2] = black[2];
627 faces[size_t(i) * 4u + 3] = black[3];
628 }
629 defaultEnvCubemap = newCubemap(1, faces.data());
630}
631
633 mesh3dMetallic = metallic;
634 mesh3dRoughness = roughness;
635 mesh3dFrameUbo.ambient.w = metallic;
636 mesh3dFrameUbo.cameraPos.w = roughness;
637}
638
639void Graphics::setMesh3DTexCellBomb(float cellScale, float strength, float rotAmount) {
640 mesh3dTexBombScale = cellScale > 1e-3f ? cellScale : 1e-3f;
641 mesh3dTexBombStrength = strength < 0.f ? 0.f : (strength > 1.f ? 1.f : strength);
642 mesh3dTexBombRot = rotAmount < 0.f ? 0.f : (rotAmount > 1.f ? 1.f : rotAmount);
643 mesh3dFrameUbo.texBomb =
644 glm::vec4(mesh3dTexBombScale, mesh3dTexBombStrength, mesh3dTexBombRot, 0.f);
645}
646
647void Graphics::setMesh3DParallax(float scale, float minLayers, float maxLayers) {
648 mesh3dParallaxScale = scale < 0.f ? 0.f : (scale > 0.25f ? 0.25f : scale);
649 float minL = minLayers < 1.f ? 1.f : minLayers;
650 float maxL = maxLayers < minL ? minL : maxLayers;
651 if (maxL > 64.f) maxL = 64.f;
652 mesh3dParallaxMinLayers = minL;
653 mesh3dParallaxMaxLayers = maxL;
654 mesh3dFrameUbo.parallax =
655 glm::vec4(mesh3dParallaxScale, mesh3dParallaxMinLayers, mesh3dParallaxMaxLayers, 0.f);
656}
657
659 mesh3dLighting = pack;
660 mesh3dFrameUbo.ambient = glm::vec4(glm::vec3(pack.ambient), mesh3dMetallic); const int n = std::max(0, std::min(pack.count, Lighting3DPack::kMaxLights));
661 mesh3dFrameUbo.lightDir.w = float(n);
662 int dirI = -1;
663 for (int i = 0; i < n; ++i) {
664 if (pack.lights[i].posRadius.w <= 0.f) {
665 dirI = i;
666 break;
667 }
668 }
669 if (dirI >= 0) {
670 glm::vec3 d(pack.lights[dirI].posRadius);
671 if (glm::length(d) < 1e-6f) d = glm::vec3(0.f, 1.f, 0.f);
672 else d = glm::normalize(d);
673 mesh3dFrameUbo.lightDir = glm::vec4(d, float(n));
674 mesh3dFrameUbo.lightColor =
675 glm::vec4(glm::vec3(pack.lights[dirI].color), mesh3dFrameUbo.lightColor.w);
676 } else {
677 // No directional: zero the legacy primary slot. mesh3d.frag always shades it.
678 mesh3dFrameUbo.lightDir = glm::vec4(0.f, 1.f, 0.f, float(n));
679 mesh3dFrameUbo.lightColor =
680 glm::vec4(0.f, 0.f, 0.f, mesh3dFrameUbo.lightColor.w);
681 }
682}
683
684void Graphics::setCloudShadows(float strength, float worldCell, float time, float windSpeed,
685 float windAngle, float coverage, float detail) {
686 mesh3dFrameUbo.cloud = glm::vec4(std::clamp(strength, 0.f, 1.f), std::max(worldCell, 1e-4f),
687 time, 0.f);
688 mesh3dFrameUbo.cloudWind = glm::vec4(std::cos(windAngle) * windSpeed,
689 std::sin(windAngle) * windSpeed,
690 std::clamp(coverage, 0.f, 1.f), std::clamp(detail, 0.f, 1.f));
691}
692
693void Graphics::ensureFlatNormalTexture3D() {
694 if (flatNormalTexture3D) return;
695 const uint8_t px[4] = {128, 128, 255, 255};
696 flatNormalTexture3D = newTexture(1, 1, px);
697}
698
699void Graphics::ensureFlatHeightTexture3D() {
700 if (flatHeightTexture3D) return;
701 // Mid-gray height: no relief when scale>0 without a real height map.
702 const uint8_t px[4] = {128, 128, 128, 255};
703 flatHeightTexture3D = newTexture(1, 1, px);
704}
705
706vkb::BoundSet Graphics::mesh3dSetFor(GpuTexture *gpuTex, GpuTexture *normalTex, GpuTexture *envTex,
707 GpuTexture *heightTex, GpuTexture *depthTex,
708 Mesh3dFrameSlots &fslots) {
709 ASSERT(gpuTex != nullptr);
710 ASSERT(normalTex != nullptr);
711 ASSERT(envTex != nullptr);
712 ASSERT(heightTex != nullptr);
713 ASSERT(currentShadowArrayView());
714 ASSERT(fslots.uboRing.buffer);
715 ASSERT(fslots.shadowRing.buffer);
716
717 Mesh3dSetKey key{gpuTex, normalTex, envTex, heightTex, depthTex};
718 auto it = fslots.sets.find(key);
719 if (it != fslots.sets.end()) return it->second;
720
721 vk::DescriptorSetAllocateInfo alloc{};
722 alloc.descriptorPool = descriptorPool;
723 alloc.descriptorSetCount = 1;
724 alloc.pSetLayouts = &mesh3dSetLayout;
725 vkb::UnboundSet unbound{device->allocateDescriptorSets(alloc).front()};
726
727 vkb::DescriptorSetUpdater updater(12, 12, 0);
728 updater.beginDescriptorSet(unbound)
729 .beginBuffers(0, 0, vk::DescriptorType::eUniformBufferDynamic)
730 .buffer(fslots.uboRing.buffer, 0, fslots.uboRing.size)
731 .beginImages(1, 0, vk::DescriptorType::eCombinedImageSampler)
732 .image(vkb::SampledImage::forLaterSample(gpuTex->sampler, gpuTex->imageView()))
733 .beginImages(2, 0, vk::DescriptorType::eCombinedImageSampler)
734 .image(vkb::SampledImage::forLaterSample(normalTex->sampler, normalTex->imageView()))
735 .beginImages(3, 0, vk::DescriptorType::eCombinedImageSampler)
736 .image(vkb::SampledImage::forLaterSample(envTex->sampler, envTex->imageView()))
737 .beginBuffers(4, 0, vk::DescriptorType::eUniformBufferDynamic)
738 .buffer(fslots.shadowRing.buffer, 0, fslots.shadowRing.size)
739 .beginImages(5, 0, vk::DescriptorType::eCombinedImageSampler)
740 .image(vkb::SampledImage::forLaterSample(shadowSampler, currentShadowArrayView()))
741 .beginImages(6, 0, vk::DescriptorType::eCombinedImageSampler)
742 .image(vkb::SampledImage::forLaterSample(heightTex->sampler, heightTex->imageView()))
743 .beginImages(7, 0, vk::DescriptorType::eCombinedImageSampler)
744 .image(vkb::SampledImage::forLaterSample(depthTex->sampler, depthTex->imageView()))
745 .update(device.instance);
746
747 vkb::BoundSet bound = std::move(unbound).publish();
748 fslots.sets.emplace(key, bound);
749 return bound;
750}
751
752void Graphics::setMesh3DViewProj(const glm::mat4 &viewProj) {
753 mesh3dFrameUbo.mvp = viewProj;
754}
755
756void Graphics::setMesh3DView(const glm::mat4 &view) {
757 mesh3dFrameUbo.view = view;
758}
759
760void Graphics::setMesh3DClip(float nearZ, float farZ) {
761 const float n = nearZ > 1e-4f ? nearZ : 0.1f;
762 const float f = farZ > n ? farZ : n + 1.f;
763 mesh3dFrameUbo.clipInfo = glm::vec4(n, f, mesh3dFrameUbo.clipInfo.z, mesh3dFrameUbo.clipInfo.w);
764 mesh3dClustered.clipInfo.x = n;
765 mesh3dClustered.clipInfo.y = f;
766}
767
768vkb::FrameGraph *Graphics::currentDeferredFrameGraph() {
769 if (deferredFrameGraphs_[0] == nullptr) return nullptr;
770 return deferredFrameGraphs_[currentFrameSlot() % deferredFrameGraphs_.size()].get();
771}
772
773void Graphics::buildDeferredFrameGraphs() {
774 // One FrameGraph per in-flight slot imports the engine-owned targets and
775 // owns the deferred passes: the 3 CSM cascades (per-layer views of the
776 // shadow array) + the G-buffer fill share one dependency-free layer, so the
777 // JobSystem executor records all four command buffers concurrently (see
778 // recordDeferredFrameGraph). The engine keeps image ownership so
779 // renderEntityIdMask / readGBufferToImageData and the postFX wrappers are
780 // unaffected. Each graph is only used on its slot's frames, so its command
781 // buffer is reused two frames later — by then the present slot fence
782 // guarantees the previous graph submit completed (same queue, submitted
783 // before the present command buffer).
784 const vk::Format depthFmt = vk::Format::eD32Sfloat;
785 const vk::Format colorFmt = pickGBufferColorFormat(device);
786 const uint32_t mapSize = uint32_t(ShadowConfig::kMapSize);
787 const uint32_t shadowLayers = uint32_t(ShadowConfig::kCascades);
788 const uint32_t w = gbufferWidth > 0 ? uint32_t(gbufferWidth) : 1u;
789 const uint32_t h = gbufferHeight > 0 ? uint32_t(gbufferHeight) : 1u;
790
791 for (size_t i = 0; i < deferredFrameGraphs_.size(); ++i) {
792 auto graph = std::make_unique<vkb::FrameGraph>(&device, 1);
793
794 vkb::TextureDesc shadowDesc;
795 shadowDesc.format = depthFmt;
796 shadowDesc.extent = vk::Extent3D{mapSize, mapSize, 1};
797 shadowDesc.arrayLayers = shadowLayers;
798 shadowDesc.aspect = vk::ImageAspectFlagBits::eDepth;
799 shadowDesc.usage = vk::ImageUsageFlagBits::eSampled |
800 vk::ImageUsageFlagBits::eDepthStencilAttachment;
801 shadowDesc.afterLayout = vk::ImageLayout::eDepthStencilReadOnlyOptimal;
802 vk::ClearValue shadowClear{};
803 shadowClear.depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
804 const bool haveShadowSlot =
805 i < shadowMaps.size() && shadowMaps[i].image.layerCount() >= shadowLayers;
806 if (haveShadowSlot) {
807 const vk::Image shadowImage = shadowMaps[i].image.image();
808 for (uint32_t c = 0; c < shadowLayers; ++c) {
809 auto shadowH = graph->importTexture("shadowCascade" + std::to_string(c),
810 shadowImage, shadowMaps[i].image.layerView(c),
811 shadowDesc);
812 graph->addPass("shadow" + std::to_string(c))
813 .depthAttachment(shadowH, vkb::AttachmentOp::clear(shadowClear))
814 .record([this, c](vkb::FrameGraphPassContext &ctx) {
815 recordShadowCascadePass(ctx, int(c));
816 });
817 }
818 }
819
820 if (i < gbufferSlots.size() && gbufferWidth > 0 && gbufferHeight > 0) {
821 auto &slot = gbufferSlots[i];
822 vkb::TextureDesc colorDesc;
823 colorDesc.format = colorFmt;
824 colorDesc.extent = vk::Extent3D{w, h, 1};
825 colorDesc.usage = vk::ImageUsageFlagBits::eSampled |
826 vk::ImageUsageFlagBits::eColorAttachment |
827 vk::ImageUsageFlagBits::eTransferSrc;
828 colorDesc.afterLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
829 auto normalH = graph->importTexture("gbNormal", slot.normal.image(),
830 slot.normal.imageView(), colorDesc);
831 auto depthColorH = graph->importTexture("gbDepthColor", slot.depthColor.image(),
832 slot.depthColor.imageView(), colorDesc);
833 auto albedoH = graph->importTexture("gbAlbedo", slot.albedo.image(),
834 slot.albedo.imageView(), colorDesc);
835
836 vkb::TextureDesc depthDesc;
837 depthDesc.format = depthFmt;
838 depthDesc.extent = vk::Extent3D{w, h, 1};
839 depthDesc.aspect = vk::ImageAspectFlagBits::eDepth;
840 depthDesc.usage = vk::ImageUsageFlagBits::eSampled |
841 vk::ImageUsageFlagBits::eDepthStencilAttachment;
842 depthDesc.afterLayout = vk::ImageLayout::eDepthStencilReadOnlyOptimal;
843 auto depthH = graph->importTexture("gbHwDepth", slot.depth.image(),
844 slot.depth.imageView(), depthDesc);
845
846 std::array<vk::ClearValue, 4> clears{};
847 clears[0].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
848 clears[1].color = vk::ClearColorValue(std::array<float, 4>{1, 1, 1, 1});
849 clears[2].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
850 clears[3].depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
851 graph->addPass("gbuffer")
852 .colorAttachment(normalH, vkb::AttachmentOp::clear(clears[0]))
853 .colorAttachment(depthColorH, vkb::AttachmentOp::clear(clears[1]))
854 .colorAttachment(albedoH, vkb::AttachmentOp::clear(clears[2]))
855 .depthAttachment(depthH, vkb::AttachmentOp::clear(clears[3]))
856 .record([this](vkb::FrameGraphPassContext &ctx) { recordGBufferPassDraws(ctx); });
857 }
858 graph->compile();
859 deferredFrameGraphs_[i] = std::move(graph);
860 }
861}
862
863void Graphics::recordShadowCascadePass(vkb::FrameGraphPassContext &ctx, int cascade) {
864 // Runs inside the FrameGraph's "shadow<cascade>" render-pass instance
865 // (already begun with a depth clear); only draw commands go here. The pass
866 // may be recorded on a JobSystem worker, so everything below must be
867 // read-only: shadowCascadeDraws was captured by endShadowPass on the main
868 // thread before the graph records.
869 auto &cb = ctx.commandBuffer();
870 const vk::Extent2D extent = ctx.extent();
871 const uint32_t size = extent.width ? extent.width : uint32_t(ShadowConfig::kMapSize);
872 setViewportAndScissor(cb, size, size);
873 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, shadowPipeline);
874 bool alphaBound = false;
875 for (const auto &d : shadowCascadeDraws[cascade]) {
876 if (!d.mesh || !d.mesh->gpuHandle) continue;
877 const bool wantAlpha = d.alphaTest && shadowAlphaPipeline;
878 if (wantAlpha != alphaBound) {
879 cb.bindPipeline(vk::PipelineBindPoint::eGraphics,
880 wantAlpha ? shadowAlphaPipeline : shadowPipeline);
881 alphaBound = wantAlpha;
882 }
883 auto *gpuMesh = static_cast<GpuMesh *>(d.mesh->gpuHandle);
884 if (wantAlpha) {
885 Texture *alb = d.albedo ? d.albedo : whiteTexture;
886 if (alb && alb->gpuHandle && texSetLayout) {
887 auto *gpuTex = static_cast<GpuTexture *>(alb->gpuHandle);
888 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
889 shadowAlphaPipelineLayout, 0, 1,
890 gpuTex->descriptorSet.ptr(), 0, nullptr);
891 }
892 }
893 cb.pushConstants(wantAlpha ? shadowAlphaPipelineLayout : shadowPipelineLayout,
894 vk::ShaderStageFlagBits::eVertex, 0, sizeof(glm::mat4), &d.mvp);
895 drawIndexedMesh(cb, *gpuMesh);
896 }
897}
898
899void Graphics::recordGBufferPassDraws(vkb::FrameGraphPassContext &ctx) {
900 // Runs inside the FrameGraph's "gbuffer" render-pass instance (already
901 // begun with the planned clear values); only draw commands go here. The
902 // pass may be recorded on a JobSystem worker, so everything below must be
903 // read-only: gbufferPassDraws was captured on the main thread.
904 auto &cb = ctx.commandBuffer();
905 const vk::Extent2D extent = ctx.extent();
906 const uint32_t w = extent.width ? extent.width : uint32_t(gbufferWidth);
907 const uint32_t h = extent.height ? extent.height : uint32_t(gbufferHeight);
908 setViewportAndScissor(cb, w, h);
909 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, gbufferPipeline);
910 bool alphaBound = false;
911 for (const auto &d : gbufferPassDraws) {
912 if (!d.mesh || !d.mesh->gpuHandle) continue;
913 const bool wantAlpha = d.alphaTest && gbufferAlphaPipeline;
914 if (wantAlpha != alphaBound) {
915 cb.bindPipeline(vk::PipelineBindPoint::eGraphics,
916 wantAlpha ? gbufferAlphaPipeline : gbufferPipeline);
917 alphaBound = wantAlpha;
918 }
919 auto *gpuMesh = static_cast<GpuMesh *>(d.mesh->gpuHandle);
920 Texture *alb = d.albedo ? d.albedo : whiteTexture;
921 if (alb && alb->gpuHandle && texSetLayout) {
922 auto *gpuTex = static_cast<GpuTexture *>(alb->gpuHandle);
923 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, gbufferPipelineLayout, 0, 1,
924 gpuTex->descriptorSet.ptr(), 0, nullptr);
925 }
926 cb.pushConstants(gbufferPipelineLayout,
927 vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, 0,
928 sizeof(GBufferPush), &d.push);
929 drawIndexedMesh(cb, *gpuMesh);
930 }
931}
932
933void Graphics::recordDeferredFrameGraph() {
934 const size_t slot = currentFrameSlot();
935 if (deferredGraphRecorded_ && deferredGraphRecordedSlot_ == slot) {
936 // render3D can be called several times per script frame (e.g. the
937 // render tests call it 3x before present). The deferred graph's command
938 // buffers must only be recorded once per slot per frame — re-recording
939 // them while the previous submit is still in flight would reset
940 // in-use command buffers (UB, GPU hang).
941 return;
942 }
943 auto *graph = currentDeferredFrameGraph();
944 if (!graph && (!shadowMaps.empty() || !gbufferSlots.empty())) {
945 // Shadows can be enabled without a G-buffer pass (or vice versa); build
946 // the deferred graphs on demand from whatever targets exist today.
947 buildDeferredFrameGraphs();
948 graph = currentDeferredFrameGraph();
949 }
950 if (!graph || !gbufferPipeline || !gbufferRenderPass || !shadowPipeline) {
951 dropPendingOffscreenPasses();
952 return;
953 }
954 // Record the declarative deferred passes (3 CSM cascades + G-buffer, one
955 // independent layer) with the JobSystem executor — the four command
956 // buffers are recorded concurrently on workers — then submit them on the
957 // graphics queue before the swapchain pass begins. Layout transitions and
958 // the render-pass instances are planned by the FrameGraph. Same-queue
959 // submission order plus the present slot fence (waited in Present::begin)
960 // keep this slot's graph command buffers safe to reuse two frames later.
961 auto *jobs = thread::Thread::create()->getJobSystem();
962 jobs->beginFrame(); // idempotent wait; recycles the per-frame arena
963 // Re-plan every frame (cheap; device objects are cached) so the graph is
964 // in the compiled phase for this record cycle — vkb::FrameGraph enforces
965 // build -> compile -> record -> submit and record() exactly once per
966 // compile.
967 graph->compile();
968 // Parallel executor: each pass owns a dedicated command pool (one pool per
969 // frame slot per pass), so the workers never share a pool while recording
970 // concurrently — the Vulkan external-synchronization rule for command
971 // pools is satisfied structurally.
973 graph->submit();
974 jobs->endFrame();
975 for (auto &d : shadowCascadeDraws) d.clear();
976 gbufferPassDraws.clear();
977 shadowPendingMask = 0;
978 gbufferPending = false;
979 deferredGraphRecorded_ = true;
980 deferredGraphRecordedSlot_ = slot;
981}
982
983} // namespace eve::graphics::vulkan
vk::ShaderModule vert
vk::ShaderModule frag
float height
Definition Grass.cpp:235
int x
Definition Grass.cpp:135
glm::vec3 n
Definition Grass.cpp:64
int h
int w
std::vector< Colorf > px
const Graph & graph
float depth
uint32_t b
uint32_t c
float roughness
Texture * normal
Texture * albedo
float metallic
int width
float f
glm::vec3 eye
Mesh * mesh
glm::mat4 viewProj
glm::mat4 view
glm::mat4 model
int d
image::ImageData::Colorf color
std::string image
float scale
Definition TreeMesh.cpp:122
V3 dir
Definition TreeMesh.cpp:121
std::unique_ptr< RenderControl > renderControl_
Definition Graphics.h:990
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
void drawMeshGBuffer(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model, float nearZ, float farZ, Texture *albedo=nullptr, float tintR=1.f, float tintG=1.f, float tintB=1.f) override
void beginShadowPass(int cascadeIndex) override
Depth-only shadow pass for one cascade layer (0..2). Draws are recorded into the next begin3DFrame co...
void begin3DFrame() override
Begin a 3D frame: shadow/gbuffer (if pending) then a sampleable scene color pass (color+depth)....
void setMesh3DParallax(float scale, float minLayers=8.f, float maxLayers=32.f) override
Parallax occlusion mapping for the next default mesh draw. scale: UV displacement strength (0=off)....
void setCloudShadows(float strength, float worldCell, float time, float windSpeed, float windAngle, float coverage, float detail) override
Dynamic cloud shadows cast on the ground by the default PBR mesh path. strength 0 disables (no change...
void drawVoxelFaceInstances(const uint32_t *packed, int count, float originX, float originY, float originZ, const std::string &faceDir, Texture *atlas, int tilesPerRow=16, const uint32_t *ao=nullptr) override
Instanced voxel face rectangles (32-bit packed instances). ao: optional per-instance ambient-occlusio...
void setMesh3DShadowReceive(bool receive) override
Per-draw: when false, shadow sampling is forced off for the next mesh draw.
void setMesh3DLighting(const Lighting3DPack &pack) override
Per-frame ambient + up to 8 lights packed into Mesh3DUBO.
void setMesh3DEnv(Texture *cube, float intensity) override
Specular IBL environment for subsequent default mesh draws. cube must be from newCubemap (or nullptr ...
bool isCanvasActive() const override
void setMesh3DTexCellBomb(float cellScale, float strength, float rotAmount=1.f) override
Texture cell bombing for the next default mesh draw (breaks tiling). cellScale: cells per UV unit (ty...
void drawMeshShadow(Mesh *mesh, const glm::mat4 &lightMVP) override
void setMesh3DCameraPos(const glm::vec3 &eye) override
Camera eye used by mesh shaders that need view/rim (stored in Mesh3DUBO).
void begin3DFrameToCanvas(Canvas *canvas) override
Open a 3D render pass targeting an offscreen Canvas (color + depth) at the canvas size....
void setMesh3DViewProj(const glm::mat4 &viewProj) override
void setMesh3DShadows(const ShadowUpload &upload) override
Upload CSM constants for subsequent default mesh draws (active=false disables).
void beginGBufferPass(int width, int height) override
Depth/normal(/albedo) fill pass for mid/post effects. One-shot submit (like shadow); call before begi...
void setMesh3DLight(const glm::vec3 &dir, const glm::vec3 &color) override
Directional light for subsequent drawMesh calls (world-space direction toward surface).
void setMesh3DHeightTexture(Texture *height) override
Optional height map for parallax (R channel; nullptr = flat / off).
void drawMeshGBufferAlpha(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model, float nearZ, float farZ, Texture *albedo=nullptr, float tintR=1.f, float tintG=1.f, float tintB=1.f) override
GBuffer fill with alpha-cutout discard (card/billboard geometry such as sprite-stack slices): same ou...
Texture * newCubemap(int faceSize, const uint8_t *rgbaFaces) override
Create an RGBA8 cubemap from 6 faces packed as +X,-X,+Y,-Y,+Z,-Z (each faceSize×faceSize,...
void setMesh3DView(const glm::mat4 &view) override
Camera view matrix for subsequent drawMesh (view-space depth / CSM select).
void drawMeshShadowAlpha(Mesh *mesh, const glm::mat4 &lightMVP, Texture *albedo=nullptr) override
Shadow pass draw with alpha-cutout discard (card/billboard geometry such as sprite-stack slices): tra...
void setMesh3DNormalTexture(Texture *normal) override
Optional normal map for the next drawMesh / drawMeshShader (nullptr = flat).
void setMesh3DClip(float nearZ, float farZ) override
Near/far used to pack linear depth into scene color A (SSGI).
Texture * newTexture(int width, int height, const uint8_t *rgba, bool repeatU=false, bool repeatV=false) override
void setMesh3DSceneDepth(Texture *depth) override
Optional scene hardware depth (G-buffer hwDepth, Vulkan NDC z) bound to mesh3d shader binding 7....
void setMesh3DMaterial(float metallic, float roughness) override
Metallic (0..1) and roughness (0..1) for the next default mesh draw.
void clear(std::optional< Color > color, std::optional< int > stencil, std::optional< double > depth) override
vkb::ColorAttachmentImage & colorImage()
Definition Canvas.h:33
void recordFrameGraphWithJobSystem(vkb::FrameGraph &graph, eve::thread::JobSystem *jobs)
Record a vkb::FrameGraph with the engine JobSystem as the parallel recording executor.
static constexpr int kMaxLights
Definition Light.h:91
Light3DGpu lights[kMaxLights]
Definition Light.h:93
static constexpr int kMapSize
Definition Shadow.h:9
static constexpr int kCascades
Definition Shadow.h:8