#include "pipeline_cache.hpp" #include "clear.hpp" #include "../gx/pipeline.hpp" #include "../sqlite_utils.hpp" #include "../webgpu/gpu.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace aurora::gfx { static Module Log("aurora::gfx::pipeline_cache"); constexpr int PipelineCacheSchema = 1; struct CachedPipeline { wgpu::RenderPipeline pipeline; uint32_t firstFrameUsed = UINT32_MAX; }; struct PendingPipeline { PipelineRef hash; uint32_t firstFrameUsed = UINT32_MAX; NewPipelineCallback create; }; struct PipelineCacheWrite { ShaderType type; PipelineRef hash; uint32_t configVersion; ByteBuffer config; uint32_t firstFrameUsed = UINT32_MAX; }; static std::mutex g_pipelineMutex; static bool g_hasPipelineThread = false; static bool g_pipelineFrameActive = false; static size_t g_pipelinesPerFrame = 0; // For synchronous pipeline fallback (OpenGL) #ifdef NDEBUG constexpr size_t BuildPipelinesPerFrame = 5; #else constexpr size_t BuildPipelinesPerFrame = 1; #endif static std::thread g_pipelineThread; static std::atomic_bool g_pipelineThreadEnd = false; static std::condition_variable g_pipelineCv; static absl::flat_hash_map g_pipelines; static std::deque g_priorityPipelines; static std::deque g_backgroundPipelines; static absl::flat_hash_set g_pendingPipelines; static sqlite3* g_pipelineCacheDb = nullptr; static sqlite3_stmt* g_pipelineCacheLoadStmt = nullptr; static sqlite3_stmt* g_pipelineCacheUpsertStmt = nullptr; static bool g_pipelineCacheBroken = false; static std::thread g_pipelineCacheWriterThread; static std::condition_variable g_pipelineCacheWriterCv; static std::mutex g_pipelineCacheWriterMutex; static std::deque g_pipelineCacheWriteQueue; static bool g_pipelineCacheWriterStop = false; static std::atomic_ref queuedPipelines{g_stats.queuedPipelines}; static std::atomic_ref createdPipelines{g_stats.createdPipelines}; template static PipelineCacheWrite make_pipeline_cache_write(ShaderType type, PipelineRef hash, const PipelineConfig& config, uint32_t firstFrameUsed) { static_assert(std::has_unique_object_representations_v); PipelineCacheWrite write{ .type = type, .hash = hash, .configVersion = config.version, .config = ByteBuffer(sizeof(config)), .firstFrameUsed = firstFrameUsed, }; std::memcpy(write.config.data(), &config, sizeof(config)); return write; } static void enqueue_pipeline_cache_write(PipelineCacheWrite write) { if (g_pipelineCacheBroken || g_pipelineCacheDb == nullptr) { return; } { std::lock_guard lock{g_pipelineCacheWriterMutex}; g_pipelineCacheWriteQueue.emplace_back(std::move(write)); } g_pipelineCacheWriterCv.notify_one(); } template static auto find_pending_pipeline(Queue& queue, PipelineRef hash) { return std::find_if(queue.begin(), queue.end(), [=](const PendingPipeline& pending) { return pending.hash == hash; }); } static PendingPipeline* touch_pending_pipeline(PipelineRef hash, bool prioritize) { auto priorityIt = find_pending_pipeline(g_priorityPipelines, hash); if (priorityIt != g_priorityPipelines.end()) { return &*priorityIt; } auto backgroundIt = find_pending_pipeline(g_backgroundPipelines, hash); if (backgroundIt == g_backgroundPipelines.end()) { return nullptr; } if (!prioritize) { return &*backgroundIt; } g_priorityPipelines.emplace_back(std::move(*backgroundIt)); g_backgroundPipelines.erase(backgroundIt); return &g_priorityPipelines.back(); } template static PipelineRef find_pipeline_impl(ShaderType type, const PipelineConfig& config, NewPipelineCallback&& cb, bool persist, std::optional firstFrameUsedOverride) { const PipelineRef hash = xxh3_hash(config, static_cast(type)); const uint32_t firstFrameUsed = firstFrameUsedOverride.value_or(current_frame()); bool notifyWorker = false; std::optional cacheWrite; { std::scoped_lock guard{g_pipelineMutex}; auto pipelineIt = g_pipelines.find(hash); if (pipelineIt != g_pipelines.end()) { if (persist && firstFrameUsed < pipelineIt->second.firstFrameUsed) { pipelineIt->second.firstFrameUsed = firstFrameUsed; cacheWrite = make_pipeline_cache_write(type, hash, config, firstFrameUsed); } } else if (g_pendingPipelines.contains(hash)) { auto* pending = touch_pending_pipeline(hash, g_pipelineFrameActive); if (pending != nullptr && firstFrameUsed < pending->firstFrameUsed) { pending->firstFrameUsed = firstFrameUsed; if (persist) { cacheWrite = make_pipeline_cache_write(type, hash, config, firstFrameUsed); } } } else { if (!g_hasPipelineThread && g_pipelinesPerFrame < BuildPipelinesPerFrame) { g_pipelines.try_emplace(hash, CachedPipeline{ .pipeline = cb(), .firstFrameUsed = firstFrameUsed, }); if (persist) { cacheWrite = make_pipeline_cache_write(type, hash, config, firstFrameUsed); } ++g_pipelinesPerFrame; ++createdPipelines; } else { auto& targetQueue = g_pipelineFrameActive ? g_priorityPipelines : g_backgroundPipelines; targetQueue.emplace_back(PendingPipeline{ .hash = hash, .firstFrameUsed = firstFrameUsed, .create = std::move(cb), }); g_pendingPipelines.insert(hash); if (persist) { cacheWrite = make_pipeline_cache_write(type, hash, config, firstFrameUsed); } notifyWorker = true; } } } if (cacheWrite) { enqueue_pipeline_cache_write(std::move(*cacheWrite)); } if (notifyWorker) { g_pipelineCv.notify_one(); ++queuedPipelines; } return hash; } static void pipeline_cache_abort() { g_pipelineCacheBroken = true; if (g_pipelineCacheLoadStmt != nullptr) { sqlite3_finalize(g_pipelineCacheLoadStmt); g_pipelineCacheLoadStmt = nullptr; } if (g_pipelineCacheUpsertStmt != nullptr) { sqlite3_finalize(g_pipelineCacheUpsertStmt); g_pipelineCacheUpsertStmt = nullptr; } if (g_pipelineCacheDb != nullptr) { sqlite3_close(g_pipelineCacheDb); g_pipelineCacheDb = nullptr; } } static bool prepare_pipeline_cache_db() { if (g_pipelineCacheBroken) { return false; } if (g_pipelineCacheDb != nullptr) { return true; } const auto path = (std::filesystem::path{g_config.configPath} / "pipeline_cache.db").string(); auto ret = sqlite3_open(path.c_str(), &g_pipelineCacheDb); if (ret != SQLITE_OK) { Log.error("Failed to open pipeline cache database: {}", sqlite3_errmsg(g_pipelineCacheDb)); pipeline_cache_abort(); return false; } ret = sqlite::exec(g_pipelineCacheDb, "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;"); if (ret != SQLITE_OK) { Log.error("Failed to set pipeline cache pragmas: {}", sqlite3_errmsg(g_pipelineCacheDb)); pipeline_cache_abort(); return false; } bool schemaFailed = false; { sqlite::Transaction tx(g_pipelineCacheDb, Log, true); if (!tx) { Log.error("Failed to begin pipeline cache schema transaction"); schemaFailed = true; } else { ret = sqlite::exec(g_pipelineCacheDb, "CREATE TABLE IF NOT EXISTS aurora_schema(value INTEGER);"); if (ret != SQLITE_OK) { Log.error("Failed to create pipeline cache schema table: {}", sqlite3_errmsg(g_pipelineCacheDb)); schemaFailed = true; } } bool schemaMatch = false; if (!schemaFailed) { const auto schemaQuery = fmt::format("SELECT 1 FROM aurora_schema WHERE value = {}", PipelineCacheSchema); ret = sqlite::exec(g_pipelineCacheDb, schemaQuery.c_str(), [&schemaMatch](int, char**, char**) { schemaMatch = true; }); if (ret != SQLITE_OK) { Log.error("Failed to read pipeline cache schema version: {}", sqlite3_errmsg(g_pipelineCacheDb)); schemaFailed = true; } } if (!schemaFailed && !schemaMatch) { const auto schemaSql = fmt::format( R"(DROP TABLE IF EXISTS pipeline_cache; CREATE TABLE pipeline_cache ( type INTEGER NOT NULL, hash INTEGER NOT NULL, config_version INTEGER NOT NULL, config_size INTEGER NOT NULL, config BLOB NOT NULL, first_frame_used INTEGER NOT NULL, PRIMARY KEY (type, hash) ); CREATE INDEX pipeline_cache_load_order_idx ON pipeline_cache(type, config_version, first_frame_used); DELETE FROM aurora_schema; INSERT INTO aurora_schema VALUES ({});)", PipelineCacheSchema); ret = sqlite::exec(g_pipelineCacheDb, schemaSql.c_str()); if (ret != SQLITE_OK) { Log.error("Failed to initialize pipeline cache schema: {}", sqlite3_errmsg(g_pipelineCacheDb)); schemaFailed = true; } } if (!schemaFailed) { tx.commit(); } } if (schemaFailed) { pipeline_cache_abort(); return false; } ret = sqlite3_prepare_v3(g_pipelineCacheDb, "SELECT config, first_frame_used FROM pipeline_cache " "WHERE type = ? AND config_version = ? " "ORDER BY first_frame_used ASC, rowid ASC", -1, SQLITE_PREPARE_PERSISTENT, &g_pipelineCacheLoadStmt, nullptr); if (ret != SQLITE_OK) { Log.error("Failed to prepare pipeline cache load statement: {}", sqlite3_errmsg(g_pipelineCacheDb)); pipeline_cache_abort(); return false; } ret = sqlite3_prepare_v3( g_pipelineCacheDb, "INSERT INTO pipeline_cache (type, hash, config_version, config_size, config, first_frame_used) " "VALUES (?, ?, ?, ?, ?, ?) " "ON CONFLICT(type, hash) DO UPDATE SET " "config_version = excluded.config_version, " "config_size = excluded.config_size, " "config = excluded.config, " "first_frame_used = MIN(pipeline_cache.first_frame_used, excluded.first_frame_used)", -1, SQLITE_PREPARE_PERSISTENT, &g_pipelineCacheUpsertStmt, nullptr); if (ret != SQLITE_OK) { Log.error("Failed to prepare pipeline cache upsert statement: {}", sqlite3_errmsg(g_pipelineCacheDb)); pipeline_cache_abort(); return false; } return true; } static void prune_old_pipeline_cache_versions() { if (!prepare_pipeline_cache_db()) { return; } const auto clearDelete = fmt::format("DELETE FROM pipeline_cache WHERE type = {} AND config_version < {}", underlying(ShaderType::Clear), clear::ClearPipelineConfigVersion); auto ret = sqlite::exec(g_pipelineCacheDb, clearDelete.c_str()); if (ret != SQLITE_OK) { Log.error("Failed to prune clear pipeline cache rows: {}", sqlite3_errmsg(g_pipelineCacheDb)); pipeline_cache_abort(); return; } const auto gxDelete = fmt::format("DELETE FROM pipeline_cache WHERE type = {} AND config_version < {}", underlying(ShaderType::GX), gx::GXPipelineConfigVersion); ret = sqlite::exec(g_pipelineCacheDb, gxDelete.c_str()); if (ret != SQLITE_OK) { Log.error("Failed to prune GX pipeline cache rows: {}", sqlite3_errmsg(g_pipelineCacheDb)); pipeline_cache_abort(); } } static bool write_pipeline_cache_record(const PipelineCacheWrite& write) { const auto fail = [&]() { sqlite3_reset(g_pipelineCacheUpsertStmt); sqlite3_clear_bindings(g_pipelineCacheUpsertStmt); return false; }; auto ret = sqlite3_bind_int(g_pipelineCacheUpsertStmt, 1, underlying(write.type)); if (ret != SQLITE_OK) { Log.error("Failed to bind pipeline cache type: {}", sqlite3_errmsg(g_pipelineCacheDb)); return fail(); } ret = sqlite3_bind_int64(g_pipelineCacheUpsertStmt, 2, static_cast(write.hash)); if (ret != SQLITE_OK) { Log.error("Failed to bind pipeline cache hash: {}", sqlite3_errmsg(g_pipelineCacheDb)); return fail(); } ret = sqlite3_bind_int(g_pipelineCacheUpsertStmt, 3, static_cast(write.configVersion)); if (ret != SQLITE_OK) { Log.error("Failed to bind pipeline cache config version: {}", sqlite3_errmsg(g_pipelineCacheDb)); return fail(); } ret = sqlite3_bind_int(g_pipelineCacheUpsertStmt, 4, static_cast(write.config.size())); if (ret != SQLITE_OK) { Log.error("Failed to bind pipeline cache config size: {}", sqlite3_errmsg(g_pipelineCacheDb)); return fail(); } ret = sqlite3_bind_blob64(g_pipelineCacheUpsertStmt, 5, write.config.data(), write.config.size(), SQLITE_TRANSIENT); if (ret != SQLITE_OK) { Log.error("Failed to bind pipeline cache config blob: {}", sqlite3_errmsg(g_pipelineCacheDb)); return fail(); } ret = sqlite3_bind_int64(g_pipelineCacheUpsertStmt, 6, static_cast(write.firstFrameUsed)); if (ret != SQLITE_OK) { Log.error("Failed to bind pipeline cache first-frame-used: {}", sqlite3_errmsg(g_pipelineCacheDb)); return fail(); } ret = sqlite3_step(g_pipelineCacheUpsertStmt); if (ret != SQLITE_DONE) { Log.error("Failed to upsert pipeline cache row: {}", sqlite3_errmsg(g_pipelineCacheDb)); return fail(); } sqlite3_reset(g_pipelineCacheUpsertStmt); sqlite3_clear_bindings(g_pipelineCacheUpsertStmt); return true; } static void pipeline_cache_writer() { #ifdef TRACY_ENABLE tracy::SetThreadName("Pipeline cache writer thread"); #endif while (true) { std::deque batch; { std::unique_lock lock{g_pipelineCacheWriterMutex}; g_pipelineCacheWriterCv.wait(lock, [] { return g_pipelineCacheWriterStop || !g_pipelineCacheWriteQueue.empty(); }); if (g_pipelineCacheWriterStop && g_pipelineCacheWriteQueue.empty()) { return; } batch.swap(g_pipelineCacheWriteQueue); } bool writeFailed = false; { sqlite::Transaction tx(g_pipelineCacheDb, Log, true); if (!tx) { Log.error("Failed to begin pipeline cache write transaction"); writeFailed = true; } else { for (const auto& write : batch) { if (!write_pipeline_cache_record(write)) { writeFailed = true; break; } } if (!writeFailed) { tx.commit(); } } } if (writeFailed) { pipeline_cache_abort(); return; } } } static void pipeline_worker() { #ifdef TRACY_ENABLE tracy::SetThreadName("Pipeline compilation thread"); #endif bool hasMore = false; while (g_hasPipelineThread || g_pipelinesPerFrame < BuildPipelinesPerFrame) { PendingPipeline pending; { std::unique_lock lock{g_pipelineMutex}; if (g_hasPipelineThread) { if (!hasMore) { g_pipelineCv.wait(lock, [] { return !g_priorityPipelines.empty() || !g_backgroundPipelines.empty() || g_pipelineThreadEnd; }); } } else if (g_priorityPipelines.empty() && g_backgroundPipelines.empty()) { return; } if (g_pipelineThreadEnd) { break; } auto& source = !g_priorityPipelines.empty() ? g_priorityPipelines : g_backgroundPipelines; pending = std::move(source.front()); source.pop_front(); } auto result = pending.create(); { std::lock_guard lock{g_pipelineMutex}; g_pipelines.try_emplace(pending.hash, CachedPipeline{ .pipeline = std::move(result), .firstFrameUsed = pending.firstFrameUsed, }); g_pendingPipelines.erase(pending.hash); hasMore = !g_priorityPipelines.empty() || !g_backgroundPipelines.empty(); } if (!g_hasPipelineThread) { ++g_pipelinesPerFrame; } ++createdPipelines; --queuedPipelines; } } template static void load_pipeline_cache_entries(ShaderType type, uint32_t configVersion, CreateFn&& create) { if (!prepare_pipeline_cache_db()) { return; } auto ret = sqlite3_bind_int(g_pipelineCacheLoadStmt, 1, underlying(type)); if (ret != SQLITE_OK) { Log.error("Failed to bind pipeline cache load type: {}", sqlite3_errmsg(g_pipelineCacheDb)); pipeline_cache_abort(); return; } ret = sqlite3_bind_int(g_pipelineCacheLoadStmt, 2, static_cast(configVersion)); if (ret != SQLITE_OK) { Log.error("Failed to bind pipeline cache load config version: {}", sqlite3_errmsg(g_pipelineCacheDb)); pipeline_cache_abort(); return; } while ((ret = sqlite3_step(g_pipelineCacheLoadStmt)) == SQLITE_ROW) { const auto* configBlob = static_cast(sqlite3_column_blob(g_pipelineCacheLoadStmt, 0)); const auto configSize = sqlite3_column_bytes(g_pipelineCacheLoadStmt, 0); const auto firstFrameUsed = static_cast(sqlite3_column_int64(g_pipelineCacheLoadStmt, 1)); if (configSize != static_cast(sizeof(PipelineConfig)) || (configSize != 0 && configBlob == nullptr)) { continue; } PipelineConfig config; std::memcpy(&config, configBlob, sizeof(config)); if (config.version != configVersion) { continue; } find_pipeline_impl(type, config, [=] { return create(config); }, false, firstFrameUsed); } if (ret != SQLITE_DONE) { Log.error("Failed to read pipeline cache rows: {}", sqlite3_errmsg(g_pipelineCacheDb)); pipeline_cache_abort(); } sqlite3_reset(g_pipelineCacheLoadStmt); sqlite3_clear_bindings(g_pipelineCacheLoadStmt); } static void load_pipeline_cache() { if (!prepare_pipeline_cache_db()) { return; } prune_old_pipeline_cache_versions(); if (g_pipelineCacheBroken) { return; } load_pipeline_cache_entries(ShaderType::Clear, clear::ClearPipelineConfigVersion, clear::create_pipeline); if (g_pipelineCacheBroken) { return; } load_pipeline_cache_entries(ShaderType::GX, gx::GXPipelineConfigVersion, gx::create_pipeline); } static void start_pipeline_cache_writer() { if (!prepare_pipeline_cache_db()) { return; } g_pipelineCacheWriterStop = false; g_pipelineCacheWriterThread = std::thread(pipeline_cache_writer); } static void stop_pipeline_cache_writer() { if (g_pipelineCacheWriterThread.joinable()) { { std::lock_guard lock{g_pipelineCacheWriterMutex}; g_pipelineCacheWriterStop = true; } g_pipelineCacheWriterCv.notify_one(); g_pipelineCacheWriterThread.join(); } else { g_pipelineCacheWriterStop = false; } g_pipelineCacheWriteQueue.clear(); } template <> PipelineRef find_pipeline(ShaderType type, const clear::PipelineConfig& config, NewPipelineCallback&& cb) { return find_pipeline_impl(type, config, std::move(cb), true, std::nullopt); } template <> PipelineRef find_pipeline(ShaderType type, const gx::PipelineConfig& config, NewPipelineCallback&& cb) { return find_pipeline_impl(type, config, std::move(cb), true, std::nullopt); } void initialize_pipeline_cache() { g_pipelineCacheBroken = false; g_pipelineCacheWriterStop = false; g_pipelineFrameActive = false; g_pipelineThreadEnd = false; if (webgpu::g_backendType == wgpu::BackendType::OpenGL || webgpu::g_backendType == wgpu::BackendType::OpenGLES || webgpu::g_backendType == wgpu::BackendType::WebGPU) { g_hasPipelineThread = false; } else { g_hasPipelineThread = true; g_pipelineThread = std::thread(pipeline_worker); } load_pipeline_cache(); if (!g_pipelineCacheBroken) { start_pipeline_cache_writer(); } } void shutdown_pipeline_cache() { if (g_hasPipelineThread) { g_pipelineThreadEnd = true; g_pipelineCv.notify_all(); g_pipelineThread.join(); } g_hasPipelineThread = false; stop_pipeline_cache_writer(); pipeline_cache_abort(); g_pipelineCacheBroken = false; g_pipelineFrameActive = false; g_pipelinesPerFrame = 0; g_pipelines.clear(); g_priorityPipelines.clear(); g_backgroundPipelines.clear(); g_pendingPipelines.clear(); queuedPipelines = 0; createdPipelines = 0; } void begin_pipeline_frame() { g_pipelineFrameActive = true; if (!g_hasPipelineThread) { g_pipelinesPerFrame = 0; } } void end_pipeline_frame() { g_pipelineFrameActive = false; if (!g_hasPipelineThread) { pipeline_worker(); } } bool get_pipeline(PipelineRef ref, wgpu::RenderPipeline& pipeline) { std::lock_guard guard{g_pipelineMutex}; const auto it = g_pipelines.find(ref); if (it == g_pipelines.end()) { return false; } pipeline = it->second.pipeline; return true; } } // namespace aurora::gfx