Merge remote-tracking branch 'origin/master' into claude/issue-61

# Conflicts:
#	interfaces/Crafter.Graphics-Device.cppm
#	interfaces/Crafter.Graphics-VulkanBuffer.cppm
#	project.cpp
This commit is contained in:
catbot 2026-06-16 18:33:08 +00:00
commit 1f12f074b1
11 changed files with 860 additions and 9 deletions

View file

@ -82,7 +82,7 @@ void ComputeShader::Load(const std::filesystem::path& spvPath) {
.layout = VK_NULL_HANDLE,
};
Device::CheckVkResult(vkCreateComputePipelines(
Device::device, VK_NULL_HANDLE, 1, &info, nullptr, &pipeline));
Device::device, Device::pipelineCache, 1, &info, nullptr, &pipeline));
}
void ComputeShader::Dispatch(VkCommandBuffer cmd,

View file

@ -133,8 +133,99 @@ void Device::CheckVkResult(VkResult result) {
}
throw std::runtime_error(string_VkResult(result));
}
}
}
}
bool Device::PipelineCacheDataCompatible(std::span<const std::byte> data) {
// VkPipelineCacheHeaderVersionOne is a fixed 32-byte little-endian header
// (Vulkan spec 16.5.2): u32 headerSize, u32 headerVersion, u32 vendorID,
// u32 deviceID, then VK_UUID_SIZE bytes of pipelineCacheUUID. Parse the
// fields by offset rather than reinterpret_cast'ing the struct so the
// check is independent of the C struct's alignment/padding.
constexpr std::size_t headerBytes = 16 + VK_UUID_SIZE;
if (data.size() < headerBytes) {
return false;
}
auto readU32 = [&](std::size_t offset) {
std::uint32_t value;
std::memcpy(&value, data.data() + offset, sizeof(value));
return value;
};
const std::uint32_t headerSize = readU32(0);
const std::uint32_t headerVersion = readU32(4);
const std::uint32_t vendorID = readU32(8);
const std::uint32_t deviceID = readU32(12);
if (headerSize < headerBytes) {
return false;
}
if (headerVersion != VK_PIPELINE_CACHE_HEADER_VERSION_ONE) {
return false;
}
if (vendorID != deviceProperties.vendorID || deviceID != deviceProperties.deviceID) {
return false;
}
return std::memcmp(data.data() + 16, deviceProperties.pipelineCacheUUID, VK_UUID_SIZE) == 0;
}
void Device::LoadPipelineCache() {
std::vector<std::byte> initialData;
std::error_code ec;
if (std::filesystem::exists(pipelineCachePath, ec)) {
std::ifstream file(pipelineCachePath, std::ios::binary | std::ios::ate);
if (file) {
const std::streamoff size = file.tellg();
if (size > 0) {
initialData.resize(static_cast<std::size_t>(size));
file.seekg(0);
file.read(reinterpret_cast<char*>(initialData.data()), size);
if (!file) {
initialData.clear(); // partial/short read — start cold
}
}
}
}
// Drop a blob the current driver/device wouldn't accept: a header from a
// different GPU (or a corrupt/empty file) would at best be ignored and at
// worst rejected. Starting empty just costs a one-time cold compile.
if (!PipelineCacheDataCompatible(initialData)) {
initialData.clear();
}
VkPipelineCacheCreateInfo info {
.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO,
.initialDataSize = initialData.size(),
.pInitialData = initialData.empty() ? nullptr : initialData.data(),
};
CheckVkResult(vkCreatePipelineCache(device, &info, nullptr, &pipelineCache));
}
void Device::SavePipelineCache() {
if (pipelineCache == VK_NULL_HANDLE) {
return;
}
// Two-call idiom: query size, then fetch. Best-effort — a failure here only
// forfeits the next run's warm start, so it must never throw out of an
// atexit handler.
std::size_t size = 0;
if (vkGetPipelineCacheData(device, pipelineCache, &size, nullptr) != VK_SUCCESS || size == 0) {
return;
}
std::vector<std::byte> data(size);
if (vkGetPipelineCacheData(device, pipelineCache, &size, data.data()) != VK_SUCCESS) {
return;
}
std::ofstream file(pipelineCachePath, std::ios::binary | std::ios::trunc);
if (file) {
file.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(size));
}
}
VkBool32 onError(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT type, const VkDebugUtilsMessengerCallbackDataEXT* callbackData, void* userData)
{
@ -603,6 +694,9 @@ void Device::Initialize() {
.pNext = &rayTracingProperties
};
vkGetPhysicalDeviceProperties2(physDevice, &properties2);
// Keep the core properties around for the pipeline-cache identity check
// (vendorID / deviceID / pipelineCacheUUID).
deviceProperties = properties2.properties;
// Cache the flush/invalidate alignment for ranged host-memory flushes
// (VulkanBuffer::FlushDevice(offset,bytes)).
@ -814,6 +908,14 @@ void Device::Initialize() {
memoryDecompressionSupported = false;
}
}
// Create the shared pipeline cache (seeded from disk when a compatible
// blob exists) and arrange for it to be written back at process exit. The
// device handle is a never-destroyed static, so it is still valid when the
// atexit handler runs. Registered once — Initialize is the single device
// bring-up.
LoadPipelineCache();
std::atexit(SavePipelineCache);
}
std::uint32_t Device::GetMemoryType(uint32_t typeBits, VkMemoryPropertyFlags required, VkMemoryPropertyFlags preferred) {

View file

@ -88,12 +88,29 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index, RTB
}
if (topologyChanged) {
// Resize the host-visible inputs to match the new count.
// Grow the host-visible inputs on a high-water mark rather than
// resizing to the exact count every topology change. These buffers
// only need to hold *at least* primitiveCount entries — the AS build
// reads exactly primitiveCount of them via tlasRangeInfo, and the
// copy loop below writes [0, primitiveCount) — so an allocation left
// over from a larger earlier frame is reused as-is. This drops the
// two host-buffer reallocations on a count decrease (and on an
// increase that still fits the previous high-water capacity),
// leaving only the AS storage + scratch rebuild below, which is tied
// to the AS itself and dominates this path regardless.
//
// instanceBuffer and metadataBuffer grow in lockstep (always resized
// together to the same entry count), so one capacity check covers
// both. size is only meaningful once buffer is non-null, so the
// null check must short-circuit before the division.
// STORAGE_BUFFER_BIT is required because the application's compute
// shaders bind this buffer as a storage SSBO (e.g. to write
// shaders bind these buffers as storage SSBOs (e.g. to write
// per-instance transforms directly into the TLAS instance data).
tlas.instanceBuffer.Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, primitiveCount);
tlas.metadataBuffer.Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, primitiveCount);
if (tlas.instanceBuffer.buffer == VK_NULL_HANDLE
|| primitiveCount > tlas.instanceBuffer.size / sizeof(VkAccelerationStructureInstanceKHR)) {
tlas.instanceBuffer.Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, primitiveCount);
tlas.metadataBuffer.Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, primitiveCount);
}
}
for(std::uint32_t i = 0; i < primitiveCount; i++) {