From 4880e1a992cc7bcd653239b06ff58284d0493a3d Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Tue, 9 Dec 2025 16:45:28 +0900 Subject: [PATCH] sgejegksng --- src/core/engine.cpp | 6 ++ src/core/pipeline/manager.cpp | 169 ++++++++++++++++++++++++++++++++-- src/core/pipeline/manager.h | 33 ++++++- 3 files changed, 201 insertions(+), 7 deletions(-) diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 8744f72..9a2a640 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -665,6 +665,12 @@ void VulkanEngine::draw() _asyncLoader->pump_main_thread(*_sceneManager); } + // Apply any completed async pipeline rebuilds before using pipelines this frame. + if (_pipelineManager) + { + _pipelineManager->pump_main_thread(); + } + _sceneManager->update_scene(); // Update IBL based on camera position and user-defined reflection volumes. diff --git a/src/core/pipeline/manager.cpp b/src/core/pipeline/manager.cpp index 591edf0..0154d60 100644 --- a/src/core/pipeline/manager.cpp +++ b/src/core/pipeline/manager.cpp @@ -16,10 +16,14 @@ PipelineManager::~PipelineManager() void PipelineManager::init(EngineContext *ctx) { _context = ctx; + start_worker(); } void PipelineManager::cleanup() { + // Stop async worker first so no background thread touches the device/context. + stop_worker(); + for (auto &kv: _graphicsPipelines) { destroyGraphics(kv.second); @@ -82,6 +86,10 @@ void PipelineManager::hotReloadChanged() { if (!_context || !_context->getDevice()) return; + // Discover pipelines whose shaders changed and enqueue async rebuild jobs. + std::vector to_enqueue; + to_enqueue.reserve(_graphicsPipelines.size()); + for (auto &kv: _graphicsPipelines) { auto &rec = kv.second; @@ -102,14 +110,14 @@ void PipelineManager::hotReloadChanged() if (needReload) { GraphicsPipelineRecord fresh = rec; + // Do not touch existing pipeline here; async worker will build into a fresh record. fresh.pipeline = VK_NULL_HANDLE; fresh.layout = VK_NULL_HANDLE; - if (buildGraphics(fresh)) - { - destroyGraphics(rec); - rec = std::move(fresh); - fmt::println("Reloaded graphics pipeline '{}'", kv.first); - } + + ReloadJob job{}; + job.name = kv.first; + job.record = std::move(fresh); + to_enqueue.push_back(std::move(job)); } } catch (const std::exception &) @@ -117,6 +125,66 @@ void PipelineManager::hotReloadChanged() // ignore hot-reload errors to avoid spamming } } + + if (to_enqueue.empty()) return; + + { + std::lock_guard lock(_jobs_mutex); + for (auto &job : to_enqueue) + { + // Avoid duplicate enqueues while a previous rebuild for this pipeline is in-flight. + if (_inflight.find(job.name) != _inflight.end()) + { + continue; + } + _inflight.insert(job.name); + _pending_jobs.push_back(std::move(job)); + } + } + _jobs_cv.notify_all(); +} + +void PipelineManager::pump_main_thread() +{ + // Move completed jobs to a local queue so we don't hold the mutex while doing Vulkan work. + std::deque completed; + { + std::lock_guard lock(_jobs_mutex); + if (_completed_jobs.empty()) return; + completed.swap(_completed_jobs); + } + + if (!_context || !_context->getDevice()) return; + + std::vector finished_names; + finished_names.reserve(completed.size()); + + for (auto &job : completed) + { + auto it = _graphicsPipelines.find(job.name); + if (it == _graphicsPipelines.end()) + { + // Pipeline was unregistered while the job was in flight; just destroy the newly built pipeline. + destroyGraphics(job.record); + } + else + { + // Replace existing pipeline with the freshly built one. + destroyGraphics(it->second); + it->second = std::move(job.record); + fmt::println("Reloaded graphics pipeline '{}' (async)", job.name); + } + finished_names.push_back(job.name); + } + + // Clear in-flight markers after commit so new reloads can be enqueued. + { + std::lock_guard lock(_jobs_mutex); + for (const auto &name : finished_names) + { + _inflight.erase(name); + } + } } void PipelineManager::debug_get_graphics(std::vector &out) const @@ -220,6 +288,95 @@ void PipelineManager::destroyGraphics(GraphicsPipelineRecord &rec) } } +void PipelineManager::start_worker() +{ + bool expected = false; + if (!_running.compare_exchange_strong(expected, true)) + { + // Already running. + return; + } + + _worker = std::thread(&PipelineManager::worker_loop, this); +} + +void PipelineManager::stop_worker() +{ + bool expected = true; + if (!_running.compare_exchange_strong(expected, false)) + { + // Was not running. + return; + } + + { + std::lock_guard lock(_jobs_mutex); + _pending_jobs.clear(); + _completed_jobs.clear(); + _inflight.clear(); + } + _jobs_cv.notify_all(); + + if (_worker.joinable()) + { + _worker.join(); + } +} + +void PipelineManager::worker_loop() +{ + while (true) + { + ReloadJob job; + { + std::unique_lock lock(_jobs_mutex); + _jobs_cv.wait(lock, [this]() { + return !_running.load(std::memory_order_acquire) || !_pending_jobs.empty(); + }); + + if (!_running.load(std::memory_order_acquire) && _pending_jobs.empty()) + { + return; + } + + if (_pending_jobs.empty()) + { + continue; + } + + job = std::move(_pending_jobs.front()); + _pending_jobs.pop_front(); + } + + if (!_context || !_context->getDevice()) + { + // Context/device went away; drop job and exit. + std::lock_guard lock(_jobs_mutex); + _inflight.erase(job.name); + return; + } + + GraphicsPipelineRecord rec = job.record; + bool ok = buildGraphics(rec); + + { + std::lock_guard lock(_jobs_mutex); + if (ok) + { + ReloadJob completed; + completed.name = job.name; + completed.record = std::move(rec); + _completed_jobs.push_back(std::move(completed)); + } + else + { + // Allow future hotReloadChanged calls to enqueue another attempt. + _inflight.erase(job.name); + } + } + } +} + // --- Compute forwarding API --- bool PipelineManager::createComputePipeline(const std::string &name, const ComputePipelineCreateInfo &info) { diff --git a/src/core/pipeline/manager.h b/src/core/pipeline/manager.h index 61d4a55..2cd9591 100644 --- a/src/core/pipeline/manager.h +++ b/src/core/pipeline/manager.h @@ -4,11 +4,17 @@ #include #include +#include +#include +#include #include +#include #include #include +#include #include #include +#include class EngineContext; @@ -97,9 +103,12 @@ public: // Convenience to interop with MaterialInstance bool getMaterialPipeline(const std::string &name, MaterialPipeline &out) const; - // Rebuild pipelines whose shaders changed on disk + // Rebuild pipelines whose shaders changed on disk (enqueue async rebuild jobs) void hotReloadChanged(); + // Apply any completed async rebuilds on the main thread. + void pump_main_thread(); + // Debug helpers (graphics only) struct GraphicsPipelineDebugInfo { @@ -125,7 +134,29 @@ private: EngineContext *_context = nullptr; std::unordered_map _graphicsPipelines; + // --- Async hot-reload state --- + struct ReloadJob + { + std::string name; + GraphicsPipelineRecord record; + }; + + std::atomic _running{false}; + std::thread _worker; + + std::mutex _jobs_mutex; + std::condition_variable _jobs_cv; + std::deque _pending_jobs; + std::deque _completed_jobs; + std::unordered_set _inflight; + bool buildGraphics(GraphicsPipelineRecord &rec) const; void destroyGraphics(GraphicsPipelineRecord &rec); + + void start_worker(); + + void stop_worker(); + + void worker_loop(); };