载入中...
搜索中...
未找到
Graphics.cpp
浏览该文件的文档.
1// Vulkan backend implementation — backend lifecycle and frame orchestration.
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
6#define VKB_IMPL
11#include "graphics/Light.h"
12#include "graphics/Outline.h"
16
17#include <SDL2/SDL.h>
18#include <SDL2/SDL_vulkan.h>
19
20#include <algorithm>
21#include <array>
22#include <cmath>
23#include <cstdio>
24#include <cstdlib>
25#include <cstring>
26#include <functional>
27#include <future>
28#include <mutex>
29#include <stdexcept>
30#include <string>
31#include <vector>
32#if !defined(_WIN32)
33#include <unistd.h>
34#endif
35
36#include "common/BootWarmup.h"
37#include "common/Capability.h"
38#include "common/CrashLog.h"
39#include "common/Exception.h"
41#include "common/config.h"
45#include "image/Image.h"
46#include "image/ImageData.h"
47#include "zeroerr/assert.h"
48
49#include <memory>
50
51
52#include <assimp/matrix3x3.h>
53#include <assimp/matrix4x4.h>
54#include <assimp/mesh.h>
55#include <assimp/vector3.h>
56#include <glm/gtc/matrix_transform.hpp>
57
59
60namespace eve::graphics::vulkan {
61
62namespace {
63
64bool wantVulkanValidation() {
65#if defined(EVENGINE_IOS)
66 return false;
67#else
68 const char* vkVal = std::getenv("EVENGINE_VULKAN_VALIDATION");
69 return vkVal && vkVal[0] != '\0' && vkVal[0] != '0';
70#endif
71}
72
73void disableImplicitLayersIfSafe() {
74 if (wantVulkanValidation()) return;
75 if (const char* existing = std::getenv("VK_LOADER_LAYERS_DISABLE")) {
76 if (existing[0] != '\0') return;
77 }
78 if (const char* layers = std::getenv("VK_INSTANCE_LAYERS")) {
79 if (layers[0] != '\0') return;
80 }
81#if defined(_WIN32)
82 _putenv_s("VK_LOADER_LAYERS_DISABLE", "~implicit~");
83#else
84 setenv("VK_LOADER_LAYERS_DISABLE", "~implicit~", 0);
85#endif
86}
87
88bool extensionAvailable(const std::vector<vk::ExtensionProperties>& props, const char* name) {
89 for (const auto& p : props) {
90 if (std::strcmp(p.extensionName, name) == 0) return true;
91 }
92 return false;
93}
94
95void addIfAvailable(std::vector<const char*>& exts, const std::vector<vk::ExtensionProperties>& props,
96 const char* name) {
97 if (extensionAvailable(props, name)) exts.push_back(name);
98}
99
100std::vector<const char*> collectFastInstanceExtensions(const std::vector<vk::ExtensionProperties>& props,
101 vk::InstanceCreateFlags* flagsOut) {
102 std::vector<const char*> exts;
103 addIfAvailable(exts, props, "VK_KHR_surface");
104 // Required before surface formats report HDR10 / extended-sRGB color spaces.
105 addIfAvailable(exts, props, "VK_EXT_swapchain_colorspace");
106#if defined(_WIN32)
107 addIfAvailable(exts, props, "VK_KHR_win32_surface");
108#elif defined(__ANDROID__)
109 addIfAvailable(exts, props, "VK_KHR_android_surface");
110#elif defined(__APPLE__)
111 addIfAvailable(exts, props, "VK_EXT_metal_surface");
112 if (extensionAvailable(props, "VK_KHR_portability_enumeration")) {
113 exts.push_back("VK_KHR_portability_enumeration");
114 if (flagsOut) *flagsOut |= vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR;
115 }
116#else
117 addIfAvailable(exts, props, "VK_KHR_xcb_surface");
118 addIfAvailable(exts, props, "VK_KHR_xlib_surface");
119 addIfAvailable(exts, props, "VK_KHR_wayland_surface");
120#endif
121 return exts;
122}
123
124void initVulkanDispatcher() {
125#if defined(EVENGINE_MACOSX)
126 if (PFN_vkGetInstanceProcAddr sdlGpa =
127 reinterpret_cast<PFN_vkGetInstanceProcAddr>(SDL_Vulkan_GetVkGetInstanceProcAddr())) {
128 VULKAN_HPP_DEFAULT_DISPATCHER.init(sdlGpa);
129 vkb::InstanceBuilder::loaded = true;
130 return;
131 }
132#endif
133 if (!vkb::InstanceBuilder::loaded) {
134 vkb::InstanceBuilder::loadDefault();
135 vkb::InstanceBuilder::loaded = true;
136 }
137}
138
139vkb::Instance createInstanceFast() {
140 initVulkanDispatcher();
141 disableImplicitLayersIfSafe();
142 // Enumerate instance extensions only. vk-bootstrap's SystemInfo::query() also
143 // walks every implicit layer (and each layer's extensions), which is the
144 // bulk of "instance + surface" on a Windows SDK install.
145 const auto props = vk::enumerateInstanceExtensionProperties();
146 vk::InstanceCreateFlags flags{};
147 const auto exts = collectFastInstanceExtensions(props, &flags);
148
149 if (wantVulkanValidation()) {
150 vkb::InstanceBuilder builder;
151 builder.require_api_version(1, 0);
152 builder.request_validation_layers();
153 builder.use_default_debug_messenger();
154 for (auto* e : exts) builder.enable_extension(e);
155#if defined(EVENGINE_MACOSX) || defined(EVENGINE_IOS)
156 builder.set_headless(true);
157 builder.add_flags(flags);
158#endif
159 std::printf("EVEngine: Vulkan validation layers enabled (EVENGINE_VULKAN_VALIDATION)\n");
160 return builder.build();
161 }
162
163 vk::ApplicationInfo app{};
164 app.pApplicationName = "EVEngine";
165 app.pEngineName = "EVEngine";
166 app.apiVersion = VK_MAKE_VERSION(1, 0, 0);
167
168 vk::InstanceCreateInfo ci{};
169 ci.flags = flags;
170 ci.pApplicationInfo = &app;
171 ci.enabledExtensionCount = static_cast<uint32_t>(exts.size());
172 ci.ppEnabledExtensionNames = exts.data();
173
174 vkb::Instance inst;
175 inst.instance = vk::createInstance(ci);
176 VULKAN_HPP_DEFAULT_DISPATCHER.init(inst.instance);
177 return inst;
178}
179
180struct InstanceWarmup {
181 std::mutex mu;
182 std::future<vkb::Instance> future;
183 bool started = false;
184 bool consumed = false;
185};
186
187InstanceWarmup& instanceWarmup() {
188 static InstanceWarmup state;
189 return state;
190}
191
192void startVulkanInstanceWarmupImpl() {
193#if defined(EVENGINE_MACOSX) || defined(EVENGINE_IOS)
194 // MoltenVK / SDL loader affinity is main-thread on Apple.
195 return;
196#else
197 auto& w = instanceWarmup();
198 std::lock_guard<std::mutex> lock(w.mu);
199 if (w.started) return;
200 w.started = true;
201 w.future = std::async(std::launch::async, [] {
202 StartupStage stage(" vulkan: instance (warmup thread)");
203 return createInstanceFast();
204 });
205#endif
206}
207
208vkb::Instance consumeWarmedInstance() {
209#if defined(EVENGINE_MACOSX) || defined(EVENGINE_IOS)
210 StartupStage stage(" vulkan: instance");
211 return createInstanceFast();
212#else
213 startVulkanInstanceWarmupImpl();
214 std::future<vkb::Instance> fut;
215 {
216 auto& w = instanceWarmup();
217 std::lock_guard<std::mutex> lock(w.mu);
218 if (w.consumed) {
219 throw Exception("Vulkan instance warmup already consumed");
220 }
221 w.consumed = true;
222 fut = std::move(w.future);
223 }
224 StartupStage waitStage(" vulkan: wait instance warmup");
225 return fut.get();
226#endif
227}
228
229void discardWarmedInstance() noexcept {
230#if !defined(EVENGINE_MACOSX) && !defined(EVENGINE_IOS)
231 std::future<vkb::Instance> fut;
232 {
233 auto& w = instanceWarmup();
234 std::lock_guard<std::mutex> lock(w.mu);
235 if (!w.started || w.consumed) return;
236 w.consumed = true;
237 fut = std::move(w.future);
238 }
239 try {
240 vkb::Instance warmed = fut.get();
241 if (static_cast<VkInstance>(warmed.instance) != VK_NULL_HANDLE) warmed.destroy();
242 } catch (const std::exception& e) {
243 std::fprintf(stderr, "[vulkan] instance warmup cleanup failed: %s\n", e.what());
244 } catch (...) {
245 std::fprintf(stderr, "[vulkan] instance warmup cleanup failed with an unknown error\n");
246 }
247#endif
248}
249
250struct RegisterVulkanWarmup {
251 RegisterVulkanWarmup() { eve::boot::registerVulkanInstanceWarmup(&startVulkanInstanceWarmupImpl); }
252} gRegisterVulkanWarmup;
253
254} // namespace
255
256// --- Backend lifecycle and frame orchestration --------------------------------
257
258#if defined(VKB_ENABLE_VMA)
259VmaAllocatorOwner::~VmaAllocatorOwner() {
260 if (allocator_) vmaDestroyAllocator(allocator_);
261}
262
263void VmaAllocatorOwner::create(const vkb::Instance& instance, const vkb::PhysicalDevice& physicalDevice,
264 vkb::Device& device) {
265 VmaAllocatorCreateInfo createInfo{};
266 createInfo.instance = static_cast<VkInstance>(instance.instance);
267 createInfo.physicalDevice = static_cast<VkPhysicalDevice>(physicalDevice.instance);
268 createInfo.device = static_cast<VkDevice>(device.instance);
269 VmaVulkanFunctions vulkanFunctions{};
270 // The headless backend does not ask SDL to load Vulkan, so its loader
271 // accessor may legitimately be null. Reuse the dispatcher that created
272 // this instance; it is also the portable path on Android.
273 vulkanFunctions.vkGetInstanceProcAddr = VULKAN_HPP_DEFAULT_DISPATCHER.vkGetInstanceProcAddr;
274 if (!vulkanFunctions.vkGetInstanceProcAddr) {
275 throw Exception("VMA could not resolve vkGetInstanceProcAddr from the Vulkan dispatcher");
276 }
277 vulkanFunctions.vkGetDeviceProcAddr = reinterpret_cast<PFN_vkGetDeviceProcAddr>(
278 vulkanFunctions.vkGetInstanceProcAddr(createInfo.instance, "vkGetDeviceProcAddr"));
279 if (!vulkanFunctions.vkGetDeviceProcAddr) {
280 throw Exception("VMA could not resolve vkGetDeviceProcAddr");
281 }
282 createInfo.pVulkanFunctions = &vulkanFunctions;
283 // InstanceBuilder requests Vulkan 1.0. VMA requires this value to describe
284 // the application's instance contract, not the physical device maximum.
285 createInfo.vulkanApiVersion = VK_API_VERSION_1_0;
286 const VkResult result = vmaCreateAllocator(&createInfo, &allocator_);
287 if (result != VK_SUCCESS) throw Exception("vmaCreateAllocator failed: %d", int(result));
288 device.attachVmaAllocator(allocator_);
289}
290#endif
291
292std::string Graphics::getBackendName() const { return "vulkan"; }
293
295
296Graphics::Graphics() = default;
297
301 if (!initialized) {
302 // A process-level boot warmup may exist even when callers decide not
303 // to initialize graphics. Join it before the Vulkan loader unloads.
304 discardWarmedInstance();
305 if (static_cast<VkInstance>(inst.instance) != VK_NULL_HANDLE) inst.destroy();
306 return;
307 }
308 device->waitIdle();
309 // Drop optional RT resources while the logical device is still valid.
310 if (auto* rt = eve::cap::query<eve::graphics::IRayTracing>()) rt->detachFromGraphics();
311 destroyPbrResources();
312 deferredFileTextures_.clear();
313 if (gpuQueryPool_) device->destroyQueryPool(gpuQueryPool_);
314 gpuQueryPool_ = nullptr;
315 // Pipeline objects hold raw Shader* owned by ownedShaders. Drop them
316 // first so their destructors do not see freed shader pointers.
317 pipelineAA_.reset();
318 pipelineAO_.reset();
319 pipelineGI_.reset();
320 renderControl_.reset();
321 destroyGpuParticleResources();
322 ownedCanvases.clear();
323 ownedMeshes.clear();
324 ownedGpuMeshes.clear();
325 ownedTextures.clear();
326 for (auto& g : ownedGpuTextures) {
327 if (g->sampler) device->destroySampler(g->sampler);
328 }
329 ownedGpuTextures.clear();
330 texturesByPath.clear();
331 for (auto& g : ownedGpuShaders) {
332 if (g->swapchainPipeline) device->destroyPipeline(g->swapchainPipeline);
333 if (g->offscreenPipeline) device->destroyPipeline(g->offscreenPipeline);
334 if (g->swapchainOpaquePipeline) device->destroyPipeline(g->swapchainOpaquePipeline);
335 if (g->offscreenOpaquePipeline) device->destroyPipeline(g->offscreenOpaquePipeline);
336 if (g->hdrOffscreenPipeline) device->destroyPipeline(g->hdrOffscreenPipeline);
337 if (g->hdrOffscreenOpaquePipeline) device->destroyPipeline(g->hdrOffscreenOpaquePipeline);
338 if (g->mesh3dPipeline) device->destroyPipeline(g->mesh3dPipeline);
339 if (g->mesh3dXrayPipeline) device->destroyPipeline(g->mesh3dXrayPipeline);
340 if (g->mesh3dOffscreenPipeline) device->destroyPipeline(g->mesh3dOffscreenPipeline);
341 if (g->mesh3dHdrOffscreenPipeline) device->destroyPipeline(g->mesh3dHdrOffscreenPipeline);
342 // pipelineLayout is shared; do not destroy per-shader
343 }
344 ownedGpuShaders.clear();
345 ownedShaders.clear();
346 destroySwapchainResources();
347 if (pipeline) device->destroyPipeline(pipeline);
348 if (solidAlphaPipeline) device->destroyPipeline(solidAlphaPipeline);
349 if (additiveSolidPipeline) device->destroyPipeline(additiveSolidPipeline);
350 if (premultipliedSolidPipeline) device->destroyPipeline(premultipliedSolidPipeline);
351 if (multiplySolidPipeline) device->destroyPipeline(multiplySolidPipeline);
352 if (pipelineLayout) device->destroyPipelineLayout(pipelineLayout);
353 if (texPipeline) device->destroyPipeline(texPipeline);
354 if (additiveTexPipeline) device->destroyPipeline(additiveTexPipeline);
355 if (premultipliedTexPipeline) device->destroyPipeline(premultipliedTexPipeline);
356 if (multiplyTexPipeline) device->destroyPipeline(multiplyTexPipeline);
357 if (opaqueTexPipeline) device->destroyPipeline(opaqueTexPipeline);
358 if (particleDistortionPipeline) device->destroyPipeline(particleDistortionPipeline);
359 if (sceneTonemapPipeline) device->destroyPipeline(sceneTonemapPipeline);
360 if (texPipelineLayout) device->destroyPipelineLayout(texPipelineLayout);
361 if (shaderPipelineLayout) device->destroyPipelineLayout(shaderPipelineLayout);
362 if (mesh3dPipeline) device->destroyPipeline(mesh3dPipeline);
363 if (mesh3dTransparentPipeline) device->destroyPipeline(mesh3dTransparentPipeline);
364 for (auto pipeline : mesh3dSurfacePipelines)
365 if (pipeline) device->destroyPipeline(pipeline);
366 for (auto pipeline : primitive3DPipelines)
367 if (pipeline) device->destroyPipeline(pipeline);
368 destroyOffscreen3DResources();
369 if (mesh3dPipelineLayout) device->destroyPipelineLayout(mesh3dPipelineLayout);
370 if (mesh3dShaderPipelineLayout) device->destroyPipelineLayout(mesh3dShaderPipelineLayout);
371 mesh3dSetLayoutUnique.reset();
372 mesh3dFrameSlots.clear();
373 destroyVoxelRectResources();
374 if (mesh3dClusteredPipeline) device->destroyPipeline(mesh3dClusteredPipeline);
375 if (mesh3dClusteredPipelineLayout) device->destroyPipelineLayout(mesh3dClusteredPipelineLayout);
376 mesh3dClusteredSetLayoutUnique.reset();
377 mesh3dClusteredFrameSlots.clear();
378 destroyShadowResources();
379 destroyGBufferResources();
380 if (skinPassPipelineLayout) device->destroyPipelineLayout(skinPassPipelineLayout);
381 skinPassPipelineLayout = nullptr;
382 skinPassSetLayoutUnique.reset();
383 skinPassSetLayout = nullptr;
384 destroyDecalResources();
385 destroySceneColorResources();
386 destroyUiColorResources();
387 auto destroyBuf = [&](vkb::GenericBuffer& b) { b.release(); };
388 for (auto& st : clusteredStorages) {
389 destroyBuf(st.lightsBuf);
390 destroyBuf(st.tableBuf);
391 destroyBuf(st.indicesBuf);
392 }
393 clusteredStorages.clear();
394 if (lit2dPipeline) device->destroyPipeline(lit2dPipeline);
395 if (lit2dAdditivePipeline) device->destroyPipeline(lit2dAdditivePipeline);
396 if (lit2dPremultipliedPipeline) device->destroyPipeline(lit2dPremultipliedPipeline);
397 if (lit2dMultiplyPipeline) device->destroyPipeline(lit2dMultiplyPipeline);
398 if (lit2dOpaquePipeline) device->destroyPipeline(lit2dOpaquePipeline);
399 if (offscreenLitPipeline) device->destroyPipeline(offscreenLitPipeline);
400 if (offscreenLitAdditivePipeline) device->destroyPipeline(offscreenLitAdditivePipeline);
401 if (offscreenLitPremultipliedPipeline) device->destroyPipeline(offscreenLitPremultipliedPipeline);
402 if (offscreenLitMultiplyPipeline) device->destroyPipeline(offscreenLitMultiplyPipeline);
403 if (offscreenLitOpaquePipeline) device->destroyPipeline(offscreenLitOpaquePipeline);
404 if (lit2dPipelineLayout) device->destroyPipelineLayout(lit2dPipelineLayout);
405 lit2dSetLayoutUnique.reset();
406 for (auto& m : lit2dSets) m.clear();
407 lit2dSets.clear();
408 offscreenLit2dSets.clear();
409 destroyBuf(offscreenLighting2dUbo);
410 if (offscreenSolidPipeline) device->destroyPipeline(offscreenSolidPipeline);
411 if (offscreenSolidAlphaPipeline) device->destroyPipeline(offscreenSolidAlphaPipeline);
412 if (offscreenAdditiveSolidPipeline) device->destroyPipeline(offscreenAdditiveSolidPipeline);
413 if (offscreenPremultipliedSolidPipeline) device->destroyPipeline(offscreenPremultipliedSolidPipeline);
414 if (offscreenMultiplySolidPipeline) device->destroyPipeline(offscreenMultiplySolidPipeline);
415 if (offscreenTexPipeline) device->destroyPipeline(offscreenTexPipeline);
416 if (offscreenAdditiveTexPipeline) device->destroyPipeline(offscreenAdditiveTexPipeline);
417 if (offscreenPremultipliedTexPipeline) device->destroyPipeline(offscreenPremultipliedTexPipeline);
418 if (offscreenMultiplyTexPipeline) device->destroyPipeline(offscreenMultiplyTexPipeline);
419 if (offscreenOpaqueTexPipeline) device->destroyPipeline(offscreenOpaqueTexPipeline);
420 if (offscreenRenderPass) device->destroyRenderPass(offscreenRenderPass);
421 if (hdrOffscreenTexPipeline) device->destroyPipeline(hdrOffscreenTexPipeline);
422 if (hdrOffscreenOpaqueTexPipeline) device->destroyPipeline(hdrOffscreenOpaqueTexPipeline);
423 if (hdrOffscreenAdditiveTexPipeline) device->destroyPipeline(hdrOffscreenAdditiveTexPipeline);
424 if (hdrOffscreenPremultipliedTexPipeline) device->destroyPipeline(hdrOffscreenPremultipliedTexPipeline);
425 if (hdrOffscreenMultiplyTexPipeline) device->destroyPipeline(hdrOffscreenMultiplyTexPipeline);
426 if (hdrOffscreenSolidPipeline) device->destroyPipeline(hdrOffscreenSolidPipeline);
427 if (hdrOffscreenSolidAlphaPipeline) device->destroyPipeline(hdrOffscreenSolidAlphaPipeline);
428 if (hdrOffscreenAdditiveSolidPipeline) device->destroyPipeline(hdrOffscreenAdditiveSolidPipeline);
429 if (hdrOffscreenPremultipliedSolidPipeline) device->destroyPipeline(hdrOffscreenPremultipliedSolidPipeline);
430 if (hdrOffscreenMultiplySolidPipeline) device->destroyPipeline(hdrOffscreenMultiplySolidPipeline);
431 if (hdrOffscreenLitPipeline) device->destroyPipeline(hdrOffscreenLitPipeline);
432 if (hdrOffscreenLitAdditivePipeline) device->destroyPipeline(hdrOffscreenLitAdditivePipeline);
433 if (hdrOffscreenLitPremultipliedPipeline) device->destroyPipeline(hdrOffscreenLitPremultipliedPipeline);
434 if (hdrOffscreenLitMultiplyPipeline) device->destroyPipeline(hdrOffscreenLitMultiplyPipeline);
435 if (hdrOffscreenLitOpaquePipeline) device->destroyPipeline(hdrOffscreenLitOpaquePipeline);
436 if (hdrOffscreenTonemapPipeline) device->destroyPipeline(hdrOffscreenTonemapPipeline);
437 if (hdrOffscreenParticleDistortionPipeline) device->destroyPipeline(hdrOffscreenParticleDistortionPipeline);
438 destroyPresentComposeResources();
439 if (hdrOffscreenRenderPass) device->destroyRenderPass(hdrOffscreenRenderPass);
440 texSetLayoutUnique.reset();
441 if (descriptorPool) device->destroyDescriptorPool(descriptorPool);
442 if (uploadPool) device->destroyCommandPool(uploadPool);
443 if (renderpass) device->destroyRenderPass(renderpass);
444 if (surface) inst.instance.destroySurfaceKHR(surface);
445}
446
447void Graphics::createInstanceAndDevice(const std::vector<const char*>& extNames, void* nativeWindow,
448 vk::SurfaceKHR* surfaceOut) {
449 (void)extNames;
450 if (static_cast<VkInstance>(inst.instance) == VK_NULL_HANDLE) inst = consumeWarmedInstance();
451
452 if (nativeWindow != nullptr && surfaceOut != nullptr) {
453 StartupStage stage(" vulkan: surface");
454 auto* window = static_cast<SDL_Window*>(nativeWindow);
455 VkSurfaceKHR rawSurface = VK_NULL_HANDLE;
456 if (!SDL_Vulkan_CreateSurface(window, static_cast<VkInstance>(inst.instance), &rawSurface))
457 throw Exception("SDL_Vulkan_CreateSurface failed: %s", SDL_GetError());
458 surface = rawSurface;
459 *surfaceOut = rawSurface;
460 }
461
462 {
463 StartupStage stage(" vulkan: physical device + device");
464 auto selector = inst.selectPhysicalDevice();
465 selector.set_minimum_version(1, 0);
466 if (surface) {
467 selector.set_surface(surface);
468 } else {
469 // Headless: pick a device without requiring a presentable surface.
470 // VKBuilder's instance.headless flag is not propagated from
471 // InstanceBuilder, so defer_surface_initialization() is the
472 // supported way to select a device with no VkSurfaceKHR.
473 selector.defer_surface_initialization();
474 }
475#if defined(EVENGINE_MACOSX) || defined(EVENGINE_IOS)
476 selector.add_required_extension("VK_KHR_portability_subset");
477#endif
478 // Soft-request KHR ray tracing. Devices without these extensions remain
479 // selectable; supported ones get the extensions into extensions_to_enable.
480 selector.add_desired_extension(VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME);
481 selector.add_desired_extension(VK_KHR_RAY_TRACING_PIPELINE_EXTENSION_NAME);
482 selector.add_desired_extension(VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME);
483 selector.add_desired_extension(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME);
484 selector.add_desired_extension(VK_KHR_SPIRV_1_4_EXTENSION_NAME);
485 selector.add_desired_extension(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME);
486 selector.add_desired_extension(VK_KHR_RAY_QUERY_EXTENSION_NAME);
487 auto phys = selector.select();
488 {
489 // Record the GPU identity into the crash/error log before any Vulkan
490 // work, so a device/driver crash can be tied to the exact GPU+driver.
491 const auto& pp = phys.properties;
492 const char* vendor = "unknown";
493 switch (pp.vendorID) {
494 case 0x10DE: vendor = "NVIDIA"; break;
495 case 0x1002: vendor = "AMD"; break;
496 case 0x8086: vendor = "Intel"; break;
497 case 0x13B5: vendor = "ARM"; break;
498 case 0x5143: vendor = "Qualcomm"; break;
499 default: break;
500 }
501 char vendorHex[16], deviceHex[16], driverHex[16];
502 std::snprintf(vendorHex, sizeof(vendorHex), "%04X", pp.vendorID);
503 std::snprintf(deviceHex, sizeof(deviceHex), "%04X", pp.deviceID);
504 std::snprintf(driverHex, sizeof(driverHex), "%08X", pp.driverVersion);
505 const uint32_t maj = VK_API_VERSION_MAJOR(pp.apiVersion);
506 const uint32_t min = VK_API_VERSION_MINOR(pp.apiVersion);
507 const uint32_t pat = VK_API_VERSION_PATCH(pp.apiVersion);
508 eve::recordLogEvent("info", std::string("gpu: ") + vendor + " '" + std::string(pp.deviceName.data()) +
509 "' vendorId=0x" + vendorHex + " deviceId=0x" + deviceHex +
510 " api=" + std::to_string(maj) + "." + std::to_string(min) + "." +
511 std::to_string(pat) + " driver=0x" + driverHex +
512 (nativeWindow ? "" : " [headless]"));
513 }
514 {
515 const vk::PhysicalDeviceFeatures supported = phys->getFeatures();
516 if (supported.samplerAnisotropy) {
517 phys.features.samplerAnisotropy = VK_TRUE;
518 maxSamplerAnisotropy = phys.properties.limits.maxSamplerAnisotropy;
519 if (maxSamplerAnisotropy < 1.f) maxSamplerAnisotropy = 1.f;
520 } else {
521 maxSamplerAnisotropy = 1.f;
522 }
523 // Probe Vulkan 1.2 GPU-driven capabilities (bindless tables + GPU
524 // cull chain). The vendored headers omit `computeShader` / draw
525 // features from VkPhysicalDeviceFeatures; compute is core and
526 // universal, so treat it as present.
527 gpuDrivenCaps_ = GpuDrivenCaps{};
528 vk::PhysicalDeviceVulkan12Features vk12{};
529 vk::PhysicalDeviceFeatures2 features2{};
530 vk12.sType = vk::StructureType::ePhysicalDeviceVulkan12Features;
531 features2.sType = vk::StructureType::ePhysicalDeviceFeatures2;
532 features2.pNext = &vk12;
533 phys->getFeatures2(&features2);
534 gpuDrivenCaps_.api12 = phys.properties.apiVersion >= VK_API_VERSION_1_2;
535 gpuDrivenCaps_.computeShader = true;
536 gpuDrivenCaps_.multiDrawIndirect = supported.multiDrawIndirect == VK_TRUE;
538 supported.shaderSampledImageArrayDynamicIndexing == VK_TRUE;
539 gpuDrivenCaps_.drawIndirectCount = vk12.drawIndirectCount == VK_TRUE;
540 gpuDrivenCaps_.descriptorIndexing = vk12.descriptorIndexing == VK_TRUE;
541 gpuDrivenCaps_.samplerArrayCapacity =
542 phys.properties.limits.maxPerStageDescriptorSamplers >= kMaxBindlessTextures;
543 if (gpuDrivenCaps_.gpuDrivenAvailable()) {
544 phys.features.shaderSampledImageArrayDynamicIndexing = VK_TRUE;
545 phys.features.shaderStorageBufferArrayDynamicIndexing = VK_TRUE;
546 phys.features.multiDrawIndirect = VK_TRUE;
547 }
548 }
549 // Optional KHR ray tracing: soft-request extensions via desired list
550 // (already added before select). Probe features here; enable them only
551 // when every required extension is present on the physical device.
552 // extensions_to_enable is private to vk-bootstrap — re-enumerate instead.
553 rayTracingCaps_ = RayTracingCaps{};
554 vk::PhysicalDeviceAccelerationStructureFeaturesKHR rtAsEnable{};
555 vk::PhysicalDeviceRayTracingPipelineFeaturesKHR rtPipeEnable{};
556 vk::PhysicalDeviceRayQueryFeaturesKHR rtQueryEnable{};
557 bool enableRtFeatures = false;
558 {
559 const auto extProps = phys->enumerateDeviceExtensionProperties();
560 auto hasExt = [&](const char* name) {
561 for (const auto& p : extProps) {
562 if (std::strcmp(p.extensionName, name) == 0) return true;
563 }
564 return false;
565 };
566 const bool extsOk = hasExt(VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME) &&
567 hasExt(VK_KHR_RAY_TRACING_PIPELINE_EXTENSION_NAME) &&
568 hasExt(VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME);
569 const bool api12 = phys.properties.apiVersion >= VK_API_VERSION_1_2;
570 const bool bdaExt = hasExt(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME) || api12;
571 const bool spirv14 = hasExt(VK_KHR_SPIRV_1_4_EXTENSION_NAME) || api12;
572
573 vk::PhysicalDeviceBufferDeviceAddressFeatures bdaFeat{};
574 bdaFeat.sType = vk::StructureType::ePhysicalDeviceBufferDeviceAddressFeatures;
575 vk::PhysicalDeviceAccelerationStructureFeaturesKHR asFeat{};
576 asFeat.sType = vk::StructureType::ePhysicalDeviceAccelerationStructureFeaturesKHR;
577 asFeat.pNext = &bdaFeat;
578 vk::PhysicalDeviceRayTracingPipelineFeaturesKHR rtFeat{};
579 rtFeat.sType = vk::StructureType::ePhysicalDeviceRayTracingPipelineFeaturesKHR;
580 rtFeat.pNext = &asFeat;
581 vk::PhysicalDeviceFeatures2 features2Rt{};
582 features2Rt.sType = vk::StructureType::ePhysicalDeviceFeatures2;
583 features2Rt.pNext = &rtFeat;
584 phys->getFeatures2(&features2Rt);
585
586 vk::PhysicalDeviceRayQueryFeaturesKHR rqFeat{};
587 rqFeat.sType = vk::StructureType::ePhysicalDeviceRayQueryFeaturesKHR;
588 if (hasExt(VK_KHR_RAY_QUERY_EXTENSION_NAME)) {
589 // Probe rayQuery separately so its feature struct is not required
590 // on devices that only expose the acceleration-structure path.
591 vk::PhysicalDeviceFeatures2 features2Rq{};
592 features2Rq.sType = vk::StructureType::ePhysicalDeviceFeatures2;
593 features2Rq.pNext = &rqFeat;
594 phys->getFeatures2(&features2Rq);
595 }
596
597 const bool featuresOk = rtFeat.rayTracingPipeline == VK_TRUE && asFeat.accelerationStructure == VK_TRUE &&
598 bdaFeat.bufferDeviceAddress == VK_TRUE;
599
600 if (extsOk && bdaExt && spirv14 && featuresOk) {
601 rayTracingCaps_.accelerationStructure = true;
602 rayTracingCaps_.rayTracingPipeline = true;
603 rayTracingCaps_.bufferDeviceAddress = true;
604 rayTracingCaps_.available = true;
605 if (hasExt(VK_KHR_RAY_QUERY_EXTENSION_NAME) && rqFeat.rayQuery == VK_TRUE)
606 rayTracingCaps_.rayQuery = true;
607
608 vk::PhysicalDeviceRayTracingPipelinePropertiesKHR rtProps{};
609 rtProps.sType = vk::StructureType::ePhysicalDeviceRayTracingPipelinePropertiesKHR;
610 vk::PhysicalDeviceProperties2 props2{};
611 props2.sType = vk::StructureType::ePhysicalDeviceProperties2;
612 props2.pNext = &rtProps;
613 phys->getProperties2(&props2);
614 rayTracingCaps_.shaderGroupHandleSize = rtProps.shaderGroupHandleSize;
615 rayTracingCaps_.shaderGroupBaseAlignment = rtProps.shaderGroupBaseAlignment;
616 rayTracingCaps_.shaderGroupHandleAlignment = rtProps.shaderGroupHandleAlignment;
617 rayTracingCaps_.maxRecursionDepth = rtProps.maxRayRecursionDepth;
618
619 // VUID-02830: do not chain VkPhysicalDeviceBufferDeviceAddressFeatures
620 // alongside VkPhysicalDeviceVulkan12Features. Enable BDA via the 1.2
621 // feature struct below; only AS / RT pipeline / optional rayQuery here.
622 rtAsEnable.sType = vk::StructureType::ePhysicalDeviceAccelerationStructureFeaturesKHR;
623 rtAsEnable.accelerationStructure = VK_TRUE;
624 rtPipeEnable.sType = vk::StructureType::ePhysicalDeviceRayTracingPipelineFeaturesKHR;
625 rtPipeEnable.rayTracingPipeline = VK_TRUE;
626 rtPipeEnable.pNext = &rtAsEnable;
627 if (rayTracingCaps_.rayQuery) {
628 rtQueryEnable.sType = vk::StructureType::ePhysicalDeviceRayQueryFeaturesKHR;
629 rtQueryEnable.rayQuery = VK_TRUE;
630 rtAsEnable.pNext = &rtQueryEnable;
631 }
632 enableRtFeatures = true;
633 }
634 // When features are missing, desired RT extensions may still be
635 // listed on the device; they stay inert without the feature bits.
636 }
637
638 vkb::DeviceBuilder deviceBuilder = phys.createDevice();
639 // Vulkan 1.2 feature: vkCmdDrawIndirectCount (VG cluster draws) and BDA for RT.
640 vk::PhysicalDeviceVulkan12Features vk12Enable{};
641 vk12Enable.sType = vk::StructureType::ePhysicalDeviceVulkan12Features;
642 if (gpuDrivenCaps_.drawIndirectCount) vk12Enable.drawIndirectCount = VK_TRUE;
643 if (enableRtFeatures) {
644 vk12Enable.bufferDeviceAddress = VK_TRUE;
645 deviceBuilder.add_pNext(&rtPipeEnable);
646 }
647 deviceBuilder.add_pNext(&vk12Enable);
648 device = deviceBuilder.build();
649 // Load device-level extension entry points (needed for KHR RT).
650 VULKAN_HPP_DEFAULT_DISPATCHER.init(device.instance);
651#if defined(VKB_ENABLE_VMA)
652 vmaAllocatorOwner_.create(inst, phys, device);
653#endif
654 maxSamplerAnisotropy = device.caps.maxSamplerAnisotropy;
655 eve::recordLogEvent("info",
656 "gpu: logical device created (gpuDriven=" +
657 std::string(gpuDrivenCaps_.gpuDrivenAvailable() ? "on" : "off") +
658 ", rayTracing=" + std::string(rayTracingCaps_.rayTracingAvailable() ? "on" : "off") +
659#if defined(VKB_ENABLE_VMA)
660 ", allocator=VMA" +
661#else
662 ", allocator=native" +
663#endif
664 ", maxAniso=" + std::to_string(maxSamplerAnisotropy) + ")");
665 }
666}
667
669 StartupStage initStage("graphics: initHeadless (total)");
670 if (initialized) {
671 if (headless_) {
672 // Re-entrant: test cases share the process-wide Graphics singleton.
673 // Update the logical viewport and keep the existing device.
675 return;
676 }
677 throw Exception("Graphics::initHeadless: already initialized with a window");
678 }
679 if (width <= 0 || height <= 0) throw Exception("Graphics::initHeadless: invalid size");
680
681 // No window, no surface extensions, no swapchain: a bare Vulkan device that
682 // renders into offscreen canvases and reads pixels back on the CPU.
683 createInstanceAndDevice({}, nullptr, nullptr);
684
685 uploadPool = device.createCommandPool();
687 headless_ = true;
688
689 {
690 StartupStage stage(" vulkan: headless renderpass + pipelines");
691 // Plain color+depth render pass mirroring the swapchain pass format so
692 // the shared pipeline builders can reuse it; nothing is ever presented.
693 vkb::RenderPassBuilder rpBuilder{device};
694 renderpass =
695 rpBuilder.addPresentAttachment(vk::Format::eB8G8R8A8Unorm, vk::AttachmentLoadOp::eClear)
696 .addDepthAttachment(depthFormat, vk::AttachmentLoadOp::eClear, vk::AttachmentStoreOp::eDontCare)
697 .addSubpass(vkb::SubpassBuilder()
698 .addAttachmentRef(0, vk::ImageLayout::eColorAttachmentOptimal)
699 .setDepthStencilAttachment(1, vk::ImageLayout::eDepthStencilAttachmentOptimal))
700 .addDependency(
701 VK_SUBPASS_EXTERNAL, 0,
702 vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests,
703 vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests,
704 {},
705 vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite |
706 vk::AccessFlagBits::eDepthStencilAttachmentWrite)
707 .build();
708 depthImage = vkb::DepthStencilImage{device, uint32_t(width), uint32_t(height), depthFormat};
709 pipelineLayout = createPipelineLayout(device);
710 createTexturedPipeline();
711 createMesh3DPipeline();
712 createMesh3DClusteredPipeline();
713 createVoxelRectPipeline();
714 }
715
716 initialized = true;
717 {
718 StartupStage stage(" vulkan: shadow resources (3x2048^2 maps + pipelines)");
719 createShadowResources();
720 }
721 {
722 StartupStage stage(" vulkan: white texture");
723 const uint8_t whitePixel[4] = {255, 255, 255, 255};
724 whiteTexture = newTexture(1, 1, whitePixel);
725 const std::vector<uint8_t> cubePx(6 * 4, 255);
726 defaultBindlessCube = newCubemap(1, cubePx.data());
727 }
728 {
729 StartupStage stage(" vulkan: gpu-driven bindless set + pipeline");
730 initGpuDrivenResources();
731 }
732 eve::recordLogEvent("info", "gfx: graphics initialized (headless)");
733}
734
735void Graphics::initWithWindow(void* nativeWindow) {
736 eve::cap::provide<eve::service::IGpuTimer>(this);
737 StartupStage initStage("graphics: initWithWindow (total)");
738 if (initialized) {
739 // Window module may destroy/recreate the SDL window (tests, setWindowSettings).
740 // The Vulkan surface is tied to the native window. SDL can hand back the
741 // same pointer for a freshly recreated window, so pointer identity alone
742 // can't tell whether the surface is still valid: Window::close() drops it
743 // via onNativeWindowDestroyed(), and a missing surface must be rebuilt too.
744 if (sdlWindow == nativeWindow && surface) return;
745 sdlWindow = nativeWindow;
746 recreateSurfaceForResume();
747 // Restore the initWithWindow contract (device + swapchain valid on
748 // return): rebuild the swapchain synchronously. On desktop this runs
749 // immediately; on Android/iOS isRenderSurfaceStable() defers it until
750 // the native surface settles, leaving swapchainDirty set (same as before).
751 rebuildSwapchainIfNeeded();
752 return;
753 }
754 sdlWindow = nativeWindow;
755 auto* window = static_cast<SDL_Window*>(nativeWindow);
756 ASSERT(window != nullptr);
757 if (!window) throw Exception("Graphics::initWithWindow: null SDL_Window");
758
759 unsigned int count = 0;
760 if (!SDL_Vulkan_GetInstanceExtensions(window, &count, nullptr))
761 throw Exception("SDL_Vulkan_GetInstanceExtensions failed: %s", SDL_GetError());
762
763 std::vector<const char*> extNames(count);
764 if (!SDL_Vulkan_GetInstanceExtensions(window, &count, extNames.data()))
765 throw Exception("SDL_Vulkan_GetInstanceExtensions failed: %s", SDL_GetError());
766
767 createInstanceAndDevice(extNames, nativeWindow, &surface);
768
769 uploadPool = device.createCommandPool();
770
771 int pw = 0, ph = 0;
772 SDL_Vulkan_GetDrawableSize(window, &pw, &ph);
773 int lw = 0, lh = 0;
774 SDL_GetWindowSize(window, &lw, &lh);
775 setViewportSize(lw, lh, pw, ph);
776
777 {
778 StartupStage stage(" vulkan: swapchain + solid pipelines");
779 createSwapchainAndPipeline();
780 eve::recordLogEvent("info", "gfx: swapchain + solid pipelines created");
781 }
782 {
783 StartupStage stage(" vulkan: textured/lit2d pipelines");
784 createTexturedPipeline();
785 }
786 {
787 StartupStage stage(" vulkan: mesh3d pipeline");
788 createMesh3DPipeline();
789 }
790 {
791 StartupStage stage(" vulkan: clustered pipeline");
792 createMesh3DClusteredPipeline();
793 }
794 {
795 StartupStage stage(" vulkan: voxel pipeline");
796 createVoxelRectPipeline();
797 }
798 initialized = true;
799 {
800 StartupStage stage(" vulkan: shadow resources (3x2048^2 maps + pipelines)");
801 createShadowResources();
802 }
803 {
804 StartupStage stage(" vulkan: white texture");
805 const uint8_t whitePixel[4] = {255, 255, 255, 255};
806 whiteTexture = newTexture(1, 1, whitePixel);
807 const std::vector<uint8_t> cubePx(6 * 4, 255);
808 defaultBindlessCube = newCubemap(1, cubePx.data());
809 }
810 {
811 StartupStage stage(" vulkan: gpu-driven bindless set + pipeline");
812 initGpuDrivenResources();
813 }
814 eve::recordLogEvent("info", "gfx: graphics initialized (window)");
815}
816
818 if (!initialized) return;
819 if (swapchainPassOpen || sceneColorPassOpen) abortOpen3DFrame();
820 // Fire registered window-destroyed callbacks (UI backend tears down its
821 // ImGui context here) before the device/surface resources are released.
822 for (auto& cb : windowDestroyedCallbacks_) cb.first(cb.second);
825
826 device->waitIdle();
827 destroySwapchainResources();
828 swapchain.destroy();
829 swapchain = vkb::Swapchain{};
830 if (surface) {
831 inst.instance.destroySurfaceKHR(surface);
832 surface = VK_NULL_HANDLE;
833 }
834 sdlWindow = nullptr;
835 swapchainDirty = true;
836}
837
838void Graphics::destroySwapchainResources() {
839 destroyPbrResources();
840 presentRecording = {};
841 swapchainPass = {};
842 presentModel.destroy();
843 presentModel = vkb::Present{};
844 destroyGpuDrivenCullResources();
845 depthImage = vkb::DepthStencilImage{};
846 destroyReadbackResources();
847 // Release reused per-frame vertex buffers. Callers hold a device-wide
848 // waitIdle before this runs.
849 for (auto& fb : frame2dBuffers) releaseFrame2dBuffers(fb);
850 frame2dBuffers.clear();
851 releaseFrame2dBuffers(offscreenBuffers);
852 // Per-frame lit-2D UBOs are keyed to the in-flight slot count; release them
853 // here (slot count may change across recreation).
854 for (auto& ubo : lighting2dUboSlots) ubo.release();
855 lighting2dUboSlots.clear();
856 // Descriptor sets cached against the released UBO handles must be dropped;
857 // they live in the shared descriptor pool, so only the cache is cleared.
858 lit2dSets.clear();
859}
860
861uint32_t Graphics::frameSlotCount() const {
862 uint32_t n = presentModel.frames_in_flight;
863 if (n == 0) n = swapchain.image_count;
864 return n ? n : 1u;
865}
866
867size_t Graphics::currentFrameSlot() const {
868 const uint32_t n = frameSlotCount();
869 size_t slot = presentRecording ? presentRecording.slot().index : swapchain.current_frame;
870 return (n > 0 && slot >= n) ? 0 : slot;
871}
872
873vk::CommandBuffer& Graphics::currentPresentCb() {
874 if (offscreen3DPassOpen && offscreen3DCB) return offscreen3DCB;
875 if (swapchainPass) return swapchainPass.commandBuffer();
876 return presentRecording.commandBuffer();
877}
878
879vkb::FrameSlot Graphics::frameToken() const {
880 if (offscreen3DPassOpen) return vkb::FrameSlot{0};
881 if (presentRecording) return presentRecording.slot();
882 return vkb::FrameSlot::gpuIdle();
883}
884
885void Graphics::waitForSharedGpuResources() { presentModel.waitForAllFrames(); }
886
887void Graphics::invalidateTextureBindings() {
888 for (auto& frame : mesh3dFrameSlots) frame.sets.clear();
889 for (auto& frame : mesh3dClusteredFrameSlots) frame.sets.clear();
890 for (auto& m : lit2dSets) m.clear();
891 offscreenLit2dSets.clear();
892 post2Sets.clear();
893 voxelRectSets.clear();
894}
895
896Graphics::Frame2DBuffers& Graphics::currentFrame2DBuffers() {
897 return currentSlot(frame2dBuffers, frameSlotCount(), currentFrameSlot());
898}
899
900Graphics::Mesh3dFrameSlots& Graphics::currentMesh3dFrameSlots() {
901 return currentSlot(mesh3dFrameSlots, frameSlotCount(), currentFrameSlot());
902}
903
904Graphics::Mesh3dClusteredFrameSlots& Graphics::currentMesh3dClusteredFrameSlots() {
905 return currentSlot(mesh3dClusteredFrameSlots, frameSlotCount(), currentFrameSlot());
906}
907
908vkb::GenericBuffer& Graphics::currentLighting2dUbo() {
909 auto& ubo = currentSlot(lighting2dUboSlots, frameSlotCount(), currentFrameSlot());
910 // allocate() reuses the slot's buffer when it already fits (same usage/
911 // memflags, fixed sizeof(Lighting2DUBO)), so this is a no-op after first use.
912 ubo.allocate(frameToken(), device, vk::BufferUsageFlagBits::eUniformBuffer, sizeof(Lighting2DUBO),
913 vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
914 return ubo;
915}
916
917std::unordered_map<Graphics::LitSetKey, vkb::BoundSet, Graphics::LitSetKeyHash>& Graphics::currentLit2dSets() {
918 return currentSlot(lit2dSets, frameSlotCount(), currentFrameSlot());
919}
920
921Graphics::ClusteredStorage& Graphics::currentClusteredStorage() {
922 return currentSlot(clusteredStorages, frameSlotCount(), currentFrameSlot());
923}
924
925Graphics::VoxelInstanceFrame& Graphics::currentVoxelInstanceFrame() {
926 return currentSlot(voxelInstanceFrames, frameSlotCount(), currentFrameSlot());
927}
928
929Graphics::ShadowMapSlot& Graphics::currentShadowMap() {
930 ASSERT(!shadowMaps.empty());
931 return shadowMaps[currentFrameSlot() % shadowMaps.size()];
932}
933
934vk::ImageView Graphics::currentShadowArrayView() {
935 if (shadowMaps.empty()) return vk::ImageView{};
936 return currentShadowMap().image.arrayView();
937}
938
939Graphics::GBufferSlot* Graphics::currentGBufferSlot() {
940 if (gbufferSlots.empty()) return nullptr;
941 return &gbufferSlots[currentFrameSlot() % gbufferSlots.size()];
942}
943
945 if (auto* slot = currentGBufferSlot(); slot && gbufferPending && slot->depthColorTex.gpuHandle)
946 return &slot->depthColorTex;
947 GBuffer* gbuffer = renderControl_ ? renderControl_->getGBuffer() : nullptr;
948 return gbuffer && gbuffer->isValid() ? gbuffer->getDepthTexture() : nullptr;
949}
950
951Graphics::DecalSlot* Graphics::currentDecalSlot() {
952 if (decalSlots.empty()) return nullptr;
953 return &decalSlots[currentFrameSlot() % decalSlots.size()];
954}
955
956Graphics::SceneColorSlot* Graphics::currentSceneColorSlot() {
957 if (sceneColorSlots.empty()) return nullptr;
958 return &sceneColorSlots[currentFrameSlot() % sceneColorSlots.size()];
959}
960
962 auto* slot = currentSceneColorSlot();
963 if (!slot || !slot->colorTex.gpuHandle) return nullptr;
964 return &slot->colorTex;
965}
966
967Graphics::UiColorSlot* Graphics::currentUiColorSlot() {
968 if (uiColorSlots.empty()) return nullptr;
969 return &uiColorSlots[currentFrameSlot() % uiColorSlots.size()];
970}
971
972void Graphics::dropPendingOffscreenPasses() {
973 shadowPendingMask = 0;
974 for (auto& d : shadowCascadeDraws) d.clear();
975 gbufferPending = false;
976 gbufferPassDraws.clear();
977 decalPending = false;
978 decalPassDraws.clear();
979}
980
981bool Graphics::beginSwapchainRenderPass() {
982 if (!beginPresentCommandBuffer()) {
983 dropPendingOffscreenPasses();
984 return false;
985 }
986 recordDeferredFrameGraph();
987 beginSwapchainColorPass();
988 return true;
989}
990
991void Graphics::beginSwapchainColorPass() {
992 std::array<vk::ClearValue, 2> clears{};
993 clears[0].color = vk::ClearColorValue(std::array<float, 4>{clearColor.r, clearColor.g, clearColor.b, clearColor.a});
994 clears[1].depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
995 swapchainPass = presentRecording.beginRenderPass(renderpass, clears.data(), uint32_t(clears.size()));
996}
997
998bool Graphics::rebuildSwapchainIfNeeded() {
999 const bool wantRecreate = surfaceNeedsRecreate.load() || swapchainDirty || presentModel.needs_recreate;
1000 if (!wantRecreate) return true;
1001 if (!isRenderSurfaceStable()) return false;
1002
1003 if (surfaceNeedsRecreate.exchange(false)) recreateSurfaceForResume();
1004 if (presentModel.needs_recreate) {
1005 presentModel.needs_recreate = false;
1006 swapchainDirty = true;
1007 }
1008 if (swapchainDirty) {
1009 device->waitIdle();
1010 createSwapchainAndPipeline();
1011 }
1012 return true;
1013}
1014
1015bool Graphics::beginPresentCommandBuffer() {
1016 for (int attempt = 0; attempt < 3; ++attempt) {
1017 if (!rebuildSwapchainIfNeeded()) return false;
1018 presentRecording = presentModel.begin();
1019 if (presentRecording) {
1021 return true;
1022 }
1023 // Acquire failed without beginning the CB (see Present::begin). Rebuild once.
1024 if (presentModel.needs_recreate) {
1025 presentModel.needs_recreate = false;
1026 swapchainDirty = true;
1027 continue;
1028 }
1029 return false;
1030 }
1031 return false;
1032}
1033
1034bool Graphics::isRenderSurfaceReady() const {
1035#if defined(EVENGINE_ANDROID) || defined(EVENGINE_IOS)
1036 auto* window = static_cast<SDL_Window*>(sdlWindow);
1037 if (!window) return false;
1038 // During background/resume and orientation changes the native window is
1039 // torn down and rebuilt; SDL reports a zero drawable size until it settles.
1040 int pw = 0, ph = 0;
1041 SDL_Vulkan_GetDrawableSize(window, &pw, &ph);
1042 return pw > 0 && ph > 0;
1043#else
1044 return true;
1045#endif
1046}
1047
1048bool Graphics::isRenderSurfaceStable() {
1049#if defined(EVENGINE_ANDROID) || defined(EVENGINE_IOS)
1050 auto* window = static_cast<SDL_Window*>(sdlWindow);
1051 if (!window) return false;
1052 int pw = 0, ph = 0;
1053 SDL_Vulkan_GetDrawableSize(window, &pw, &ph);
1054 if (pw <= 0 || ph <= 0) {
1055 surfaceStableFrames = 0;
1056 return false;
1057 }
1058 if (pw != pendingSurfaceW || ph != pendingSurfaceH) {
1059 // Size still changing (mid-rotation / Split View); wait for it to settle.
1060 pendingSurfaceW = pw;
1061 pendingSurfaceH = ph;
1062 surfaceStableFrames = 1;
1063 return false;
1064 }
1065 // Require a couple of consecutive identical polls before rebuilding.
1066 if (surfaceStableFrames < 3) {
1067 ++surfaceStableFrames;
1068 return false;
1069 }
1070 return true;
1071#else
1072 return true;
1073#endif
1074}
1075
1076void Graphics::recreateSurfaceForResume() {
1077 // Mobile platforms destroy the native window when backgrounded, which
1078 // invalidates the VkSurfaceKHR. On resume SDL creates a fresh native
1079 // window, so we must rebuild the surface (and the swapchain that depends on
1080 // it) before presenting again. Runs on the render thread.
1081 if (!initialized) return;
1082 auto* window = static_cast<SDL_Window*>(sdlWindow);
1083 if (!window) return;
1084
1085 device->waitIdle();
1086
1087 // Tear down surface-dependent resources first.
1088 destroySwapchainResources();
1089 swapchain.destroy();
1090 swapchain = vkb::Swapchain{};
1091 if (surface) {
1092 inst.instance.destroySurfaceKHR(surface);
1093 surface = VK_NULL_HANDLE;
1094 }
1095
1096 // Create a new surface from the freshly created native window.
1097 VkSurfaceKHR rawSurface = VK_NULL_HANDLE;
1098 if (!SDL_Vulkan_CreateSurface(window, static_cast<VkInstance>(inst.instance), &rawSurface))
1099 throw Exception("resume: SDL_Vulkan_CreateSurface failed: %s", SDL_GetError());
1100 surface = rawSurface;
1101 device.surface = surface;
1102
1103 // Refresh drawable size (orientation / Split View may have changed).
1104 int pw = 0, ph = 0, lw = 0, lh = 0;
1105 SDL_Vulkan_GetDrawableSize(window, &pw, &ph);
1106 SDL_GetWindowSize(window, &lw, &lh);
1107 if (pw > 0 && ph > 0) setViewportSize(lw, lh, pw, ph);
1108
1109 swapchainDirty = true;
1110}
1111
1113 if (!initialized) return;
1114 if (headless_) return;
1115 if (!isActive()) return;
1116 if (isCanvasActive()) throw Exception("present: cannot present while a Canvas is active");
1117 if (!isRenderSurfaceReady()) {
1118 // Native window is mid-(re)creation / rotation; drop this frame's work
1119 // instead of touching an invalid swapchain (which crashes the driver).
1120 if (swapchainPassOpen)
1121 abortOpen3DFrame();
1122 else
1123 clear2DBatches();
1124 hasPendingClear = false;
1125 return;
1126 }
1127 flushBatch();
1128}
1129
1130void Graphics::draw(eve::graphics::Graphics*, const glm::mat4&) const {}
1131void Graphics::draw(Canvas*, const glm::mat4&) const {}
1132
1133// ---- GPU frame timing ------------------------------------------------------
1134
1136 if (gpuQueryPool_) return;
1137 try {
1138 timestampPeriod_ = device.physical_device.properties.limits.timestampPeriod;
1139 vk::QueryPoolCreateInfo ci;
1140 ci.queryType = vk::QueryType::eTimestamp;
1141 ci.queryCount = 2;
1142 gpuQueryPool_ = device->createQueryPool(ci);
1143 gpuTimingReady_ = true;
1144 } catch (...) {
1145 gpuTimingReady_ = false;
1146 gpuQueryPool_ = nullptr;
1147 }
1148}
1149
1151 if (!gpuTimingReady_ || !presentRecording) return;
1152 vk::CommandBuffer cb = presentRecording.commandBuffer();
1153 cb.resetQueryPool(gpuQueryPool_, 0, 2);
1154 cb.writeTimestamp(vk::PipelineStageFlagBits::eTopOfPipe, gpuQueryPool_, 0);
1155}
1156
1158 if (!gpuTimingReady_ || !presentRecording) return;
1159 vk::CommandBuffer cb = presentRecording.commandBuffer();
1160 cb.writeTimestamp(vk::PipelineStageFlagBits::eBottomOfPipe, gpuQueryPool_, 1);
1161}
1162
1164 if (!gpuTimingReady_ || !gpuQueryPool_) return;
1165 // drawFrame() waits on the in-flight fence, so results are valid here.
1166 std::array<uint64_t, 2> ts{};
1167 const auto r = device->getQueryPoolResults(gpuQueryPool_, 0, 2, sizeof(ts), ts.data(), sizeof(uint64_t),
1168 vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWait);
1169 if (r == vk::Result::eSuccess && ts[1] > ts[0])
1170 gpuFrameMs_ = float(double(ts[1] - ts[0]) * double(timestampPeriod_) / 1'000'000.0);
1171 else
1172 gpuFrameMs_ = 0.f;
1173}
1174
1175bool Graphics::gpuTimingAvailable() const { return gpuTimingReady_ && gpuFrameMs_ > 0.f; }
1176
1177float Graphics::gpuFrameMs() const { return gpuFrameMs_; }
1178
1179} // namespace eve::graphics::vulkan
float w
Definition AnimClip.cpp:738
Persistent, thread-safe record of engine crash and problem events.
glm::vec4 p[6]
tensor::Graph g
Definition GpuGraph.cpp:7
vkb::Device & device
glm::vec3 n
Definition Grass.cpp:63
double r
std::uint32_t height
std::uint32_t width
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
std::function< eve::Result< void >()> release
Definition Procgen.cpp:86
float d
std::string string
std::uint32_t count
float m[16]
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
StartupStage public API.
Canvas public API.
Definition Canvas.h:17
Screen-space buffers for mid/post effects and Hybrid deferred lighting.
Definition GBuffer.h:28
Texture * getDepthTexture() const
Returns the depth texture.
Definition GBuffer.h:51
bool isValid() const
True when valid.
Definition GBuffer.cpp:5
std::vector< std::pair< WindowDestroyedCallback, void * > > windowDestroyedCallbacks_
Definition Graphics.h:2261
std::unique_ptr< AntiAliasing > pipelineAA_
Definition Graphics.h:2269
std::vector< DeferredFileTexture > deferredFileTextures_
Definition Graphics.h:2239
void retireResourceLifetime() const
Retire resource lifetime.
void clearPresentOverlay()
Drop the present overlay callback (window gone; re-register on UI init).
Definition Graphics.h:1584
std::unique_ptr< RenderControl > renderControl_
Definition Graphics.h:2264
bool isActive() const
True when active.
Definition Graphics.h:1551
std::unique_ptr< GlobalIllumination > pipelineGI_
Definition Graphics.h:2267
std::unique_ptr< AmbientOcclusion > pipelineAO_
Definition Graphics.h:2266
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:18
void onNativeWindowDestroyed() override
On native window destroyed.
Definition Graphics.cpp:817
void draw(eve::graphics::Graphics *gfx, const glm::mat4 &matrix) const override
Draws .
void present() override
Present.
Texture * getSceneColorTexture() override
Returns the scene color texture.
Definition Graphics.cpp:961
std::string getBackendName() const override
Returns the backend name.
Definition Graphics.cpp:292
void waitForSharedGpuResources()
Block until all in-flight GPU work (all frames) has completed.
Definition Graphics.cpp:885
bool isCanvasActive() const override
True when canvas active.
void initWithWindow(void *nativeWindow) override
Initializes with window.
Definition Graphics.cpp:735
void writeGpuTimestampEnd()
Writes an end timestamp into the current present command buffer.
void setViewportSize(int width, int height, int pixelwidth, int pixelheight) override
Sets the viewport size.
float gpuFrameMs() const override
Gpu frame ms.
Graphics()
Starts asynchronous VkInstance creation (overlaps later window init).
bool gpuTimingAvailable() const override
Gpu timing available.
bool supportsRuntimeGlslCompilation() const override
Supports runtime glsl compilation.
Definition Graphics.cpp:294
Texture * getSceneLinearDepthTexture() override
Returns the scene linear depth texture.
Definition Graphics.cpp:944
Texture * newCubemap(int faceSize, const uint8_t *rgbaFaces) override
Creates a cubemap. @ownership Caller deletes unless documented otherwise.
void readGpuFrameTiming()
Reads the query results after the frame's submit has completed.
void initGpuTiming()
Creates the GPU timestamp query pool; call after device init.
Texture * newTexture(int width, int height, const uint8_t *rgba, bool repeatU=false, bool repeatV=false) override
Creates a texture. @ownership Caller deletes unless documented otherwise.
void initHeadless(int width, int height) override
Initializes headless.
Definition Graphics.cpp:668
~Graphics() override
Graphics.
Definition Graphics.cpp:298
void clear(std::optional< Color > color, std::optional< int > stencil, std::optional< double > depth) override
Clears .
void writeGpuTimestampBegin()
Writes a begin timestamp into the current present command buffer.
EVENGINE_API_FOUNDATION void registerVulkanInstanceWarmup(void(*start)())
Register the graphics-backend function that starts VkInstance creation.
constexpr uint32_t kMaxBindlessTextures
Definition GpuDriven.h:19
bool glslRuntimeCompilationAvailable()
Whether this build can compile GLSL text to SPIR-V at runtime.
void detachGraphicsArtifactProvider(Graphics *graphics) noexcept
Detach a Graphics instance before its derived backend is destroyed.
WidgetDesc window(std::string title, std::vector< WidgetDesc > children, std::string id)
Top-level window widget with a title bar.
Definition Widget.cpp:292
void recordLogEvent(const std::string &level, const std::string &message)
Appends a timestamped event line to the crash/error log.
Definition CrashLog.cpp:70
bool available
Full KHR RT pipeline path usable
bool rayQuery
Optional VK_KHR_ray_query
bool gpuDrivenAvailable() const
Gpu driven available.
Definition GpuDriven.h:41
bool started
Definition Graphics.cpp:183
bool consumed
Definition Graphics.cpp:184
std::mutex mu
Definition Graphics.cpp:181
std::future< vkb::Instance > future
Definition Graphics.cpp:182