#include #include #include #include #include "../fs_helper.hpp" #include "../internal.hpp" #include "../sqlite_utils.hpp" #include #include #if defined(AURORA_CACHE_USE_ZSTD) #include #endif #define XXH_STATIC_LINKING_ONLY #include namespace aurora::webgpu { static Module Log("aurora::gpu::cache"); static sqlite3* db; static sqlite3_stmt* load_stmt; static sqlite3_stmt* store_stmt; static bool cache_broken; static std::mutex cache_mutex; #if defined(AURORA_CACHE_USE_ZSTD) static std::vector compress_buffer; #endif constexpr int CACHE_SCHEMA = 2; static void init_abort() { cache_broken = true; sqlite3_close(db); db = nullptr; } static int check(int ret) { if (ret != SQLITE_OK) { Log.error("SQLite operation failed: {}", sqlite3_errmsg(db)); } return ret; } static bool ensure_schema_up_to_date() { sqlite::Transaction tx(db, Log, true); if (!tx) { Log.error("Failed to open schema check transaction", sqlite3_errmsg(db)); return false; } auto ret = sqlite::exec(db, "CREATE TABLE IF NOT EXISTS aurora_schema(value INTEGER);"); if (ret != SQLITE_OK) { Log.error("Failed to create schema table: {}", sqlite3_errmsg(db)); return false; } bool match = false; auto cmd = fmt::format("SELECT * FROM aurora_schema WHERE value = {}", CACHE_SCHEMA); ret = sqlite::exec(db, cmd.c_str(), [&match](int, char**, char**) { match = true; }, nullptr); if (ret != SQLITE_OK) { Log.error("Failed to check schema table: {}", sqlite3_errmsg(db)); return false; } if (match) { return true; } cmd = fmt::format( R"(DROP TABLE IF EXISTS cache; CREATE TABLE cache ( key BLOB PRIMARY KEY NOT NULL, value BLOB NOT NULL, size INTEGER NOT NULL, compressed INTEGER NOT NULL ); DELETE FROM aurora_schema; INSERT INTO aurora_schema VALUES ({});)", CACHE_SCHEMA); ret = sqlite::exec(db, cmd.c_str()); if (ret != SQLITE_OK) { Log.error("Failed to update schema: {}", sqlite3_errmsg(db)); return false; } tx.commit(); return true; } static bool cache_init_core() { Log.debug("SQLite version {}", sqlite3_libversion()); std::string file = fs_path_to_string(std::filesystem::path{reinterpret_cast(g_config.configPath)} / "dawn_cache.db"); Log.debug("Using dawn cache at {}", file); auto ret = sqlite3_open(file.c_str(), &db); if (ret != SQLITE_OK) { Log.error("Failed to open database: {}", sqlite3_errmsg(db)); return false; } // WAL mode + NORMAL = no need for disk syncs, consistent but not durable is fine. ret = sqlite::exec(db, "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;"); if (ret != SQLITE_OK) { Log.error("Failed to set pragmas: {}", sqlite3_errmsg(db)); return false; } if (!ensure_schema_up_to_date()) { Log.error("Failed to validate schema"); return false; } ret = sqlite3_prepare_v3(db, "SELECT value, size, compressed FROM cache WHERE key = ?", -1, SQLITE_PREPARE_PERSISTENT, &load_stmt, nullptr); if (ret != SQLITE_OK) { Log.error("Failed to prepare statement: {}", sqlite3_errmsg(db)); return false; } ret = sqlite3_prepare_v3(db, "REPLACE INTO cache (key, value, size, compressed) VALUES (?, ?, ?, ?)", -1, SQLITE_PREPARE_PERSISTENT, &store_stmt, nullptr); if (ret != SQLITE_OK) { Log.error("Failed to prepare statement: {}", sqlite3_errmsg(db)); return false; } return true; } static bool cache_init() { if (cache_broken) { return false; } if (db) { return true; } if (!cache_init_core()) { Log.error("SQLite DB init failed"); init_abort(); return false; } Log.debug("SQLite cache init succeeded"); return true; } size_t load_from_cache(void const* key, size_t keySize, void* value, size_t valueSize, void*) { std::lock_guard lock(cache_mutex); if (!cache_init()) { return 0; } sqlite::Transaction tx(db, Log); if (!tx) { Log.error("Failed to open load transaction"); return 0; } const auto keyHash = XXH128(key, keySize, 0); check(sqlite3_bind_blob(load_stmt, 1, &keyHash, sizeof(keyHash), SQLITE_TRANSIENT)); const auto ret = sqlite3_step(load_stmt); size_t foundSize; if (ret == SQLITE_ROW) { // Hit const auto foundPtr = sqlite3_column_blob(load_stmt, 0); foundSize = sqlite3_column_int64(load_stmt, 1); const bool compressed = sqlite3_column_int(load_stmt, 2) != 0; if (value && valueSize == foundSize) { if (compressed) { #if defined(AURORA_CACHE_USE_ZSTD) const auto compSize = sqlite3_column_bytes(load_stmt, 0); const auto zstdRet = ZSTD_decompress(value, valueSize, foundPtr, compSize); if (ZSTD_isError(zstdRet)) { Log.error("zstd decompression error: {}", ZSTD_getErrorName(zstdRet)); foundSize = 0; } else if (zstdRet != foundSize) { Log.error("zstd decompression size mismatch: expected {}, got {}", foundSize, zstdRet); foundSize = 0; } #else Log.error("Cache entry is zstd-compressed but zstd support is disabled"); foundSize = 0; #endif } else { if (foundSize != 0 && !foundPtr) { Log.error("Cache entry is missing raw value data"); foundSize = 0; } else if (foundSize != 0) { std::memcpy(value, foundPtr, foundSize); } } } } else if (ret == SQLITE_DONE) { // Miss foundSize = 0; } else { Log.error("Looking up cache key failed: {}", sqlite3_errmsg(db)); return 0; } check(sqlite3_reset(load_stmt)); return foundSize; } void store_to_cache(void const* key, size_t keySize, void const* value, size_t valueSize, void*) { std::lock_guard lock(cache_mutex); if (!cache_init()) { return; } sqlite::Transaction tx(db, Log, true); if (!tx) { Log.error("Failed to open store transaction"); return; } const void* storedValue = value; sqlite3_uint64 storedValueSize = valueSize; int compressed = 0; #if defined(AURORA_CACHE_USE_ZSTD) const auto bound = ZSTD_compressBound(valueSize); if (ZSTD_isError(bound)) { Log.error("Failed to calculate ZSTD_compressBound: {}", ZSTD_getErrorName(bound)); return; } if (compress_buffer.size() < bound) { compress_buffer.resize(bound); } const auto compressRet = ZSTD_compress(compress_buffer.data(), compress_buffer.size(), value, valueSize, 0); if (ZSTD_isError(compressRet)) { Log.error("ZSTD compression error: {}", ZSTD_getErrorName(compressRet)); return; } if (compressRet < valueSize) { storedValue = compress_buffer.data(); storedValueSize = compressRet; compressed = 1; } #endif const auto keyHash = XXH128(key, keySize, 0); check(sqlite3_bind_blob64(store_stmt, 1, &keyHash, sizeof(keyHash), SQLITE_TRANSIENT)); check( sqlite3_bind_blob64(store_stmt, 2, storedValue, storedValueSize, compressed ? SQLITE_STATIC : SQLITE_TRANSIENT)); check(sqlite3_bind_int64(store_stmt, 3, static_cast(valueSize))); check(sqlite3_bind_int(store_stmt, 4, compressed)); const auto ret = sqlite3_step(store_stmt); if (ret != SQLITE_DONE) { // Error or something Log.error("Failed to insert row: {}", sqlite3_errmsg(db)); return; } check(sqlite3_reset(store_stmt)); check(sqlite3_bind_null(store_stmt, 2)); check(sqlite3_bind_null(store_stmt, 4)); tx.commit(); } void cache_shutdown() { #if defined(AURORA_CACHE_USE_ZSTD) compress_buffer.clear(); #endif check(sqlite3_finalize(load_stmt)); check(sqlite3_finalize(store_stmt)); check(sqlite3_close(db)); db = nullptr; } } // namespace aurora::webgpu