Vulkan Schnee 0.0.1
High-performance rendering engine
Loading...
Searching...
No Matches
Engine.cpp
Go to the documentation of this file.
5#include "glm/gtc/type_ptr.inl"
7
8#include <cstring>
10#include <GLFW/glfw3.h>
11#include <iostream>
12#include <stdexcept>
13#include <string>
14#include <vector>
15#include <vulkan/vulkan_core.h>
16
17#ifdef RENDERDOC_ENABLED
18 #include "renderdoc_app.h"
19#endif
20
21#include <algorithm>
22#include <thread>
23
25#define XR_LOG_STRING
27#include "Engine/Ecs/EcsData.h"
28#include "Engine/Entity/Scene.h"
29#include "Engine/Files/Path.h"
32
36#include <Engine/Core/Ticker.h>
40#include <Engine/Mesh/Mesh.h>
43#include <execution>
44#include <filesystem>
45#include <chrono>
46#include <iomanip>
47#include <sstream>
48#include <plog/Appenders/ColorConsoleAppender.h>
49#include <plog/Appenders/RollingFileAppender.h>
50#include <plog/Formatters/TxtFormatter.h>
51#include <plog/Init.h>
52#include <plog/Log.h>
53#include <plog/Severity.h>
54#ifdef RENDERDOC_ENABLED
56#endif
57
58namespace EngineCore {
61
63#ifdef RENDERDOC_ENABLED
64 auto renderDoc = RenderDoc::get();
65 if (renderDoc.init()) {
66 renderDoc.printStatus();
67 renderDoc.setCapturePath("./vr_captures/debug_frame_{0}.rdc");
68 renderDoc.enableOverlay(true);
69 }
70#endif
71 }
72
73 void Engine::run(std::unique_ptr<GameModule> module) {
74 this->module = std::move(module);
75
76 // Create named thread pool for XR frame operations (visible in Tracy as "XR Frame 1")
77 xrFrameThreadPool_ = std::make_unique<NamedThreadPool>(1, "XR Frame");
78
79 // init glfw
80 initWindow();
82 initVulkan();
84 this->module->initUI(mirrorView->getGlfwWindow());
85 this->module->startup();
86 mainLoop();
87 PLOGI << "Shutting down module...";
88 this->module->shutdown();
89 PLOGI << "Module shutdown complete. Destroying physics world...";
91 PLOGI << "Physics world destroyed. Starting cleanup...";
92 cleanup();
93 PLOGI << "Cleanup complete.";
94 }
95
96 std::shared_ptr<InputHandler> Engine::getInputHandler() {
97 if (inputHandler == nullptr) {
98 throw std::runtime_error("Input handler does not exist yet");
99 }
100 return inputHandler;
101 }
102
103 const std::unique_ptr<AssetManager> & Engine::getAssetManager() const {
104 return assetManager;
105 }
106
108 {
109 return deltaTimeSeconds;
110 }
111
113 {
114 return gameTime;
115 }
116
121
123 return Ecs::RegistryManager::get().view<Ecs::StaticMeshData>( ).size();
124 }
125
127 {
128 return renderer;
129 }
130
132#ifdef ENABLE_TRACY
134#endif
135 if (!glfwInit()) {
136 throw std::runtime_error("Could not initialize glfw!");
137 }
138 std::cout << "GLFW has been intialized!" << std::endl;
139
140 if (!glfwVulkanSupported()) {
141 throw std::runtime_error("Glfw does not support vulkan");
142 }
143 std::cout << "GLFW supports vulkan!" << std::endl;
144 }
145
147#ifdef IS_IN_DEBUG
148 constexpr VkDebugUtilsMessageTypeFlagsEXT typeFlags = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
149 VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
150 VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
151 constexpr VkDebugUtilsMessageSeverityFlagsEXT severityFlags =
152 VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT |
153 VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
154
155 const auto callback = [](VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
156 VkDebugUtilsMessageTypeFlagsEXT messageTypes,
157 const VkDebugUtilsMessengerCallbackDataEXT* callbackData, void* userData) -> VkBool32
158 {
159 if (callbackData == nullptr || callbackData->pMessage == nullptr) {
160 return VK_FALSE;
161 }
162
163 // Filter out known Monado OpenXR runtime bugs
164 // These MUST be filtered silently to prevent Monado from failing during swapchain creation
165 const char* msg = callbackData->pMessage;
166
167 // Monado bug: uses wrong access masks for vk_image_collection images during swapchain creation
168 if (std::strstr(msg, "vk_image_collection image") != nullptr &&
169 std::strstr(msg, "accessMask is") != nullptr) {
170 // Silently ignore - this is an internal Monado bug
171 return VK_FALSE;
172 }
173
174 // Monado bug: uses VK_PIPELINE_STAGE_ALL_COMMANDS_BIT in transitions
175 if (std::strstr(msg, "vkCmdPipelineBarrier()") != nullptr &&
176 std::strstr(msg, "using VK_PIPELINE_STAGE_ALL_COMMANDS_BIT") != nullptr) {
177 // Silently ignore - this is an internal Monado bug
178 return VK_FALSE;
179 }
180
181 // Monado uses SIMULTANEOUS_USE_BIT internally
182 if (std::strstr(msg, "VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT") != nullptr) {
183 // Silently ignore - this is internal Monado behavior
184 return VK_FALSE;
185 }
186
187 // Log non-filtered messages
188 switch (messageSeverity) {
189 case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT:
190 DPLOGI << "Vulkan Debug Message: " << msg << std::endl;
191 break;
192 case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT:
193 PLOGW << "[Vulkan] " << msg << std::endl;
194 break;
195 case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT:
196 PLOGE << "[Vulkan] " << msg << std::endl;
197 break;
198 case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT:
199 PLOGI << "[Vulkan] " << msg;
200 break;
201 default:
202 PLOGE << "This should never be called as the default case is enum_MAX";
203 PLOGF << "[Vulkan] " << msg << std::endl;
204 }
205
206 return VK_FALSE; // Returning VK_TRUE will force the calling function to fail
207 };
208
209 VkDebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfo{};
210 debugUtilsMessengerCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
211 debugUtilsMessengerCreateInfo.messageType = typeFlags;
212 debugUtilsMessengerCreateInfo.messageSeverity = severityFlags;
213 debugUtilsMessengerCreateInfo.pfnUserCallback = callback;
214
215 PLOG_FN_VK(VulkanHelper::getFunctions().vkCreateDebugUtilsMessengerEXT(applicationContext->getVkInstance(), &debugUtilsMessengerCreateInfo, nullptr, &debugMessenger));
216#endif
217 }
218
219 void Engine::populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) {
220 createInfo = { .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
221 .pNext = nullptr,
222 .flags = 0,
223 .messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT |
224 VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
225 VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
226 .messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
227 VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
228 VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT,
229 .pfnUserCallback = debugCallback,
230 .pUserData = nullptr };
231 }
232
233 VkBool32 Engine::debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
234 VkDebugUtilsMessageTypeFlagsEXT messageType,
235 const VkDebugUtilsMessengerCallbackDataEXT *pCallbackData,
236 void *pUserData) {
237
238 // Suppress known SteamVR OpenXR spec violation (ValveSoftware/openvr#1822)
239 // SteamVR's compositor creates internal SAMPLED_IMAGE descriptors for swapchain images
240 // with incorrect layout expectations. This is a runtime bug, not an application error.
241 // The OpenXR spec requires COLOR_ATTACHMENT_OPTIMAL, but SteamVR expects different
242 // layouts for different array layers, which is impossible to satisfy.
243 if (pCallbackData->pMessageIdName != nullptr &&
244 std::strcmp(pCallbackData->pMessageIdName, "VUID-vkCmdDraw-None-09600") == 0) {
245 return VK_FALSE; // Suppress this SteamVR-caused validation error
246 }
247
248 if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
249 std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl;
250 }
251
252 // Also log INFO messages when GPU_PRINTF mode is enabled (for shader debugPrintfEXT)
254 if (messageSeverity == VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT) {
255 PLOGI << "[SHADER_DEBUG] " << pCallbackData->pMessage;
256 }
257 }
258
259 return VK_FALSE;
260 }
261
263 uint32_t layerCount;
264 vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
265
266 std::vector<VkLayerProperties> availableLayers(layerCount);
267 vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
268
269 for (const char* layerName : validationLayers) {
270 bool layerFound = false;
271
272 for (const auto& layerProperties : availableLayers) {
273 if (strcmp(layerName, layerProperties.layerName) == 0) {
274 layerFound = true;
275 break;
276 }
277 }
278
279 if (!layerFound) {
280 return false;
281 }
282 }
283
284 return true;
285 }
286
288 sceneManager->setActiveScene(module->loadInitialScene());
289 }
290
291
293 {
294#ifdef ENABLE_TRACY
296#endif
298
301
303
305
307#ifdef RENDERDOC_ENABLED
308 //RenderDoc& rd = RenderDoc::get();
309 //rd.setWindow(mirrorView->getGlfwWindow());
310#endif
311
312 applicationContext->createDevice(mirrorView->getSurface());
314
316
318
320 sceneManager = std::make_unique<SceneManager>(applicationContext);
323 assetManager->attachToRenderer(renderer);
324 assetManager->setRenderingDataManager(renderer->getRenderingDataManager().get());
326
327 renderer->allocateDescriptors();
328
329 mirrorView->connect(headset, renderer);
330
331#ifdef ENABLE_TRACY
332 const auto & graphicsCommandBuffers = renderer->getGraphicsCommandBuffers();
333 tracyVulkanContext.reserve( graphicsCommandBuffers.size() );
334
335 for ( VkCommandBuffer commandBuffer : graphicsCommandBuffers )
336 {
337 TracyVkCtx tracyContext = TracyVkContext(
338 applicationContext->getVkPhysicalDevice(),
339 applicationContext->getVkDevice(),
340 applicationContext->getGraphicsQueue(),
341 commandBuffer
342 );
343
344 tracyVulkanContext.push_back( tracyContext );
345 }
346
347 renderer->setupTracy( tracyVulkanContext );
348#endif
349
350 PLOGI << "Finished init vulkan";
351 }
352
354 {
355 try
356 {
358 applicationContext->createResources();
359 }
360 catch(VrHeadsetConnectionException& exception)
361 {
362 PLOGV << exception.what();
363 PLOGI << "Failed to acquire the headset! Retrying in 5 Seconds.";
364 applicationContext->cleanup();
365 delete applicationContext;
366 applicationContext = nullptr;
367
368 std::this_thread::sleep_for(std::chrono::milliseconds(5000));
369
371 }
372 }
373
375 {
376 TRACY_ZONE_SCOPED_NAMED( "Process mirror window events" );
377 glfwPollEvents();
378 }
379
380 void Engine::calculateFrameTime( double& lastTime )
381 {
382 TRACY_ZONE_SCOPED_NAMED( "Frame timing calculation" );
383 const double currentTime = glfwGetTime();
384 deltaTimeSeconds = currentTime - lastTime;
385 lastTime = currentTime;
386 }
387
389 {
390 TRACY_ZONE_SCOPED_NAMED_ASSET( "Fixing loading assets" );
391 auto& registry = Ecs::RegistryManager::get();
392 for (auto& entity : registry.view<Ecs::StaticMeshData, Ecs::AssetRequested>( ))
393 {
394 TRACY_ZONE_SCOPED_NAMED( "Asset" );
395 auto& data = registry.get<Ecs::StaticMeshData>( entity );
396 auto* meshAsset = assetManager->getMeshAsset( data.asset );
397 if (meshAsset == nullptr) continue;
398
399 data.assetData = meshAsset;
400 registry.remove<Ecs::AssetRequested>( entity );
401 }
402 }
403
405 {
406 TRACY_ZONE_SCOPED_NAMED("Processing resource pipelines");
407
408 assetManager->getTextureAssetPipeline().tick( true );
409 assetManager->getMeshAssetPipeline().tick( true );
410 }
411
412 void Engine::update( double & gameTime )
413 {
414 TRACY_ZONE_SCOPED_NAMED("Frame Update");
415
417
418 entt::registry& registry = Ecs::RegistryManager::get();
419 {
420 TRACY_ZONE_SCOPED_NAMED( "Physics Tick" );
421 // physics tick
423 auto physicsTickView = registry.view<Ecs::SimulatesPhysics, Ecs::LocalTransform>();
424 // phsics step 1 -
425 for (auto [entity, simulatesPhysics, transform] : physicsTickView.each())
426 {
427 TRACY_ZONE_SCOPED_NAMED( "Physics Object Tick" );
428 if (simulatesPhysics.rigidBody == nullptr) continue;
429 if (simulatesPhysics.isStatic) continue;
430
431 if (simulatesPhysics.isKinematic)
432 {
433 btTransform btTrans;
434 btTrans.setFromOpenGLMatrix(glm::value_ptr(transform.matrix));
435 simulatesPhysics.rigidBody->getMotionState()->setWorldTransform(btTrans);
436 registry.emplace_or_replace<Ecs::TransformDirty>(entity);
437 registry.emplace_or_replace<Ecs::TransformDirtyRenderer>(entity);
438 continue;
439 }
440
441 // Pull transform FROM Bullet (dynamic bodies)
442 btTransform btTrans;
443 simulatesPhysics.rigidBody->getMotionState()->getWorldTransform(btTrans);
444 btTrans.getOpenGLMatrix(glm::value_ptr(transform.matrix));
445 registry.emplace_or_replace<Ecs::TransformDirty>(entity);
446 registry.emplace_or_replace<Ecs::TransformDirtyRenderer>(entity);
447 }
448 // Physics Step 2
449 // Propagate dirty flags to children
450 {
451 TRACY_ZONE_SCOPED_NAMED( "Transform Dirty Propagation" );
452 auto dirtyView = registry.view<Ecs::TransformDirty, Ecs::SceneNodeRef>();
453 for (auto [entity, nodeRef] : dirtyView.each()) {
454 if (auto node = nodeRef.node.lock()) {
455 for (const auto& weakChild : node->getChildren()) {
456 if (auto child = weakChild.lock()) {
457 child->markDirtyRecursive();
458 }
459 }
460 }
461 }
462 }
463 }
464
465 {
466 TRACY_ZONE_SCOPED_NAMED( "Logic Tick" );
467 // logic tick
468 auto logicTickView = registry.view<Ecs::Tick>();
469 if (module)
470 module->preTick();
471 for (auto [entity, tick] : logicTickView.each())
472 {
473 TRACY_ZONE_SCOPED_NAMED("Pre Tick");
474 tick.tickable->preTick();
475 }
476
477 if (module)
478 module->tick( deltaTimeSeconds );
479 for (auto [entity, tick] : logicTickView.each())
480 {
482 tick.tickable->tick(deltaTimeSeconds);
483 }
484
485 if (module)
486 module->postTick();
487 for (auto [entity, tick] : logicTickView.each())
488 {
489 TRACY_ZONE_SCOPED_NAMED("Post Tick");
490 tick.tickable->postTick();
491 }
492 }
493
494
495 {
496 TRACY_ZONE_SCOPED_NAMED( "Hand tracking update" );
497 /*if ( leftHand )
498 leftHand->getTransform()->overrideMatrix(
499 xrInputHandler->getHandPoseInfo( Input::Hand::LEFT ).matrix
500 );
501 if ( rightHand )
502 rightHand->getTransform()->overrideMatrix(
503 xrInputHandler->getHandPoseInfo( Input::Hand::RIGHT ).matrix
504 );*/
505 }
506 }
507
509 {
510#ifdef RENDERDOC_ENABLED
511 auto & rd = RenderDoc::get();
512 {
513 TRACY_ZONE_SCOPED_NAMED( "RenderDoc operations" );
514 if ( rd.init() )
515 {
516 rd.startCapture( applicationContext->getVkDevice() );
517 rd.printStatus();
518 }
519 else
520 {
521 PLOGW << "Could not init renderdoc, capture failed!";
522 }
523 }
524#endif
525 }
526
528 {
529 return glfwGetWindowAttrib( mirrorView->getGlfwWindow(), GLFW_ICONIFIED );
530 }
531
532 void Engine::completeFrameAsync(VkFence graphicsFence, bool presentMirror)
533 {
534 // Note: This function is called from a background thread.
535 // The graphicsFence parameter is captured before advanceFrameIndices() is called,
536 // so it refers to the correct fence for the frame that was just submitted.
537
538 TRACY_ZONE_SCOPED_NAMED("Async Frame Completion");
539
540 // Wait for GPU to finish rendering this frame
541 {
542 TRACY_ZONE_SCOPED_NAMED("Wait Graphics Fence (Async)");
543 vkWaitForFences(applicationContext->getVkDevice(), 1, &graphicsFence, VK_TRUE, UINT64_MAX);
544 }
545
546 // End XR frame (includes xrReleaseSwapchainImage)
547 {
548 TRACY_ZONE_SCOPED_NAMED("End XR Frame (Async)");
549 PLOGI << "[ASYNC] About to call endXrFrame()";
550 headset->endXrFrame();
551 PLOGI << "[ASYNC] endXrFrame() completed";
552 }
553
554 // Present mirror view if requested
555 if (presentMirror)
556 {
557 TRACY_ZONE_SCOPED_NAMED("Mirror View Present (Async)");
558 PLOGI << "[ASYNC] About to call mirrorView->present()";
559 mirrorView->present();
560 PLOGI << "[ASYNC] mirrorView->present() completed";
561 }
562
563 // Signal completion for next frame
564 previousFrameXrComplete_.store(true, std::memory_order_release);
565 }
566
568 {
569 TRACY_ZONE_SCOPED_NAMED("Engine::mainLoop");
570 double lastTime = glfwGetTime();
571
572
573 renderer->initializeGpuBuffers();
574
575 // check if the meshlet draw function is loaded to prevent exit in the renderer
576 if ( !VulkanHelper::getFunctions().vkCmdDrawMeshTasksIndirectEXT )
577 PLOGF << "Draw call pointer not valid!";
578
579 while ( !glfwWindowShouldClose( mirrorView->getGlfwWindow() ) && !headset->getIsExitRequested() )
580 {
581 TRACY_ZONE_SCOPED_NAMED( "Engine::mainLoop" );
582
583 calculateFrameTime( lastTime );
585
586 // If window close was requested, notify OpenXR to begin shutdown
587 // This prevents xrWaitFrame from blocking indefinitely
588 if ( glfwWindowShouldClose( mirrorView->getGlfwWindow() ) )
589 {
590 headset->requestExitSession();
591 }
592
593 // === STEP 1: Call xrWaitFrame early (can happen before previous xrEndFrame) ===
594 uint32_t swapchainImageIndex = 0;
595 Headset::BeginFrameResult beginFrameResult;
596 Headset::WaitFrameResult waitFrameResult;
597 {
598 #if defined(HEADLESS)
599 {
600 beginFrameResult = Headset::BeginFrameResult::SkipFully;
601 waitFrameResult.result = beginFrameResult;
602 waitFrameResult.shouldCallBeginFrame = false;
603 }
604 #elif defined(COMPUTE_DEBUG)
605 {
607 waitFrameResult.result = beginFrameResult;
608 waitFrameResult.shouldCallBeginFrame = false;
609 }
610 #else
611 {
612 TRACY_ZONE_SCOPED_NAMED( "Wait XR Frame (Early)" );
613 waitFrameResult = headset->waitForXrFrame( renderer );
614 beginFrameResult = waitFrameResult.result;
615 }
616 #endif
617 }
618
619 // === STEP 2: Do work that doesn't need swapchain/poses ===
620 // This overlaps with GPU work from previous frame
623
624 if ( sceneManager->isSceneChangePending() )
625 {
626 TRACY_ZONE_SCOPED_NAMED( "Scene loading operations" );
627 sceneManager->executePendingSceneLoading();
628 }
629
630 // === STEP 3: Wait for previous frame's xrEndFrame to complete ===
631 // This is required before calling xrBeginFrame
632 if ( frameCompletionFuture_.valid() )
633 {
634 TRACY_ZONE_SCOPED_NAMED( "Wait for previous frame completion" );
636 }
637
638 // Frame mark after previous frame fully completes (including async xrEndFrame)
640
641 // === STEP 4: Now safe to call xrBeginFrame ===
642 if ( waitFrameResult.shouldCallBeginFrame )
643 {
644 TRACY_ZONE_SCOPED_NAMED( "Begin XR Frame (After Wait)" );
645 beginFrameResult = headset->beginXrFrameAfterWait( swapchainImageIndex );
646 }
647
648 // Input sync needs poses from xrBeginFrame
649 if ( beginFrameResult == Headset::BeginFrameResult::RenderFully )
650 {
651 TRACY_ZONE_SCOPED_NAMED( "Input synchronization" );
652 XrTime predictedDisplayTime = headset->getXrFrameState().predictedDisplayTime;
653
654 xrInputHandler->syncInputs( headset->getReferenceSpace(), predictedDisplayTime );
655 }
656
657 update( gameTime );
658
659 // Track previous frame result to detect transitions from skip to render
661
662 switch ( beginFrameResult )
663 {
665 {
666 THROW_ERROR( "Failed to begin xr frame!" );
667 break;
668 }
670 {
671 TRACY_ZONE_SCOPED_NAMED( "Full frame render" );
672
673 // Mark frame start time for stall detection
674 renderer->markFrameStart();
675
676 // Sync timeline semaphore after transitioning from skip to render.
677 // This ensures the frame counter is advanced enough that timeline
678 // semaphore signals will have increasing values (required by Vulkan).
679 if (previousFrameResult == Headset::BeginFrameResult::SkipRender ||
680 previousFrameResult == Headset::BeginFrameResult::SkipFully)
681 {
682 PLOGI << "Resuming rendering after skip - syncing timeline";
683 renderer->syncTimelineAfterPause();
684 }
685
687
688 PLOGI << "Waiting for last frame in flight to complete";
689
690 // Wait for transfer fence (frame N-2 for 2 frames in flight)
691 {
692 TRACY_ZONE_SCOPED_NAMED( "Transfer fence synchronization" );
693 RenderProcess * renderProcess = renderer->getCurrentRenderProcess();
694 vkWaitForFences(
695 applicationContext->getVkDevice(),
696 1,
697 &renderProcess->getTransferCompleteFence(),
698 VK_TRUE,
699 UINT64_MAX
700 );
701 vkResetFences(
702 applicationContext->getVkDevice(),
703 1,
704 &renderProcess->getTransferCompleteFence()
705 );
706 renderProcess->setTransferFenceSubmitted( false );
707 }
708
709 // Wait for previous graphics completion (frame N-2 for 2 frames in flight)
710 // Note: We don't wait for frame N-1's graphics fence here anymore.
711 // Frame N-1's graphics fence is waited on by the async completion thread.
712 {
713 TRACY_ZONE_SCOPED_NAMED( "Graphics fence synchronization" );
714 RenderProcess * renderProcess = renderer->getCurrentRenderProcess();
715 if ( renderProcess->getGraphicsFenceSubmitted() )
716 {
717 vkWaitForFences(
718 applicationContext->getVkDevice(),
719 1,
720 &renderProcess->getGraphicsCompleteFence(),
721 VK_TRUE,
722 UINT64_MAX
723 );
724 vkResetFences(
725 applicationContext->getVkDevice(),
726 1,
727 &renderProcess->getGraphicsCompleteFence()
728 );
729 renderProcess->setGraphicsFenceSubmitted( false );
730 }
731 }
732
733 {
734 TRACY_ZONE_SCOPED_NAMED( "Process completed meshlet generations" );
735 // Safe point: write completed meshlet data to registry
736 auto pendingCompletions = assetManager->getMeshAssetPipeline().takePendingMeshletCompletions();
737 renderer->getRenderingDataManager()->processCompletedMeshletGenerations(std::move(pendingCompletions));
738 }
739
740 {
741 TRACY_ZONE_SCOPED_NAMED( "Process pending mesh loads" );
742 // Safe point: process deferred mesh loads before render update
743 renderer->getRenderingDataManager()->processPendingMeshLoads();
744 }
745
746 {
747 TRACY_ZONE_SCOPED_NAMED( "Update primitive GPU data" );
748 renderer->getRenderingDataManager()->updateIfDirty();
749 }
750
751 {
752 TRACY_ZONE_SCOPED_NAMED( "Update transform GPU data" );
753 renderer->getRenderingDataManager()->updateTransforms();
754 }
755
756 // Sync descriptor sets with RenderingDataManager using version tracking
757 {
758 TRACY_ZONE_SCOPED_NAMED( "Sync descriptor sets" );
759 renderer->getCurrentRenderProcess()->syncWithRenderingDataManager(
760 renderer->getRenderingDataManager().get());
761 }
762
763 {
764 TRACY_ZONE_SCOPED_NAMED( "Renderer data upload" );
765 renderer->uploadFrameData( static_cast<float>( gameTime ) );
766 }
767
768 {
769 TRACY_ZONE_SCOPED_NAMED( "Transfer command recording" );
770 renderer->recordTransfer( static_cast<float>( gameTime ) );
771 }
772
773 {
774 TRACY_ZONE_SCOPED_NAMED( "Transfer submission" );
775 renderer->submitTransfer();
776 }
777
778 {
779 TRACY_ZONE_SCOPED_NAMED( "XR rendering" );
780 renderer->renderToXr( swapchainImageIndex, static_cast<float>( gameTime ) );
781 }
782
783 VkSemaphore mirrorSemaphore = VK_NULL_HANDLE;
784 bool shouldPresentMirror = false;
785 {
786 TRACY_ZONE_SCOPED_NAMED( "Mirror view operations" );
787 // Skip mirror view after timeline sync to prevent WRITE_AFTER_PRESENT hazard
788 const bool skipAfterSync = renderer->shouldSkipMirrorView();
789 const bool isMirrorVisible =
790 !isMirrorViewMinimized() && !skipAfterSync;
791
792 #ifndef HEADLESS
793 if ( isMirrorVisible )
794 {
795 auto mirrorResult = mirrorView->render( swapchainImageIndex );
796 if ( mirrorResult.status == MirrorView::RenderResult::Status::Visible )
797 {
798 mirrorSemaphore = mirrorResult.imageAvailableSemaphore;
799 shouldPresentMirror = true;
800 }
801 }
802 #endif
803 }
804
805 {
806 TRACY_ZONE_SCOPED_NAMED( "Graphics submission" );
807 renderer->submitGraphics( swapchainImageIndex, mirrorSemaphore );
808 }
809
810 #ifdef RENDERDOC_ENABLED
811 {
812 TRACY_ZONE_SCOPED_NAMED( "RenderDoc end capture" );
813 rd.endCapture( applicationContext->getVkDevice() );
814 }
815 #endif
816
817 // === STEP 8: Spawn async completion (non-blocking) ===
818 // This runs on a background thread: waits for GPU fence, ends XR frame, presents mirror
819 {
820 TRACY_ZONE_SCOPED_NAMED( "Spawn async frame completion" );
821 #ifndef HEADLESS
822 previousFrameXrComplete_.store(false, std::memory_order_release);
823
824 // Capture the graphics fence BEFORE advanceFrameIndices changes currentRenderProcess
825 // The async thread needs the fence from THIS frame's RenderProcess
826 VkFence graphicsFence = renderer->getCurrentRenderProcess()->getGraphicsCompleteFence();
827
829 [this, graphicsFence, shouldPresentMirror]() {
830 completeFrameAsync(graphicsFence, shouldPresentMirror);
831 });
832 #endif
833 }
834
835 {
836 TRACY_ZONE_SCOPED_NAMED( "Frame cleanup and advancement" );
837 // Staging buffer cleanup moved to Renderer::submitTransfer() after fence wait
838 renderer->advanceFrameIndices();
839 }
840 break;
841 }
843 {
844 TRACY_ZONE_SCOPED_NAMED("Skip render frame");
845 PLOGI << "Skipping render.";
846 headset->endXrFrameNoRender();
847 // Staging buffer cleanup moved to Renderer::submitTransfer() after fence wait
848 break;
849 }
851 TRACY_ZONE_SCOPED_NAMED("Skip frame fully");
852 PLOGI << "Skipping frame fully.";
853 // Staging buffer cleanup moved to Renderer::submitTransfer() after fence wait
854
855 std::this_thread::sleep_for(std::chrono::microseconds(8300));
856 break;
857 }
859 {
860 TRACY_ZONE_SCOPED_NAMED( "Compute Debug Frame" );
861 PLOGI << "[COMPUTE_DEBUG] Running compute-only frame";
862
863 // Wait for transfer fence
864 {
865 TRACY_ZONE_SCOPED_NAMED( "Transfer fence synchronization" );
866 RenderProcess * renderProcess = renderer->getCurrentRenderProcess();
867 if ( renderProcess->getTransferFenceSubmitted() )
868 {
869 vkWaitForFences(
870 applicationContext->getVkDevice(),
871 1,
872 &renderProcess->getTransferCompleteFence(),
873 VK_TRUE,
874 UINT64_MAX
875 );
876 vkResetFences(
877 applicationContext->getVkDevice(),
878 1,
879 &renderProcess->getTransferCompleteFence()
880 );
881 renderProcess->setTransferFenceSubmitted( false );
882 }
883 }
884
885 // Wait for previous graphics/compute completion
886 {
887 TRACY_ZONE_SCOPED_NAMED( "Graphics fence synchronization" );
888 RenderProcess * renderProcess = renderer->getCurrentRenderProcess();
889 if ( renderProcess->getGraphicsFenceSubmitted() )
890 {
891 vkWaitForFences(
892 applicationContext->getVkDevice(),
893 1,
894 &renderProcess->getGraphicsCompleteFence(),
895 VK_TRUE,
896 UINT64_MAX
897 );
898 vkResetFences(
899 applicationContext->getVkDevice(),
900 1,
901 &renderProcess->getGraphicsCompleteFence()
902 );
903 renderProcess->setGraphicsFenceSubmitted( false );
904 }
905 }
906
907 {
908 TRACY_ZONE_SCOPED_NAMED( "Process completed meshlet generations" );
909 // Safe point: write completed meshlet data to registry
910 auto pendingCompletions = assetManager->getMeshAssetPipeline().takePendingMeshletCompletions();
911 renderer->getRenderingDataManager()->processCompletedMeshletGenerations(std::move(pendingCompletions));
912 }
913
914 {
915 TRACY_ZONE_SCOPED_NAMED( "Process pending mesh loads" );
916 // Safe point: process deferred mesh loads before render update
917 renderer->getRenderingDataManager()->processPendingMeshLoads();
918 }
919
920 // Update primitive data if dirty
921 {
922 TRACY_ZONE_SCOPED_NAMED( "Update primitive GPU data" );
923 renderer->getRenderingDataManager()->updateIfDirty();
924 }
925
926 {
927 TRACY_ZONE_SCOPED_NAMED( "Update transform GPU data" );
928 renderer->getRenderingDataManager()->updateTransforms();
929 }
930
931 // Sync descriptor sets with RenderingDataManager using version tracking
932 {
933 TRACY_ZONE_SCOPED_NAMED( "Sync descriptor sets" );
934 renderer->getCurrentRenderProcess()->syncWithRenderingDataManager(
935 renderer->getRenderingDataManager().get());
936 }
937
938 // Update CPU-side rendering data
939 {
940 TRACY_ZONE_SCOPED_NAMED( "Renderer data upload" );
941 renderer->uploadFrameData( static_cast<float>( gameTime ) );
942 }
943
944 // Record and submit transfer commands
945 {
946 TRACY_ZONE_SCOPED_NAMED( "Transfer command recording" );
947 renderer->recordTransfer( static_cast<float>( gameTime ) );
948 }
949
950 {
951 TRACY_ZONE_SCOPED_NAMED( "Transfer submission" );
952 renderer->submitTransfer();
953 }
954
955 // Run compute pipeline (without graphics/mesh shaders)
956 {
957 TRACY_ZONE_SCOPED_NAMED( "Compute dispatch" );
958 renderer->renderToXr( 0, static_cast<float>( gameTime ) );
959 }
960
961 // Submit compute commands
962 {
963 TRACY_ZONE_SCOPED_NAMED( "Compute submission" );
964 renderer->submitGraphics( 0, VK_NULL_HANDLE );
965 }
966
967 {
968 TRACY_ZONE_SCOPED_NAMED( "Frame cleanup and advancement" );
969 // Staging buffer cleanup moved to Renderer::submitTransfer() after fence wait
970 renderer->advanceFrameIndices();
971 }
972
973 // Small delay to prevent spinning too fast in debug mode
974 std::this_thread::sleep_for( std::chrono::milliseconds( 8 ) );
975 break;
976 }
977 }
978
979 // Update previous frame result for next iteration's skip-to-render detection
980 previousFrameResult = beginFrameResult;
981
982 frameCounter++;
983 }
984 PLOGI << "Exiting main loop";
985 }
986
988 PLOGI << "Engine::cleanup() started";
989
990 // Wait for any pending async frame completion before cleanup
991 if (frameCompletionFuture_.valid()) {
992 PLOGI << "Waiting for async frame completion to finish...";
994 PLOGI << "Async frame completion finished";
995 }
996
997 if (applicationContext->getVkDevice()) {
998 // Signal the timeline semaphore from CPU to unblock any GPU work
999 // that may be waiting on timeline values that will never be signaled
1000 // by subsequent GPU submissions (since we're shutting down)
1001 if (renderer) {
1002 PLOGI << "Signaling timeline semaphore to unblock pending GPU work...";
1003 const auto& timelineSync = renderer->getTimelineSynchronizer();
1004 uint64_t currentValue = timelineSync.getCurrentValue();
1005 // Signal to a value high enough to unblock any pending work
1006 // Using current + 100 frames worth of stages ensures all pending
1007 // waits are satisfied
1008 uint64_t unblockValue = currentValue + (100 * PIPELINE_STAGE_COUNT);
1009 PLOGI << "Timeline semaphore: current=" << currentValue << ", signaling=" << unblockValue;
1010 timelineSync.signalValue(unblockValue);
1011 }
1012
1013 // Wait for all in-flight fences to complete before device wait idle
1014 // This ensures any submitted work completes before we try to clean up
1015 PLOGI << "Waiting for in-flight fences...";
1016 if (renderer) {
1017 std::vector<VkFence> fences;
1018 for (RenderProcess* process : renderer->getRenderProcesses()) {
1019 if (process->getTransferFenceSubmitted()) {
1020 fences.push_back(process->getTransferCompleteFence());
1021 }
1022 if (process->getGraphicsFenceSubmitted()) {
1023 fences.push_back(process->getGraphicsCompleteFence());
1024 }
1025 }
1026 if (!fences.empty()) {
1027 PLOGI << "Waiting for " << fences.size() << " fences...";
1028 VkResult result = vkWaitForFences(
1029 applicationContext->getVkDevice(),
1030 static_cast<uint32_t>(fences.size()),
1031 fences.data(),
1032 VK_TRUE,
1033 5000000000ULL // 5 second timeout
1034 );
1035 if (result == VK_TIMEOUT) {
1036 PLOGW << "Fence wait timed out - GPU work may be stuck";
1037 }
1038 }
1039 }
1040
1041 // Wait for the device to be idle before cleaning up resources
1042 PLOGI << "Waiting for device to be idle...";
1043 vkDeviceWaitIdle(applicationContext->getVkDevice());
1044 PLOGI << "Device is idle";
1045
1046 if (sceneManager) {
1047 PLOGI << "Cleaning up scene manager...";
1048 sceneManager->cleanup();
1049 }
1050 // asset manager
1051 if (assetManager) {
1052 PLOGI << "Cleaning up asset manager...";
1053 assetManager->cleanup();
1054 assetManager.reset();
1055 }
1056
1057 PLOGI << "Cleaning up renderer...";
1058 renderer->cleanup();
1059 delete renderer;
1060 renderer = nullptr;
1061
1062 PLOGI << "Cleaning up headset...";
1063 headset->cleanup();
1064 delete headset;
1065 headset = nullptr;
1066
1067 PLOGI << "Cleaning up mirror view...";
1068 mirrorView->cleanup();
1069 delete mirrorView;
1070 mirrorView = nullptr;
1071
1072#ifdef IS_IN_DEBUG
1073 // Destroy debug messenger
1076 }
1077#endif
1078
1079 applicationContext->cleanup();
1080 delete applicationContext;
1081 applicationContext = nullptr;
1082
1083 // call cleanup on the module while all vulkan objects still exist
1084 if (module) {
1085 module->shutdown();
1086 module = nullptr;
1087 }
1088 }
1089 }
1090
1092 {
1093 auto now = std::chrono::system_clock::now();
1094 auto time = std::chrono::system_clock::to_time_t(now);
1095 std::ostringstream oss;
1096 oss << "GameLog_" << std::put_time(std::localtime(&time), "%Y-%m-%d_%H-%M-%S") << ".log";
1097 static std::string logFileName = oss.str();
1098
1099 static plog::RollingFileAppender<plog::TxtFormatter> fileAppender(logFileName.c_str());
1100 static plog::ColorConsoleAppender<plog::TxtFormatter> consoleAppender;
1101 plog::init<PLOG_DEFAULT_INSTANCE_ID>(plog::Severity::info, &fileAppender).addAppender(&consoleAppender);
1102 }
1103
1105 {
1106 if (sceneManager->getActiveScene()) {
1107 sceneManager->getActiveScene()->loadContent();
1108 }
1109 }
1110
1112 {
1113 return physicsEngine;
1114 }
1115
1117 assetManager = std::make_unique<AssetManager>(applicationContext);
1118 }
1119
1121 PLOGI << "Total mesh count after loading is now: " << assetManager->getAllMeshes().size();
1122 }
1123
1125 {
1127 }
1128
1130 {
1131 delete physicsEngine;
1132 physicsEngine = nullptr;
1133 }
1134
1136 static EngineManager instance;
1137 return instance;
1138 }
1139
1143
1144 void EngineManager::setEngine(Engine *engineInstance) {
1145 engine = engineInstance;
1146 }
1147} // namespace EngineCore end
1148
1149
1150#undef USE_VULKAN
1151#undef XR_LOG_STRING
const std::vector< const char * > validationLayers
Definition Settings.h:19
constexpr bool ARE_VALIDATION_LAYERS_ENABLED
Definition Settings.h:29
constexpr GPU_DEBUGGING_MODE USE_GPU_PRINTF
Definition Settings.h:54
#define TRACY_ZONE_SCOPED_NAMED_ASSET(name)
#define TRACY_FRAME_MARK
#define TRACY_ZONE_SCOPED_FUNCTION
#define TRACY_ZONE_SCOPED_NAMED(name)
static entt::registry & get()
Gets the registry for all components.
The application context is the core class which stores the basic openxr and vulkan objects.
void setEngine(Engine *engineInstance)
set the content pointer for this singleton
Definition Engine.cpp:1144
EngineCore::Engine * getEngineModule()
gets the pointer to the engine object
Definition Engine.cpp:1140
static EngineManager & getInstance()
gets a reference to the engine manager
Definition Engine.cpp:1135
EngineCore::Engine * engine
Definition Engine.h:319
void initVulkan()
Initialisation of all vulkan related resources.
Definition Engine.cpp:292
std::shared_ptr< InputHandler > inputHandler
Definition Engine.h:96
std::unique_ptr< AssetManager > assetManager
Definition Engine.h:95
void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT &createInfo)
Definition Engine.cpp:219
const std::unique_ptr< AssetManager > & getAssetManager() const
Definition Engine.cpp:103
Headset * headset
Definition Engine.h:167
bool checkValidationLayerSupport()
Definition Engine.cpp:262
void fixLoadingAssets() const
When assets have been requested but are not loaded yet the static mesh data is in a limbo where it ha...
Definition Engine.cpp:388
int isMirrorViewMinimized()
Definition Engine.cpp:527
std::future< void > frameCompletionFuture_
Definition Engine.h:172
void tryCreateApplicationContext()
will try to create a window and if there is no openxr runtime detected it will wait 5 seconds and try...
Definition Engine.cpp:353
void createAssetManager()
Definition Engine.cpp:1116
void completeFrameAsync(VkFence graphicsFence, bool presentMirror)
Runs on background thread: waits for GPU fence, ends XR frame, presents mirror This allows CPU work t...
Definition Engine.cpp:532
std::atomic< bool > previousFrameXrComplete_
Definition Engine.h:173
static VkBool32 debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageType, const VkDebugUtilsMessengerCallbackDataEXT *pCallbackData, void *pUserData)
Definition Engine.cpp:233
void cleanup()
Cleanup of pointers for vulkan and subsystems.
Definition Engine.cpp:987
VkDebugUtilsMessengerEXT debugMessenger
Definition Engine.h:102
Renderer * renderer
Definition Engine.h:168
Renderer * getRenderer() const
Getter for the renderer.
Definition Engine.cpp:126
void createPhysicsWorld()
PHYSICS.
Definition Engine.cpp:1124
void calculateFrameTime(double &lastTime)
Definition Engine.cpp:380
double getGlobalTimeSeconds()
Gets the time since the beginning of the simulation (sum of all delta times)
Definition Engine.cpp:112
void update(double &gameTime)
Definition Engine.cpp:412
void mainLoop()
Main engine loop where the rendering and game steps happen.
Definition Engine.cpp:567
ApplicationContext * applicationContext
Definition Engine.h:165
PhysicsEngine * physicsEngine
Definition Engine.h:286
std::unique_ptr< SceneManager > sceneManager
Definition Engine.h:277
double getDeltaTimeSeconds()
gets the time between frames
Definition Engine.cpp:107
void processResourceLoadingPipelines()
Collects and processes the pipelines which have finished and dispatches the new steps....
Definition Engine.cpp:404
MirrorView * mirrorView
Definition Engine.h:166
Input::XrInputHandler * xrInputHandler
Definition Engine.h:104
void processMirrorWindowEvents()
processes glfw window events like key presses or window events
Definition Engine.cpp:374
PhysicsEngine * getPhysicsEngine() const
Getter for the pointer to the physics engine.
Definition Engine.cpp:1111
void createInputManager()
Definition Engine.cpp:117
void initializePlogDebugger()
Definition Engine.cpp:1091
uint32_t getRenderableSceneObjectCount() const
Counts the amount of entities which have renderable components.
Definition Engine.cpp:122
std::unique_ptr< GameModule > module
Definition Engine.h:99
void captureRenderdocFrame()
Captures a frame for render doc if the define RENDERDOC_ENABLED is set.
Definition Engine.cpp:508
void run(std::unique_ptr< GameModule > module)
Runs the engine. Is the entry point for this module.
Definition Engine.cpp:73
void initStartupScene()
Definition Engine.cpp:1104
void createVulkanDebugMessenger()
Definition Engine.cpp:146
double gameTime
Definition Engine.h:69
void initRenderDoc()
Initialization point for software driven captures.
Definition Engine.cpp:62
void createStartupScene()
Creates the initial scene the engine loads before an actual level is loaded.
Definition Engine.cpp:287
std::shared_ptr< InputHandler > getInputHandler()
Gets a shared handle to the input handler. This is used for any type of input.
Definition Engine.cpp:96
void destroyPhysicsWorld()
Definition Engine.cpp:1129
std::unique_ptr< NamedThreadPool > xrFrameThreadPool_
Definition Engine.h:176
double deltaTimeSeconds
Definition Engine.h:93
uint64_t frameCounter
Definition Engine.h:105
This class houses the components needed to display a vr image on the desktop.
Definition MirrorView.h:21
The physics engine manages creating and destroying physics objects for the physics simulation.
void update(float deltaTimeSeconds)
Steps the physics simulation by 1/120th of a second. The idea of the engine is that it should run on ...
The render process class consolidates all the resources that needs to be duplicated for each frame th...
bool getGraphicsFenceSubmitted() const
bool getTransferFenceSubmitted() const
void setGraphicsFenceSubmitted(bool isSubmitted)
void setTransferFenceSubmitted(bool isSubmitted)
static RenderDoc & get()
Definition RenderDoc.cpp:15
static void initializeFunctions(VkInstance instance, VkDevice device)
static const VulkanFunctions & getFunctions()
Log category system implementation.
constexpr uint32_t PIPELINE_STAGE_COUNT
Is a tag for asset loading. When an asset has been rquested for loading but isnt yet created this tag...
Definition EcsData.h:53
A struct which allows the engine to find out which SceneNode needs to propagate a dirty flag down the...
Definition EcsData.h:130
Tag for EnTT which tags entities which should simulate physics.
Definition EcsData.h:101
Tag for everything which wants to receive tick events.
Definition EcsData.h:146
Tag for transforms that need GPU upload. Separate from TransformDirty because the renderer clears thi...
Definition EcsData.h:123
Tag for dirty transforms (used by scene graph synchronization)
Definition EcsData.h:113
Result from waitForXrFrame indicating what the main loop should do.
Definition Headset.h:58
PFN_vkDestroyDebugUtilsMessengerEXT vkDestroyDebugUtilsMessengerEXT