sgejegksng

This commit is contained in:
2025-12-09 16:45:28 +09:00
parent f62ce93695
commit 4880e1a992
3 changed files with 201 additions and 7 deletions

View File

@@ -665,6 +665,12 @@ void VulkanEngine::draw()
_asyncLoader->pump_main_thread(*_sceneManager); _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(); _sceneManager->update_scene();
// Update IBL based on camera position and user-defined reflection volumes. // Update IBL based on camera position and user-defined reflection volumes.

View File

@@ -16,10 +16,14 @@ PipelineManager::~PipelineManager()
void PipelineManager::init(EngineContext *ctx) void PipelineManager::init(EngineContext *ctx)
{ {
_context = ctx; _context = ctx;
start_worker();
} }
void PipelineManager::cleanup() void PipelineManager::cleanup()
{ {
// Stop async worker first so no background thread touches the device/context.
stop_worker();
for (auto &kv: _graphicsPipelines) for (auto &kv: _graphicsPipelines)
{ {
destroyGraphics(kv.second); destroyGraphics(kv.second);
@@ -82,6 +86,10 @@ void PipelineManager::hotReloadChanged()
{ {
if (!_context || !_context->getDevice()) return; if (!_context || !_context->getDevice()) return;
// Discover pipelines whose shaders changed and enqueue async rebuild jobs.
std::vector<ReloadJob> to_enqueue;
to_enqueue.reserve(_graphicsPipelines.size());
for (auto &kv: _graphicsPipelines) for (auto &kv: _graphicsPipelines)
{ {
auto &rec = kv.second; auto &rec = kv.second;
@@ -102,14 +110,14 @@ void PipelineManager::hotReloadChanged()
if (needReload) if (needReload)
{ {
GraphicsPipelineRecord fresh = rec; GraphicsPipelineRecord fresh = rec;
// Do not touch existing pipeline here; async worker will build into a fresh record.
fresh.pipeline = VK_NULL_HANDLE; fresh.pipeline = VK_NULL_HANDLE;
fresh.layout = VK_NULL_HANDLE; fresh.layout = VK_NULL_HANDLE;
if (buildGraphics(fresh))
{ ReloadJob job{};
destroyGraphics(rec); job.name = kv.first;
rec = std::move(fresh); job.record = std::move(fresh);
fmt::println("Reloaded graphics pipeline '{}'", kv.first); to_enqueue.push_back(std::move(job));
}
} }
} }
catch (const std::exception &) catch (const std::exception &)
@@ -117,6 +125,66 @@ void PipelineManager::hotReloadChanged()
// ignore hot-reload errors to avoid spamming // ignore hot-reload errors to avoid spamming
} }
} }
if (to_enqueue.empty()) return;
{
std::lock_guard<std::mutex> 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<ReloadJob> completed;
{
std::lock_guard<std::mutex> lock(_jobs_mutex);
if (_completed_jobs.empty()) return;
completed.swap(_completed_jobs);
}
if (!_context || !_context->getDevice()) return;
std::vector<std::string> 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<std::mutex> lock(_jobs_mutex);
for (const auto &name : finished_names)
{
_inflight.erase(name);
}
}
} }
void PipelineManager::debug_get_graphics(std::vector<GraphicsPipelineDebugInfo> &out) const void PipelineManager::debug_get_graphics(std::vector<GraphicsPipelineDebugInfo> &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<std::mutex> 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<std::mutex> 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<std::mutex> lock(_jobs_mutex);
_inflight.erase(job.name);
return;
}
GraphicsPipelineRecord rec = job.record;
bool ok = buildGraphics(rec);
{
std::lock_guard<std::mutex> 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 --- // --- Compute forwarding API ---
bool PipelineManager::createComputePipeline(const std::string &name, const ComputePipelineCreateInfo &info) bool PipelineManager::createComputePipeline(const std::string &name, const ComputePipelineCreateInfo &info)
{ {

View File

@@ -4,11 +4,17 @@
#include <render/pipelines.h> #include <render/pipelines.h>
#include <compute/vk_compute.h> #include <compute/vk_compute.h>
#include <atomic>
#include <condition_variable>
#include <deque>
#include <functional> #include <functional>
#include <mutex>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <vector> #include <vector>
#include <filesystem> #include <filesystem>
#include <thread>
class EngineContext; class EngineContext;
@@ -97,9 +103,12 @@ public:
// Convenience to interop with MaterialInstance // Convenience to interop with MaterialInstance
bool getMaterialPipeline(const std::string &name, MaterialPipeline &out) const; 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(); void hotReloadChanged();
// Apply any completed async rebuilds on the main thread.
void pump_main_thread();
// Debug helpers (graphics only) // Debug helpers (graphics only)
struct GraphicsPipelineDebugInfo struct GraphicsPipelineDebugInfo
{ {
@@ -125,7 +134,29 @@ private:
EngineContext *_context = nullptr; EngineContext *_context = nullptr;
std::unordered_map<std::string, GraphicsPipelineRecord> _graphicsPipelines; std::unordered_map<std::string, GraphicsPipelineRecord> _graphicsPipelines;
// --- Async hot-reload state ---
struct ReloadJob
{
std::string name;
GraphicsPipelineRecord record;
};
std::atomic<bool> _running{false};
std::thread _worker;
std::mutex _jobs_mutex;
std::condition_variable _jobs_cv;
std::deque<ReloadJob> _pending_jobs;
std::deque<ReloadJob> _completed_jobs;
std::unordered_set<std::string> _inflight;
bool buildGraphics(GraphicsPipelineRecord &rec) const; bool buildGraphics(GraphicsPipelineRecord &rec) const;
void destroyGraphics(GraphicsPipelineRecord &rec); void destroyGraphics(GraphicsPipelineRecord &rec);
void start_worker();
void stop_worker();
void worker_loop();
}; };