graphics: on-disk cache for runtime-compiled shader bytecode
This commit is contained in:
parent
4a6e9afacb
commit
b6c738c8e1
4 changed files with 174 additions and 13 deletions
|
|
@ -513,4 +513,15 @@ class Device {
|
||||||
virtual ~Device() = default;
|
virtual ~Device() = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Optional on-disk cache directory for runtime-compiled shader bytecode
|
||||||
|
// (the D3D12 backend's D3DCompile output; backends consuming prebuilt
|
||||||
|
// shaders ignore it). Entries are keyed by a content hash of the stage,
|
||||||
|
// entry point, macros and full source text, so source edits miss naturally
|
||||||
|
// and stale bytecode is never served. Set once during app startup, before
|
||||||
|
// the first CreateShader; empty (the default) disables caching. A missing
|
||||||
|
// or unwritable directory only costs compile time, never correctness.
|
||||||
|
void SetShaderBytecodeCacheDirectory(const char* path);
|
||||||
|
// The configured directory, or an empty string when caching is disabled.
|
||||||
|
const char* GetShaderBytecodeCacheDirectory();
|
||||||
|
|
||||||
} // namespace rex::graphics::nrhi
|
} // namespace rex::graphics::nrhi
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,14 @@
|
||||||
#include <rex/graphics/d3d12/native_rhi_d3d12.h>
|
#include <rex/graphics/d3d12/native_rhi_d3d12.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
|
@ -29,6 +34,107 @@ using nrhi::HeapKind;
|
||||||
using nrhi::ResourceState;
|
using nrhi::ResourceState;
|
||||||
using nrhi::TextureKind;
|
using nrhi::TextureKind;
|
||||||
|
|
||||||
|
// ---- Shader bytecode cache ------------------------------------------------
|
||||||
|
// D3DCompile of the scene uber shader costs whole seconds and runs on the
|
||||||
|
// render thread during pipeline creation, which holds the first presented
|
||||||
|
// frame (a launch black screen). Compiled DXBC is content-addressed on disk
|
||||||
|
// (see nrhi::SetShaderBytecodeCacheDirectory): the key hashes everything
|
||||||
|
// that affects codegen, so source edits miss naturally and entries never go
|
||||||
|
// stale. Every failure path falls back to compiling.
|
||||||
|
|
||||||
|
uint64_t Fnv1a64(const void* data, size_t size, uint64_t hash) {
|
||||||
|
const uint8_t* p = static_cast<const uint8_t*>(data);
|
||||||
|
for (size_t i = 0; i < size; ++i) {
|
||||||
|
hash ^= p[i];
|
||||||
|
hash *= 1099511628211ull;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path ShaderCachePath(const nrhi::ShaderDesc& desc,
|
||||||
|
const char* target) {
|
||||||
|
const char* dir = nrhi::GetShaderBytecodeCacheDirectory();
|
||||||
|
if (dir == nullptr || dir[0] == '\0') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// Two independent FNV-1a streams form a 128-bit key; the terminating NUL
|
||||||
|
// fed with each field keeps concatenations unambiguous.
|
||||||
|
uint64_t h1 = 14695981039346656037ull;
|
||||||
|
uint64_t h2 = 0x84222325CBF29CE4ull;
|
||||||
|
const auto feed_str = [&](const char* s) {
|
||||||
|
const size_t n = std::strlen(s) + 1;
|
||||||
|
h1 = Fnv1a64(s, n, h1);
|
||||||
|
h2 = Fnv1a64(s, n, h2);
|
||||||
|
};
|
||||||
|
feed_str("nrhi-dxbc-1"); // cache format tag
|
||||||
|
feed_str(target);
|
||||||
|
feed_str(desc.entry_point != nullptr ? desc.entry_point : "");
|
||||||
|
if (desc.macros != nullptr) {
|
||||||
|
for (uint32_t i = 0; desc.macros[i].name != nullptr; ++i) {
|
||||||
|
feed_str(desc.macros[i].name);
|
||||||
|
feed_str(desc.macros[i].value != nullptr ? desc.macros[i].value : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
feed_str(desc.hlsl_source != nullptr ? desc.hlsl_source : "");
|
||||||
|
char name[48];
|
||||||
|
std::snprintf(name, sizeof(name), "%016llx%016llx.dxbc",
|
||||||
|
static_cast<unsigned long long>(h1),
|
||||||
|
static_cast<unsigned long long>(h2));
|
||||||
|
return std::filesystem::path(dir) / name;
|
||||||
|
}
|
||||||
|
|
||||||
|
ID3DBlob* TryLoadCachedBlob(const std::filesystem::path& path) {
|
||||||
|
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||||
|
if (!f.is_open()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const std::streamoff size = f.tellg();
|
||||||
|
if (size < 8) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
ID3DBlob* blob = nullptr;
|
||||||
|
if (FAILED(D3DCreateBlob(static_cast<SIZE_T>(size), &blob))) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
f.seekg(0);
|
||||||
|
f.read(static_cast<char*>(blob->GetBufferPointer()),
|
||||||
|
static_cast<std::streamsize>(size));
|
||||||
|
// The DXBC container magic rejects truncated or foreign files.
|
||||||
|
if (!f.good() || std::memcmp(blob->GetBufferPointer(), "DXBC", 4) != 0) {
|
||||||
|
blob->Release();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StoreCachedBlob(const std::filesystem::path& path, ID3DBlob* blob) {
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(path.parent_path(), ec);
|
||||||
|
// Write-then-rename so a crash mid-write never leaves a truncated blob
|
||||||
|
// under the final name (the loader's DXBC magic check is the second line
|
||||||
|
// of defense). The per-process temp name keeps two instances compiling
|
||||||
|
// the same shader from interleaving writes.
|
||||||
|
std::filesystem::path tmp = path;
|
||||||
|
tmp += ".tmp" + std::to_string(GetCurrentProcessId());
|
||||||
|
{
|
||||||
|
std::ofstream f(tmp, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!f.is_open()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
f.write(static_cast<const char*>(blob->GetBufferPointer()),
|
||||||
|
static_cast<std::streamsize>(blob->GetBufferSize()));
|
||||||
|
if (!f.good()) {
|
||||||
|
f.close();
|
||||||
|
std::filesystem::remove(tmp, ec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::filesystem::rename(tmp, path, ec);
|
||||||
|
if (ec) {
|
||||||
|
std::filesystem::remove(tmp, ec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DXGI_FORMAT ToDxgi(Format format) {
|
DXGI_FORMAT ToDxgi(Format format) {
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case Format::kR8G8B8A8_UNORM:
|
case Format::kR8G8B8A8_UNORM:
|
||||||
|
|
@ -640,9 +746,12 @@ class NrDeviceD3D12 : public nrhi::Device {
|
||||||
++macro_count;
|
++macro_count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ID3DBlob* blob = nullptr;
|
|
||||||
ID3DBlob* errors = nullptr;
|
|
||||||
const char* target = desc.stage == nrhi::ShaderStage::kVertex ? "vs_5_0" : "ps_5_0";
|
const char* target = desc.stage == nrhi::ShaderStage::kVertex ? "vs_5_0" : "ps_5_0";
|
||||||
|
const std::filesystem::path cache_path = ShaderCachePath(desc, target);
|
||||||
|
ID3DBlob* blob = cache_path.empty() ? nullptr : TryLoadCachedBlob(cache_path);
|
||||||
|
if (blob == nullptr) {
|
||||||
|
ID3DBlob* errors = nullptr;
|
||||||
|
const auto compile_start = std::chrono::steady_clock::now();
|
||||||
HRESULT hr = D3DCompile(desc.hlsl_source, std::strlen(desc.hlsl_source),
|
HRESULT hr = D3DCompile(desc.hlsl_source, std::strlen(desc.hlsl_source),
|
||||||
desc.name != nullptr ? desc.name : "nrhi_shader",
|
desc.name != nullptr ? desc.name : "nrhi_shader",
|
||||||
macro_count != 0 ? macros : nullptr, nullptr, desc.entry_point,
|
macro_count != 0 ? macros : nullptr, nullptr, desc.entry_point,
|
||||||
|
|
@ -656,6 +765,16 @@ class NrDeviceD3D12 : public nrhi::Device {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
if (errors) errors->Release();
|
if (errors) errors->Release();
|
||||||
|
const auto compile_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now() - compile_start)
|
||||||
|
.count();
|
||||||
|
if (!cache_path.empty()) {
|
||||||
|
StoreCachedBlob(cache_path, blob);
|
||||||
|
}
|
||||||
|
REXLOG_INFO("nrhi-d3d12: compiled {} {} in {} ms{}",
|
||||||
|
desc.name != nullptr ? desc.name : "?", desc.entry_point, compile_ms,
|
||||||
|
cache_path.empty() ? "" : " (cached to disk)");
|
||||||
|
}
|
||||||
auto* shader = new NrShaderD3D12();
|
auto* shader = new NrShaderD3D12();
|
||||||
shader->blob = blob;
|
shader->blob = blob;
|
||||||
return shader;
|
return shader;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include <rex/graphics/native_guest_renderer.h>
|
#include <rex/graphics/native_guest_renderer.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include <rex/cvar.h>
|
#include <rex/cvar.h>
|
||||||
|
|
||||||
|
|
@ -146,3 +147,22 @@ bool ShouldSuppressPassAtPitch(uint32_t surface_pitch) {
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace rex::graphics
|
} // namespace rex::graphics
|
||||||
|
|
||||||
|
namespace rex::graphics::nrhi {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Set once during app startup before any CreateShader (see native_rhi.h);
|
||||||
|
// lives in this shared TU so every backend reads the same value.
|
||||||
|
std::string g_shader_bytecode_cache_dir;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void SetShaderBytecodeCacheDirectory(const char* path) {
|
||||||
|
g_shader_bytecode_cache_dir = path != nullptr ? path : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* GetShaderBytecodeCacheDirectory() {
|
||||||
|
return g_shader_bytecode_cache_dir.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace rex::graphics::nrhi
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
#include <rex/ui/overlay/fps_overlay.h>
|
#include <rex/ui/overlay/fps_overlay.h>
|
||||||
#include <rex/ui/overlay/settings_overlay.h>
|
#include <rex/ui/overlay/settings_overlay.h>
|
||||||
#include <rex/graphics/graphics_system.h>
|
#include <rex/graphics/graphics_system.h>
|
||||||
|
#include <rex/graphics/native_rhi.h>
|
||||||
#if REX_HAS_VULKAN
|
#if REX_HAS_VULKAN
|
||||||
#include <rex/graphics/vulkan/graphics_system.h>
|
#include <rex/graphics/vulkan/graphics_system.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -357,6 +358,16 @@ bool ReXApp::SetupEnvironment() {
|
||||||
config_path_ = path_config.config_path;
|
config_path_ = path_config.config_path;
|
||||||
resolved_defaults_ = std::move(path_config);
|
resolved_defaults_ = std::move(path_config);
|
||||||
|
|
||||||
|
// Native-render RHI shader bytecode cache: the D3D12 backend runtime-
|
||||||
|
// compiles its HLSL, and the largest scene shader costs seconds on the
|
||||||
|
// render thread at first-frame pipeline creation; cached bytecode turns
|
||||||
|
// every launch after the first into a file read. Must be set before the
|
||||||
|
// graphics system creates its first shader.
|
||||||
|
if (!cache_root_.empty()) {
|
||||||
|
graphics::nrhi::SetShaderBytecodeCacheDirectory(
|
||||||
|
(cache_root_ / "nrhi_shaders").string().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
// Late-phase logging
|
// Late-phase logging
|
||||||
std::string log_file_cvar = REXCVAR_GET(log_file);
|
std::string log_file_cvar = REXCVAR_GET(log_file);
|
||||||
std::string log_level_str = REXCVAR_GET(log_level);
|
std::string log_level_str = REXCVAR_GET(log_level);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue