Files
aurora/lib/gfx/pipeline_cache.cpp

644 lines
21 KiB
C++

#include "pipeline_cache.hpp"
#include "clear.hpp"
#include "../gx/pipeline.hpp"
#include "../sqlite_utils.hpp"
#include "../webgpu/gpu.hpp"
#include <algorithm>
#include <atomic>
#include <condition_variable>
#include <deque>
#include <filesystem>
#include <mutex>
#include <thread>
#include <absl/container/flat_hash_map.h>
#include <absl/container/flat_hash_set.h>
#include <fmt/format.h>
#include <tracy/Tracy.hpp>
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<PipelineRef, CachedPipeline> g_pipelines;
static std::deque<PendingPipeline> g_priorityPipelines;
static std::deque<PendingPipeline> g_backgroundPipelines;
static absl::flat_hash_set<PipelineRef> 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<PipelineCacheWrite> g_pipelineCacheWriteQueue;
static bool g_pipelineCacheWriterStop = false;
static std::atomic_ref queuedPipelines{g_stats.queuedPipelines};
static std::atomic_ref createdPipelines{g_stats.createdPipelines};
template <typename PipelineConfig>
static PipelineCacheWrite make_pipeline_cache_write(ShaderType type, PipelineRef hash, const PipelineConfig& config,
uint32_t firstFrameUsed) {
static_assert(std::has_unique_object_representations_v<PipelineConfig>);
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 <typename Queue>
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 <typename PipelineConfig>
static PipelineRef find_pipeline_impl(ShaderType type, const PipelineConfig& config, NewPipelineCallback&& cb,
bool persist, std::optional<uint32_t> firstFrameUsedOverride) {
ZoneScoped;
const PipelineRef hash = xxh3_hash(config, static_cast<HashType>(type));
const uint32_t firstFrameUsed = firstFrameUsedOverride.value_or(current_frame());
bool notifyWorker = false;
std::optional<PipelineCacheWrite> 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<sqlite3_int64>(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<int>(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<int>(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<sqlite3_int64>(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<PipelineCacheWrite> 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 <typename PipelineConfig, typename CreateFn>
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<int>(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<const uint8_t*>(sqlite3_column_blob(g_pipelineCacheLoadStmt, 0));
const auto configSize = sqlite3_column_bytes(g_pipelineCacheLoadStmt, 0);
const auto firstFrameUsed = static_cast<uint32_t>(sqlite3_column_int64(g_pipelineCacheLoadStmt, 1));
if (configSize != static_cast<int>(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<clear::PipelineConfig>(ShaderType::Clear, clear::ClearPipelineConfigVersion,
clear::create_pipeline);
if (g_pipelineCacheBroken) {
return;
}
load_pipeline_cache_entries<gx::PipelineConfig>(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