feat(host): vendor PyroWave + minimal Granite subset as crates/pyrowave-sys

Phase 0 of design/pyrowave-codec-plan.md — the opt-in wired-LAN ultra-low-
latency codec. Vendored at upstream 509e4f88 (API 0.4.0, Granite 44362775,
volk + vulkan-headers pins in PUNKTFUNK-VENDOR.txt), pruned to the 6.6 MB
the standalone no-renderer build needs; scripts/vendor-pyrowave.sh
reproduces the tree (a pin bump is protocol-affecting, plan §4.2).

build.rs drives the wrapper CMakeLists (static archives incl. a static
C-API lib upstream only ships shared) + bindgen over pyrowave.h; Linux and
Windows only, empty stub elsewhere (Apple gets a native Metal port, §4.7).
Offline-safe by construction: no network, no system lib, vendored Vulkan
headers — same model as the opus dep (flatpak builder has no network).

Phase-0 validation on .21 (RTX 5070 Ti, driver 610.43.03):
- upstream pyrowave-c-test + interop test (incl. dmabuf/DRM-modifier
  Vulkan<->Vulkan) pass, from the pristine AND the pruned tree
- GPU kernel times at ~1.6 bpp noise: encode/decode 0.090/0.042 ms @800p,
  0.146/0.067 @1080p, 0.226/0.103 @1440p, 0.477/0.201 @4K — order of
  magnitude under NVENC's 1-2 ms retrieve, CBR lands within ~100 B of
  target
- cargo test -p pyrowave-sys green (static link + API-version pin check)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 00:35:10 +02:00
parent 1b73361372
commit 4c3b11445c
396 changed files with 140058 additions and 0 deletions
@@ -0,0 +1,126 @@
add_granite_internal_lib(granite-vulkan
context.cpp context.hpp
vulkan_headers.hpp vulkan_prerotate.hpp
device.cpp device.hpp
wsi.cpp wsi.hpp
wsi_pacer.cpp wsi_pacer.hpp
buffer_pool.cpp buffer_pool.hpp
image.cpp image.hpp
cookie.cpp cookie.hpp
sampler.cpp sampler.hpp
command_pool.cpp command_pool.hpp
fence_manager.cpp fence_manager.hpp
descriptor_set.cpp descriptor_set.hpp
semaphore_manager.cpp semaphore_manager.hpp
command_buffer.cpp command_buffer.hpp
shader.cpp shader.hpp
render_pass.cpp render_pass.hpp
buffer.cpp buffer.hpp
rtas.cpp rtas.hpp
indirect_layout.cpp indirect_layout.hpp
pipeline_cache.cpp pipeline_cache.hpp
semaphore.cpp semaphore.hpp
memory_allocator.cpp memory_allocator.hpp
fence.hpp fence.cpp
format.hpp
limits.hpp
type_to_string.hpp
quirks.hpp
vulkan_common.hpp
event_manager.cpp event_manager.hpp
breadcrumbs.cpp breadcrumbs.hpp
pipeline_event.cpp pipeline_event.hpp
query_pool.cpp query_pool.hpp
texture/texture_format.cpp texture/texture_format.hpp)
if (WIN32 AND GRANITE_VULKAN_DXGI_INTEROP)
target_sources(granite-vulkan PRIVATE wsi_dxgi.cpp wsi_dxgi.hpp)
target_compile_definitions(granite-vulkan PUBLIC HAVE_WSI_DXGI_INTEROP)
endif()
target_include_directories(granite-vulkan PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
if (GRANITE_RENDERDOC_CAPTURE)
target_link_libraries(granite-vulkan PRIVATE granite-renderdoc-app)
target_sources(granite-vulkan PRIVATE renderdoc_capture.cpp)
if (NOT WIN32)
target_link_libraries(granite-vulkan PRIVATE dl)
endif()
endif()
if (GRANITE_VULKAN_SYSTEM_HANDLES)
if (GRANITE_VULKAN_FOSSILIZE)
target_compile_definitions(granite-vulkan PUBLIC GRANITE_VULKAN_FOSSILIZE)
target_sources(granite-vulkan PRIVATE device_fossilize.cpp device_fossilize.hpp)
target_link_libraries(granite-vulkan PUBLIC fossilize)
endif()
target_compile_definitions(granite-vulkan PUBLIC GRANITE_VULKAN_SYSTEM_HANDLES)
target_sources(granite-vulkan PRIVATE
managers/shader_manager.cpp
managers/shader_manager.hpp
managers/resource_manager.cpp
managers/resource_manager.hpp)
target_sources(granite-vulkan PRIVATE
texture/memory_mapped_texture.cpp texture/memory_mapped_texture.hpp
mesh/meshlet.hpp mesh/meshlet.cpp
texture/texture_files.cpp texture/texture_files.hpp
texture/texture_decoder.cpp texture/texture_decoder.hpp)
target_link_libraries(granite-vulkan
PUBLIC granite-filesystem
PRIVATE granite-threading granite-rapidjson granite-stb granite-math)
target_include_directories(granite-vulkan PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/managers
${CMAKE_CURRENT_SOURCE_DIR}/texture
${CMAKE_CURRENT_SOURCE_DIR}/mesh)
if (GRANITE_VULKAN_SHADER_MANAGER_RUNTIME_COMPILER)
target_compile_definitions(granite-vulkan PUBLIC GRANITE_VULKAN_SHADER_MANAGER_RUNTIME_COMPILER=1)
target_link_libraries(granite-vulkan PRIVATE granite-compiler)
endif()
endif()
if (${CMAKE_BUILD_TYPE} MATCHES "Debug")
target_compile_definitions(granite-vulkan PUBLIC VULKAN_DEBUG)
endif()
target_link_libraries(granite-vulkan
PRIVATE granite-volk
PUBLIC granite-util granite-volk-headers)
if (GRANITE_VULKAN_SPIRV_CROSS)
target_link_libraries(granite-vulkan PRIVATE spirv-cross-core)
target_compile_definitions(granite-vulkan PRIVATE GRANITE_VULKAN_SPIRV_CROSS=1)
endif()
if (ANDROID AND GRANITE_ANDROID_SWAPPY)
find_package(games-frame-pacing REQUIRED CONFIG)
target_link_libraries(granite-vulkan PRIVATE games-frame-pacing::swappy)
target_compile_definitions(granite-vulkan PRIVATE HAVE_SWAPPY)
endif()
if (GRANITE_SHIPPING)
target_compile_definitions(granite-vulkan PUBLIC GRANITE_SHIPPING)
endif()
if (GRANITE_FFMPEG_VULKAN)
target_compile_definitions(granite-vulkan PRIVATE HAVE_FFMPEG_VULKAN)
endif()
if (GRANITE_VULKAN_PROFILES)
# Must be defined by caller as an INTERFACE library before including Granite.
if (NOT TARGET granite-vulkan-profiles)
message(FATAL_ERROR "granite-vulkan-profiles is not a target. This must be defined by caller before add_subdirectory(Granite).")
endif()
target_link_libraries(granite-vulkan PRIVATE granite-vulkan-profiles)
target_compile_definitions(granite-vulkan PRIVATE GRANITE_VULKAN_PROFILES)
endif()
if (GRANITE_VULKAN_POST_MORTEM)
add_subdirectory(post-mortem)
target_link_libraries(granite-vulkan PRIVATE granite-vulkan-post-mortem)
target_compile_definitions(granite-vulkan PUBLIC HAVE_GRANITE_VULKAN_POST_MORTEM)
endif()
@@ -0,0 +1,568 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "breadcrumbs.hpp"
#include "shader.hpp"
#include "device.hpp"
#include "timer.hpp"
#include <time.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
namespace Vulkan
{
void CheckpointString::report(FILE *file)
{
fprintf(file, "%s\n", str.c_str());
}
void CheckpointDispatch::report(FILE *file)
{
fprintf(file, "Dispatch (%u, %u, %u)\n", x, y, z);
}
void CheckpointDraw::report(FILE *file)
{
fprintf(file, "Draw (%u, %u, %d, %u)\n",
vertex_count, instance_count, vertex_offset, instance_offset);
}
void CheckpointDrawIndexed::report(FILE *file)
{
fprintf(file, "DrawIndexed (%u, %u, %u, %d, %u)\n",
index_count, instance_count, first_index, vertex_offset, instance_offset);
}
void CheckpointMeshDispatch::report(FILE *file)
{
fprintf(file, "MeshTasks (%u, %u, %u)\n", x, y, z);
}
void CheckpointIndirectBase::report(FILE *file)
{
fprintf(file, "%s (#%016llx)\n", tag, static_cast<unsigned long long>(va));
}
void CheckpointMultiIndirectBase::report(FILE *file)
{
fprintf(file, "%s (#%016llx), count %u, stride %u\n",
tag,
static_cast<unsigned long long>(va),
count, stride);
}
void CheckpointShader::report(FILE *file)
{
fprintf(file, "Shader (#%016llx)\n", static_cast<unsigned long long>(shader->get_hash()));
}
static void *nv_encode_checkpoint(uint32_t index, uint32_t counter)
{
return reinterpret_cast<void *>(uintptr_t(index) + uintptr_t(counter) * BreadcrumbsTracker::MaxCommandBuffers);
}
static uint32_t nv_decode_context(void *opaque)
{
return reinterpret_cast<uintptr_t>(opaque) % BreadcrumbsTracker::MaxCommandBuffers;
}
static uint32_t nv_decode_counter(void *opaque)
{
return reinterpret_cast<uintptr_t>(opaque) / BreadcrumbsTracker::MaxCommandBuffers;
}
void BreadcrumbsTracker::init(Device *device_)
{
device = device_;
if (!device->get_device_features().supports_post_mortem)
return;
active = true;
command_buffers.resize(MaxCommandBuffers);
vacant_command_buffers.reserve(MaxCommandBuffers);
for (uint32_t i = MaxCommandBuffers; i; i--)
vacant_command_buffers.push_back(i - 1);
if (device->get_device_features().supports_amd_buffer_marker)
{
BufferCreateInfo info = {};
info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
info.domain = BufferDomain::DebugReadback;
info.size = MaxCommandBuffers * sizeof(uint32_t) * 2;
info.misc = BUFFER_MISC_ZERO_INITIALIZE_BIT;
amd_marker_buffer = device->create_buffer(info).release();
}
blocks.init(CheckpointObjectSize);
}
void BreadcrumbsTracker::deinit()
{
for (auto &cmd : command_buffers)
reset_command_buffer(cmd);
if (amd_marker_buffer)
amd_marker_buffer->release_reference();
}
BufferMarkerHandle BreadcrumbsTracker::allocate_command_buffer(VkCommandBuffer cmd)
{
if (!active)
return {};
std::lock_guard<std::mutex> holder{lock};
if (vacant_command_buffers.empty())
return {};
BufferMarkerHandle ret = { vacant_command_buffers.back() };
vacant_command_buffers.pop_back();
command_buffers[ret.index].cmd = cmd;
return ret;
}
void BreadcrumbsTracker::free_command_buffer(BufferMarkerHandle handle)
{
if (handle.index == BufferMarkerHandle::Invalid)
return;
std::lock_guard<std::mutex> holder{lock};
assert(handle.index < MaxCommandBuffers);
vacant_command_buffers.push_back(handle.index);
reset_command_buffer(command_buffers[handle.index]);
}
void BreadcrumbsTracker::reset_command_buffer(CommandBuffer &cmd)
{
for (auto &check : cmd.checkpoints)
{
if (check.iface)
{
check.iface->~CheckpointReportInterface();
blocks.free(reinterpret_cast<uint8_t *>(check.iface));
}
}
// Free the memory too to avoid extreme bloat.
cmd = {};
}
void BreadcrumbsTracker::begin(BufferMarkerHandle handle)
{
if (handle.index == BufferMarkerHandle::Invalid)
return;
auto &cmd = command_buffers[handle.index];
cmd.counter++;
cmd.checkpoints.push_back({ nullptr, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, cmd.counter });
if (device->get_device_features().supports_nv_checkpoints)
{
cmd.checkpoints.push_back({ nullptr, VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, cmd.counter });
// A checkpoint is implicitly a top and a bottom marker.
device->get_device_table().vkCmdSetCheckpointNV(cmd.cmd, nv_encode_checkpoint(handle.index, cmd.counter));
}
else if (device->get_device_features().supports_amd_buffer_marker)
{
device->get_device_table().vkCmdWriteBufferMarkerAMD(cmd.cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
amd_marker_buffer->get_buffer(),
(2 * handle.index + 0) * sizeof(uint32_t), cmd.counter);
}
}
void BreadcrumbsTracker::signal(BufferMarkerHandle handle)
{
if (handle.index == BufferMarkerHandle::Invalid)
return;
auto &cmd = command_buffers[handle.index];
if (device->get_device_features().supports_nv_checkpoints)
{
cmd.counter++;
cmd.checkpoints.push_back({ nullptr, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, cmd.counter });
cmd.checkpoints.push_back({ nullptr, VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, cmd.counter });
device->get_device_table().vkCmdSetCheckpointNV(cmd.cmd, nv_encode_checkpoint(handle.index, cmd.counter));
}
else if (device->get_device_features().supports_amd_buffer_marker)
{
cmd.checkpoints.push_back({ nullptr, VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, cmd.counter });
device->get_device_table().vkCmdWriteBufferMarkerAMD(cmd.cmd, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
amd_marker_buffer->get_buffer(),
(2 * handle.index + 1) * sizeof(uint32_t), cmd.counter);
cmd.counter++;
cmd.checkpoints.push_back({ nullptr, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, cmd.counter });
device->get_device_table().vkCmdWriteBufferMarkerAMD(cmd.cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
amd_marker_buffer->get_buffer(),
(2 * handle.index + 0) * sizeof(uint32_t), cmd.counter);
}
}
void BreadcrumbsTracker::end(BufferMarkerHandle handle)
{
if (handle.index == BufferMarkerHandle::Invalid)
return;
auto &cmd = command_buffers[handle.index];
cmd.counter = UINT32_MAX;
if (device->get_device_features().supports_nv_checkpoints)
{
device->get_device_table().vkCmdSetCheckpointNV(cmd.cmd, nv_encode_checkpoint(handle.index, cmd.counter));
}
else if (device->get_device_features().supports_amd_buffer_marker)
{
device->get_device_table().vkCmdWriteBufferMarkerAMD(cmd.cmd, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
amd_marker_buffer->get_buffer(),
(2 * handle.index + 0) * sizeof(uint32_t), cmd.counter);
device->get_device_table().vkCmdWriteBufferMarkerAMD(cmd.cmd, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
amd_marker_buffer->get_buffer(),
(2 * handle.index + 1) * sizeof(uint32_t), cmd.counter);
}
cmd.checkpoints.push_back({ nullptr, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, cmd.counter });
cmd.checkpoints.push_back({ nullptr, VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, cmd.counter });
// Avoid breadcrumbs spilling between command buffers.
VkDependencyInfo dep = { VK_STRUCTURE_TYPE_DEPENDENCY_INFO };
VkMemoryBarrier2 bar = { VK_STRUCTURE_TYPE_MEMORY_BARRIER_2 };
dep.memoryBarrierCount = 1;
dep.pMemoryBarriers = &bar;
bar.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
bar.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT;
bar.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
bar.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT;
device->get_device_table().vkCmdPipelineBarrier2(cmd.cmd, &dep);
}
void BreadcrumbsTracker::report_command_list(FILE *file, CommandBuffer &cmd, uint32_t top_marker, uint32_t bottom_marker)
{
bool observed_begin_cmd = false;
bool observed_end_cmd = false;
fprintf(file, "\n=== Command Buffer ===\n");
if (bottom_marker == 0)
{
fprintf(file, "=== Crash region BEGIN ===\n");
observed_begin_cmd = true;
}
for (auto &check : cmd.checkpoints)
{
if (!observed_end_cmd && check.stages == VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT && check.counter > top_marker)
{
// The command processor did not reach this checkpoint. Any command after this point cannot be the culprit.
fprintf(file, "=== Crash region END ===\n");
observed_end_cmd = true;
}
if (check.iface)
check.iface->report(file);
if (!observed_begin_cmd && check.stages == VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT && check.counter == bottom_marker)
{
// The GPU completed all commands up to this point and is the last counter that was completed.
// Crash must be after this point.
fprintf(file, "=== Crash region BEGIN ===\n");
observed_begin_cmd = true;
}
}
if (top_marker == UINT32_MAX)
fprintf(file, "=== Crash region END ===\n");
fprintf(file, "====================\n");
}
void BreadcrumbsTracker::report_command_list_amd(FILE *file, uint32_t index)
{
auto &cmd = command_buffers[index];
// Unused, cannot be the culprit.
if (cmd.counter == 0)
return;
auto *ptr = static_cast<const uint32_t *>(device->map_host_buffer(*amd_marker_buffer, MEMORY_ACCESS_READ_BIT, 2 * sizeof(uint32_t) * index, sizeof(uint32_t) * 2));
uint32_t top_marker = ptr[0];
uint32_t bottom_marker = ptr[1];
// The command buffer is done executing.
if (top_marker == UINT32_MAX && bottom_marker == UINT32_MAX)
return;
// Never started executing properly. Cannot be a culprit.
if (top_marker == 0 && bottom_marker == 0)
return;
// Edge case where we crashed before the first command of a recycled command buffer completed.
if (top_marker > 0 && bottom_marker == UINT32_MAX)
bottom_marker = 0;
fprintf(file, "Reporting for command index %u, top marker %u, bottom marker %u\n", index, top_marker, bottom_marker);
report_command_list(file, cmd, top_marker, bottom_marker);
reported = true;
}
void BreadcrumbsTracker::notify_device_hung()
{
if (!active)
return;
std::lock_guard<std::mutex> holder{lock};
if (reported)
return;
char path[256];
std::time_t t = std::time(nullptr);
std::tm gmt;
// Date-time in C was always a great time :')
#ifdef _WIN32
gmtime_s(&gmt, &t);
#else
gmtime_r(&t, &gmt);
#endif
// Windows does not like colons in path names, so %T breaks.
strftime(path, sizeof(path), "granite-post-mortem-%Y-%m-%d-%H-%M-%S.txt", &gmt);
LOGE("Device hung ... Attempting to grab post-mortem data to: %s\n", path);
auto start_time = Util::get_current_time_nsecs();
auto end_time = start_time + 5ll * 1000 * 1000 * 1000;
// Try to observe device lost properly.
VkResult vr = VK_SUCCESS;
while (vr != VK_ERROR_DEVICE_LOST && Util::get_current_time_nsecs() < end_time)
vr = device->get_device_table().vkDeviceWaitIdle(device->get_device());
if (vr == VK_ERROR_DEVICE_LOST)
LOGE("Observed device lost after %.3f seconds of blocking.\n", 1e-9 * (Util::get_current_time_nsecs() - start_time));
FILE *file = fopen(path, "w");
if (!file)
{
LOGE("Failed to open \"%s\", dumping to stderr instead.\n", path);
file = stderr;
}
if (vr != VK_ERROR_DEVICE_LOST)
{
if (file != stderr)
LOGE("Cannot observe device lost state, report may be incomplete ...\n");
fprintf(file, "Cannot observe device lost state, report may be incomplete ...\n");
}
fprintf(file, "Post-mortem analysis ...\n");
if (device->get_device_features().supports_nv_checkpoints)
{
auto &queues = device->get_queue_info().queues;
for (uint32_t i = 0; i < QUEUE_INDEX_COUNT; i++)
{
if (queues[i] == VK_NULL_HANDLE || std::find(queues, queues + i, queues[i]) != queues + i)
continue;
auto &table = device->get_device_table();
uint32_t count;
table.vkGetQueueCheckpointDataNV(queues[i], &count, nullptr);
if (count == 0)
continue;
std::vector<VkCheckpointDataNV> checkpoints(count);
for (auto &check : checkpoints)
check.sType = VK_STRUCTURE_TYPE_CHECKPOINT_DATA_NV;
table.vkGetQueueCheckpointDataNV(queues[i], &count, checkpoints.data());
uint32_t top_marker = 0;
uint32_t bottom_marker = 0;
uint32_t top_context = BufferMarkerHandle::Invalid;
uint32_t bottom_context = BufferMarkerHandle::Invalid;
for (auto &check : checkpoints)
{
uint32_t context = nv_decode_context(check.pCheckpointMarker);
uint32_t counter = nv_decode_counter(check.pCheckpointMarker);
if (check.stage == VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT)
{
top_marker = counter;
top_context = context;
}
else if (check.stage == VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT)
{
bottom_marker = counter;
bottom_context = context;
}
}
if (top_context == BufferMarkerHandle::Invalid)
fprintf(file, "Missing context, this should not happen.\n");
else if (top_context != bottom_context || top_context == BufferMarkerHandle::Invalid)
fprintf(file, "Mismatching contexts, this should not happen.\n");
else
{
report_command_list(file, command_buffers[top_context], top_marker, bottom_marker);
reported = true;
}
}
}
else if (device->get_device_features().supports_amd_buffer_marker)
{
for (uint32_t i = 0; i < MaxCommandBuffers; i++)
report_command_list_amd(file, i);
}
// Need to observe the device lost properly first before we can query fault information.
auto &table = device->get_device_table();
const auto addr_type_to_str = [](VkDeviceFaultAddressTypeKHR type)
{
switch (type)
{
case VK_DEVICE_FAULT_ADDRESS_TYPE_NONE_KHR: return "None";
case VK_DEVICE_FAULT_ADDRESS_TYPE_READ_INVALID_KHR: return "ReadInvalid";
case VK_DEVICE_FAULT_ADDRESS_TYPE_WRITE_INVALID_KHR: return "WriteInvalid";
case VK_DEVICE_FAULT_ADDRESS_TYPE_EXECUTE_INVALID_KHR: return "ExecuteInvalid";
case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_UNKNOWN_KHR: return "IPUnknown";
case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_INVALID_KHR: return "IPInvalid";
case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_FAULT_KHR: return "IPFault";
default: return "???";
}
};
const auto report_address = [&](const char *tag, const VkDeviceFaultAddressInfoKHR &info)
{
fprintf(file, " %s fault: %s\n", tag, addr_type_to_str(info.addressType));
fprintf(file, " %s address: #%016llx\n", tag,
static_cast<unsigned long long>(info.reportedAddress));
fprintf(file, " %s precision: #%016llx\n", tag,
static_cast<unsigned long long>(info.addressPrecision));
};
const auto report_vendor = [&](const VkDeviceFaultVendorInfoKHR &info)
{
fprintf(file, " Vendor desc: %s\n", info.description);
fprintf(file, " Vendor fault code: %llu\n",
static_cast<unsigned long long>(info.vendorFaultCode));
fprintf(file, " Vendor fault data: %llu\n",
static_cast<unsigned long long>(info.vendorFaultData));
};
if (device->get_device_features().fault_features_khr.deviceFault)
{
std::vector<VkDeviceFaultInfoKHR> faults;
uint32_t count;
if (table.vkGetDeviceFaultReportsKHR(device->get_device(), UINT64_MAX, &count, nullptr) != VK_SUCCESS)
{
fprintf(file, "Failed to get fault reports.\n");
return;
}
faults.resize(count);
for (auto &fault : faults)
fault.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_INFO_KHR;
if (table.vkGetDeviceFaultReportsKHR(device->get_device(), UINT64_MAX, &count, faults.data()) != VK_SUCCESS)
{
fprintf(file, "Failed to get fault reports.\n");
return;
}
for (auto &fault : faults)
{
fprintf(file, "=== Fault ===\n");
fprintf(file, " Desc: %s\n", fault.description);
fprintf(file, " groupID: %llu\n", static_cast<unsigned long long>(fault.groupId));
if (fault.flags & VK_DEVICE_FAULT_FLAG_DEVICE_LOST_KHR)
fprintf(file, " Fault caused DEVICE_LOST\n");
if (fault.flags & VK_DEVICE_FAULT_FLAG_WATCHDOG_TIMEOUT_KHR)
fprintf(file, " GPU Timeout\n");
if (fault.flags & VK_DEVICE_FAULT_FLAG_OVERFLOW_KHR)
fprintf(file, " Fault buffer overflowed\n");
if (fault.flags & VK_DEVICE_FAULT_FLAG_VENDOR_KHR)
report_vendor(fault.vendorInfo);
if (fault.flags & VK_DEVICE_FAULT_FLAG_MEMORY_ADDRESS_KHR)
report_address("Memory", fault.faultAddressInfo);
if (fault.flags & VK_DEVICE_FAULT_FLAG_INSTRUCTION_ADDRESS_KHR)
report_address("Instruction ", fault.instructionAddressInfo);
}
}
else
{
VkDeviceFaultCountsEXT counts = { VK_STRUCTURE_TYPE_DEVICE_FAULT_COUNTS_EXT };
VkDeviceFaultInfoEXT fault = { VK_STRUCTURE_TYPE_DEVICE_FAULT_INFO_EXT };
if (table.vkGetDeviceFaultInfoEXT(device->get_device(), &counts, nullptr) != VK_SUCCESS)
{
fprintf(file, "Failed to get fault reports.\n");
return;
}
std::vector<VkDeviceFaultAddressInfoEXT> addresses(counts.addressInfoCount);
std::vector<VkDeviceFaultVendorInfoEXT> vendor_infos(counts.vendorInfoCount);
uint8_t *vendor_data = counts.vendorBinarySize ? new uint8_t[counts.vendorBinarySize] : nullptr;
fault.pAddressInfos = addresses.data();
fault.pVendorInfos = vendor_infos.data();
fault.pVendorBinaryData = vendor_data;
if (table.vkGetDeviceFaultInfoEXT(device->get_device(), &counts, &fault) != VK_SUCCESS)
{
fprintf(file, "Failed to get fault reports.\n");
return;
}
for (uint32_t i = 0; i < counts.addressInfoCount; i++)
report_address("Memory", addresses[i]);
for (uint32_t i = 0; i < counts.vendorInfoCount; i++)
report_vendor(vendor_infos[i]);
delete[] vendor_data;
}
fprintf(file, "... DONE\n");
if (file != stderr)
fclose(file);
LOGE("Completed post-mortem analysis, will crash now.\n");
#ifdef _WIN32
char msg[512];
snprintf(msg, sizeof(msg), "GPU crashed, see post-mortem log in %s. Application will now terminate.", path);
MessageBoxA(nullptr, msg, "Granite Post Mortem", MB_OK);
TerminateProcess(GetCurrentProcess(), 1);
#endif
std::terminate();
}
}
@@ -0,0 +1,222 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "slab_allocator.hpp"
#include "vulkan_headers.hpp"
#include "vulkan_common.hpp"
#include <mutex>
#include <assert.h>
#include <stdio.h>
namespace Vulkan
{
class Device;
class Buffer;
class Shader;
struct CheckpointReportInterface
{
virtual void report(FILE *file) = 0;
virtual ~CheckpointReportInterface() = default;
};
struct CheckpointString : CheckpointReportInterface
{
CheckpointString(std::string str_) : str(std::move(str_)) {}
void report(FILE *file) override;
std::string str;
};
struct CheckpointDispatch : CheckpointReportInterface
{
CheckpointDispatch(uint32_t x_, uint32_t y_, uint32_t z_)
: x(x_), y(y_), z(z_) {}
void report(FILE *file) override;
uint32_t x, y, z;
};
struct CheckpointDraw : CheckpointReportInterface
{
CheckpointDraw(uint32_t vertex_count_, uint32_t instance_count_, int32_t vertex_offset_, uint32_t instance_offset_)
: vertex_count(vertex_count_), instance_count(instance_count_)
, vertex_offset(vertex_offset_), instance_offset(instance_offset_) {}
void report(FILE *file) override;
uint32_t vertex_count;
uint32_t instance_count;
int32_t vertex_offset;
uint32_t instance_offset;
};
struct CheckpointDrawIndexed : CheckpointReportInterface
{
CheckpointDrawIndexed(uint32_t index_count_, uint32_t instance_count_, uint32_t first_index_, int32_t vertex_offset_, uint32_t instance_offset_)
: index_count(index_count_), instance_count(instance_count_), first_index(first_index_)
, vertex_offset(vertex_offset_), instance_offset(instance_offset_) {}
void report(FILE *file) override;
uint32_t index_count;
uint32_t instance_count;
uint32_t first_index;
int32_t vertex_offset;
uint32_t instance_offset;
};
struct CheckpointMeshDispatch : CheckpointReportInterface
{
CheckpointMeshDispatch(uint32_t x_, uint32_t y_, uint32_t z_)
: x(x_), y(y_), z(z_) {}
void report(FILE *file) override;
uint32_t x, y, z;
};
struct CheckpointIndirectBase : CheckpointReportInterface
{
CheckpointIndirectBase(const char *tag_, VkDeviceAddress va_) : tag(tag_), va(va_) {}
void report(FILE *file) override;
const char *tag;
VkDeviceAddress va;
};
struct CheckpointMultiIndirectBase : CheckpointReportInterface
{
CheckpointMultiIndirectBase(const char *tag_, VkDeviceAddress va_,
uint32_t count_, uint32_t stride_)
: tag(tag_), va(va_), count(count_), stride(stride_) {}
void report(FILE *file) override;
const char *tag;
VkDeviceAddress va;
uint32_t count;
uint32_t stride;
};
struct CheckpointMultiIndirectCountBase : CheckpointReportInterface
{
CheckpointMultiIndirectCountBase(const char *tag_, VkDeviceAddress va_, VkDeviceAddress count_va_,
uint32_t count_, uint32_t stride_)
: tag(tag_), va(va_), count_va(count_va_), count(count_), stride(stride_) {}
void report(FILE *file) override
{
fprintf(file, "%s (#%016llx), countVA (#%016llx), count %u, stride %u\n",
tag,
static_cast<unsigned long long>(va),
static_cast<unsigned long long>(count_va),
count, stride);
}
const char *tag;
VkDeviceAddress va;
VkDeviceAddress count_va;
uint32_t count;
uint32_t stride;
};
struct CheckpointShader : CheckpointReportInterface
{
CheckpointShader(const Shader *shader_) : shader(shader_) {}
void report(FILE *file) override;
const Shader *shader;
};
// 1 second.
// It seems like if we have too long timeout, NV driver on Windows is broken
// and loses checkpoint information (?!?!).
// It seems like we have to call it before we start getting vkQueueSubmit() device losts,
// then it's too late.
static constexpr uint64_t PostMortemTimeout = 1ull * 1000 * 1000 * 1000;
class BreadcrumbsTracker
{
public:
enum { CheckpointObjectSize = 64, MaxCommandBuffers = 8 * 1024 };
void init(Device *device);
void deinit();
BufferMarkerHandle allocate_command_buffer(VkCommandBuffer cmd);
void free_command_buffer(BufferMarkerHandle handle);
void begin(BufferMarkerHandle handle);
template <typename T, typename... Ts>
void checkpoint(BufferMarkerHandle handle, Ts &&... ts)
{
if (!active)
return;
static_assert(sizeof(T) <= CheckpointObjectSize, "Object size is too large.");
auto *raw = reinterpret_cast<T *>(blocks.allocate());
new (raw) T(std::forward<Ts>(ts)...);
Checkpoint checkpoint = {};
checkpoint.iface = raw;
assert(handle.index < command_buffers.size());
command_buffers[handle.index].checkpoints.push_back(checkpoint);
}
template <typename T, typename... Ts>
void checkpoint_with_signal(BufferMarkerHandle handle, Ts &&... ts)
{
checkpoint<T>(handle, std::forward<Ts>(ts)...);
signal(handle);
}
void signal(BufferMarkerHandle handle);
void end(BufferMarkerHandle handle);
void notify_device_hung();
private:
Device *device = nullptr;
bool active = false;
Buffer *amd_marker_buffer = nullptr;
std::mutex lock;
bool reported = false;
struct Checkpoint
{
CheckpointReportInterface *iface;
VkPipelineStageFlags2 stages;
uint32_t counter;
};
struct CommandBuffer
{
std::vector<Checkpoint> checkpoints;
uint32_t counter = 0;
VkCommandBuffer cmd;
};
std::vector<CommandBuffer> command_buffers;
std::vector<uint32_t> vacant_command_buffers;
Util::ThreadSafeSlabAllocator blocks;
void reset_command_buffer(CommandBuffer &cmd);
void report_command_list_amd(FILE *file, uint32_t index);
void report_command_list(FILE *file, CommandBuffer &cmd, uint32_t top_marker, uint32_t bottom_marker);
};
}
@@ -0,0 +1,89 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "buffer.hpp"
#include "device.hpp"
namespace Vulkan
{
Buffer::Buffer(Device *device_, VkBuffer buffer_, const DeviceAllocation &alloc_, const BufferCreateInfo &info_,
VkDeviceAddress bda_)
: Cookie(device_)
, device(device_)
, buffer(buffer_)
, alloc(alloc_)
, info(info_)
, bda(bda_)
{
}
ExternalHandle Buffer::export_handle()
{
return alloc.export_handle(*device);
}
Buffer::~Buffer()
{
if (owns_buffer)
{
if (internal_sync)
{
device->destroy_buffer_nolock(buffer);
device->free_memory_nolock(alloc);
}
else
{
device->destroy_buffer(buffer);
device->free_memory(alloc);
}
}
}
void BufferDeleter::operator()(Buffer *buffer)
{
buffer->device->handle_pool.buffers.free(buffer);
}
BufferView::BufferView(Device *device_,
const CachedBufferView &view_,
const BufferViewCreateInfo &create_info_)
: Cookie(device_)
, device(device_)
, view(view_)
, info(create_info_)
{
}
BufferView::~BufferView()
{
if (internal_sync)
device->destroy_buffer_view_nolock(view);
else
device->destroy_buffer_view(view);
}
void BufferViewDeleter::operator()(BufferView *view)
{
view->device->handle_pool.buffer_views.free(view);
}
}
@@ -0,0 +1,183 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "cookie.hpp"
#include "vulkan_common.hpp"
#include "memory_allocator.hpp"
namespace Vulkan
{
class Device;
enum class BufferDomain
{
Device, // Device local. Probably not visible from CPU.
LinkedDeviceHost, // On desktop, directly mapped VRAM over PCI.
LinkedDeviceHostPreferDevice, // Prefer device local of host visible.
Host, // Host-only, needs to be synced to GPU. Might be device local as well on iGPUs.
CachedHost,
CachedCoherentHostPreferCoherent, // Aim for both cached and coherent, but prefer COHERENT
CachedCoherentHostPreferCached, // Aim for both cached and coherent, but prefer CACHED
UMACachedCoherentPreferDevice, // Aim for DEVICE | CACHED | COHERENT, but fallback to plain DEVICE if not supported.
DebugReadback // DEVICE_COHERENT + HOST_COHERENT. For AMD_buffer_marker and other breadcrumbs.
};
enum BufferMiscFlagBits
{
BUFFER_MISC_ZERO_INITIALIZE_BIT = 1 << 0,
BUFFER_MISC_EXTERNAL_MEMORY_BIT = 1 << 1
};
using BufferMiscFlags = uint32_t;
struct BufferCreateInfo
{
BufferDomain domain = BufferDomain::Device;
VkDeviceSize size = 0;
VkBufferUsageFlags2 usage = 0;
BufferMiscFlags misc = 0;
VkMemoryRequirements allocation_requirements = {};
ExternalHandle external;
void *pnext = nullptr;
};
class Buffer;
struct BufferDeleter
{
void operator()(Buffer *buffer);
};
class BufferView;
struct BufferViewDeleter
{
void operator()(BufferView *view);
};
class Buffer : public Util::IntrusivePtrEnabled<Buffer, BufferDeleter, HandleCounter>,
public Cookie, public InternalSyncEnabled
{
public:
friend struct BufferDeleter;
~Buffer();
VkBuffer get_buffer() const
{
return buffer;
}
const BufferCreateInfo &get_create_info() const
{
return info;
}
DeviceAllocation &get_allocation()
{
return alloc;
}
const DeviceAllocation &get_allocation() const
{
return alloc;
}
ExternalHandle export_handle();
VkDeviceAddress get_device_address() const
{
VK_ASSERT(bda);
return bda;
}
void disown_buffer()
{
owns_buffer = false;
}
private:
friend class Util::ObjectPool<Buffer>;
Buffer(Device *device, VkBuffer buffer, const DeviceAllocation &alloc, const BufferCreateInfo &info,
VkDeviceAddress bda);
Device *device;
VkBuffer buffer;
DeviceAllocation alloc;
BufferCreateInfo info;
VkDeviceAddress bda;
bool owns_buffer = true;
};
using BufferHandle = Util::IntrusivePtr<Buffer>;
struct BufferViewCreateInfo
{
const Buffer *buffer;
VkFormat format;
VkDeviceSize offset;
VkDeviceSize range;
};
class BufferView : public Util::IntrusivePtrEnabled<BufferView, BufferViewDeleter, HandleCounter>,
public Cookie, public InternalSyncEnabled
{
public:
friend struct BufferViewDeleter;
~BufferView();
VkBufferView get_view() const
{
VK_ASSERT(view.view);
return view.view;
}
const CachedDescriptorPayload &get_uniform_payload() const
{
VK_ASSERT(view.uniform.ptr && view.uniform.type == VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER);
return view.uniform;
}
const CachedDescriptorPayload &get_storage_payload() const
{
VK_ASSERT(view.storage.ptr && view.storage.type == VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER);
return view.storage;
}
const BufferViewCreateInfo &get_create_info()
{
return info;
}
const Buffer &get_buffer() const
{
return *info.buffer;
}
private:
friend class Util::ObjectPool<BufferView>;
BufferView(Device *device, const CachedBufferView &view, const BufferViewCreateInfo &info);
Device *device;
CachedBufferView view;
BufferViewCreateInfo info;
};
using BufferViewHandle = Util::IntrusivePtr<BufferView>;
}
@@ -0,0 +1,128 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#define NOMINMAX
#include "buffer_pool.hpp"
#include "device.hpp"
#include <utility>
namespace Vulkan
{
void BufferPool::init(Device *device_, VkDeviceSize block_size_,
VkDeviceSize alignment_, VkBufferUsageFlags usage_)
{
device = device_;
block_size = block_size_;
alignment = alignment_;
usage = usage_;
}
void BufferPool::set_max_retained_blocks(size_t max_blocks)
{
max_retained_blocks = max_blocks;
}
BufferBlock::~BufferBlock()
{
}
void BufferPool::reset()
{
blocks.clear();
}
BufferBlock BufferPool::allocate_block(VkDeviceSize size)
{
BufferDomain ideal_domain = ((usage & VK_BUFFER_USAGE_TRANSFER_SRC_BIT) != 0) ?
BufferDomain::Host : BufferDomain::LinkedDeviceHost;
BufferBlock block;
BufferCreateInfo info;
info.domain = ideal_domain;
info.size = size;
info.usage = usage;
block.buffer = device->create_buffer(info, nullptr);
device->set_name(*block.buffer, "chain-allocated-block");
block.buffer->set_internal_sync_object();
// Try to map it, will fail unless the memory is host visible.
block.mapped = static_cast<uint8_t *>(device->map_host_buffer(*block.buffer, MEMORY_ACCESS_WRITE_BIT));
block.offset = 0;
block.alignment = alignment;
block.size = size;
return block;
}
BufferBlock BufferPool::request_block(VkDeviceSize minimum_size)
{
if ((minimum_size > block_size) || blocks.empty())
{
return allocate_block(std::max(block_size, minimum_size));
}
else
{
auto back = std::move(blocks.back());
blocks.pop_back();
back.mapped = static_cast<uint8_t *>(device->map_host_buffer(*back.buffer, MEMORY_ACCESS_WRITE_BIT));
back.offset = 0;
return back;
}
}
void BufferPool::recycle_block(BufferBlock &block)
{
VK_ASSERT(block.size == block_size);
if (blocks.size() < max_retained_blocks)
blocks.push_back(std::move(block));
else
block = {};
}
BufferPool::~BufferPool()
{
VK_ASSERT(blocks.empty());
}
BufferBlockAllocation BufferBlock::allocate(VkDeviceSize allocate_size)
{
auto aligned_offset = (offset + alignment - 1) & ~(alignment - 1);
if (aligned_offset + allocate_size <= size)
{
auto *ret = mapped + aligned_offset;
offset = aligned_offset + allocate_size;
return { ret, buffer, aligned_offset, allocate_size };
}
else
return { nullptr, {}, 0, 0 };
}
void BufferBlock::unmap(Device &device)
{
device.unmap_host_buffer(*buffer, MEMORY_ACCESS_WRITE_BIT);
mapped = nullptr;
}
}
@@ -0,0 +1,91 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_headers.hpp"
#include "intrusive.hpp"
#include <vector>
#include <algorithm>
namespace Vulkan
{
class Device;
class Buffer;
struct BufferBlockAllocation
{
uint8_t *host;
Util::IntrusivePtr<Buffer> buffer;
VkDeviceSize offset;
VkDeviceSize padded_size;
};
class BufferBlock
{
public:
~BufferBlock();
BufferBlockAllocation allocate(VkDeviceSize allocate_size);
bool is_mapped() const { return mapped != nullptr; }
const Buffer &get_buffer() const { return *buffer; }
void unmap(Device &device);
VkDeviceSize get_offset() const { return offset; }
VkDeviceSize get_size() const { return size; }
private:
friend class BufferPool;
Util::IntrusivePtr<Buffer> buffer;
VkDeviceSize offset = 0;
VkDeviceSize alignment = 0;
VkDeviceSize size = 0;
uint8_t *mapped = nullptr;
};
class BufferPool
{
public:
~BufferPool();
void init(Device *device, VkDeviceSize block_size, VkDeviceSize alignment, VkBufferUsageFlags usage);
void reset();
void set_max_retained_blocks(size_t max_blocks);
VkDeviceSize get_block_size() const
{
return block_size;
}
BufferBlock request_block(VkDeviceSize minimum_size);
void recycle_block(BufferBlock &block);
private:
Device *device = nullptr;
VkDeviceSize block_size = 0;
VkDeviceSize alignment = 0;
VkBufferUsageFlags usage = 0;
size_t max_retained_blocks = 0;
std::vector<BufferBlock> blocks;
BufferBlock allocate_block(VkDeviceSize size);
};
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,180 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "command_pool.hpp"
#include "device.hpp"
namespace Vulkan
{
CommandPool::CommandPool(Device *device_, uint32_t queue_family_index)
: device(device_), table(&device_->get_device_table())
{
VkCommandPoolCreateInfo info = { VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO };
info.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
info.queueFamilyIndex = queue_family_index;
if (queue_family_index != VK_QUEUE_FAMILY_IGNORED)
table->vkCreateCommandPool(device->get_device(), &info, nullptr, &pool);
}
CommandPool::CommandPool(CommandPool &&other) noexcept
{
*this = std::move(other);
}
CommandPool &CommandPool::operator=(CommandPool &&other) noexcept
{
if (this != &other)
{
device = other.device;
table = other.table;
if (!buffers.empty())
table->vkFreeCommandBuffers(device->get_device(), pool, buffers.size(), buffers.data());
if (pool != VK_NULL_HANDLE)
table->vkDestroyCommandPool(device->get_device(), pool, nullptr);
pool = VK_NULL_HANDLE;
buffers.clear();
std::swap(pool, other.pool);
std::swap(buffers, other.buffers);
index = other.index;
other.index = 0;
#ifdef VULKAN_DEBUG
in_flight.clear();
std::swap(in_flight, other.in_flight);
#endif
}
return *this;
}
CommandPool::~CommandPool()
{
if (!buffers.empty())
table->vkFreeCommandBuffers(device->get_device(), pool, buffers.size(), buffers.data());
if (!secondary_buffers.empty())
table->vkFreeCommandBuffers(device->get_device(), pool, secondary_buffers.size(), secondary_buffers.data());
if (pool != VK_NULL_HANDLE)
table->vkDestroyCommandPool(device->get_device(), pool, nullptr);
}
void CommandPool::signal_submitted(VkCommandBuffer cmd)
{
#ifdef VULKAN_DEBUG
VK_ASSERT(in_flight.find(cmd) != end(in_flight));
in_flight.erase(cmd);
#else
(void)cmd;
#endif
}
VkCommandBuffer CommandPool::request_secondary_command_buffer()
{
VK_ASSERT(pool != VK_NULL_HANDLE);
if (secondary_index < secondary_buffers.size())
{
auto ret = secondary_buffers[secondary_index++];
#ifdef VULKAN_DEBUG
VK_ASSERT(in_flight.find(ret) == end(in_flight));
in_flight.insert(ret);
#endif
return ret;
}
else
{
VkCommandBuffer cmd;
VkCommandBufferAllocateInfo info = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
info.commandPool = pool;
info.level = VK_COMMAND_BUFFER_LEVEL_SECONDARY;
info.commandBufferCount = 1;
table->vkAllocateCommandBuffers(device->get_device(), &info, &cmd);
#ifdef VULKAN_DEBUG
VK_ASSERT(in_flight.find(cmd) == end(in_flight));
in_flight.insert(cmd);
#endif
secondary_buffers.push_back(cmd);
secondary_index++;
return cmd;
}
}
VkCommandBuffer CommandPool::request_command_buffer()
{
VK_ASSERT(pool != VK_NULL_HANDLE);
if (index < buffers.size())
{
auto ret = buffers[index++];
#ifdef VULKAN_DEBUG
VK_ASSERT(in_flight.find(ret) == end(in_flight));
in_flight.insert(ret);
#endif
return ret;
}
else
{
VkCommandBuffer cmd;
VkCommandBufferAllocateInfo info = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
info.commandPool = pool;
info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
info.commandBufferCount = 1;
table->vkAllocateCommandBuffers(device->get_device(), &info, &cmd);
#ifdef VULKAN_DEBUG
VK_ASSERT(in_flight.find(cmd) == end(in_flight));
in_flight.insert(cmd);
#endif
buffers.push_back(cmd);
index++;
return cmd;
}
}
void CommandPool::begin()
{
if (pool == VK_NULL_HANDLE)
return;
#ifdef VULKAN_DEBUG
VK_ASSERT(in_flight.empty());
#endif
if (index > 0 || secondary_index > 0)
table->vkResetCommandPool(device->get_device(), pool, 0);
index = 0;
secondary_index = 0;
}
void CommandPool::trim()
{
if (pool == VK_NULL_HANDLE)
return;
table->vkResetCommandPool(device->get_device(), pool, VK_COMMAND_POOL_RESET_RELEASE_RESOURCES_BIT);
if (!buffers.empty())
table->vkFreeCommandBuffers(device->get_device(), pool, buffers.size(), buffers.data());
if (!secondary_buffers.empty())
table->vkFreeCommandBuffers(device->get_device(), pool, secondary_buffers.size(), secondary_buffers.data());
buffers.clear();
secondary_buffers.clear();
table->vkTrimCommandPool(device->get_device(), pool, 0);
}
}
@@ -0,0 +1,61 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_headers.hpp"
#include <unordered_set>
#include <vector>
namespace Vulkan
{
class Device;
class CommandPool
{
public:
CommandPool(Device *device, uint32_t queue_family_index);
~CommandPool();
CommandPool(CommandPool &&) noexcept;
CommandPool &operator=(CommandPool &&) noexcept;
CommandPool(const CommandPool &) = delete;
void operator=(const CommandPool &) = delete;
void begin();
void trim();
VkCommandBuffer request_command_buffer();
VkCommandBuffer request_secondary_command_buffer();
void signal_submitted(VkCommandBuffer cmd);
private:
Device *device;
const VolkDeviceTable *table;
VkCommandPool pool = VK_NULL_HANDLE;
std::vector<VkCommandBuffer> buffers;
std::vector<VkCommandBuffer> secondary_buffers;
#ifdef VULKAN_DEBUG
std::unordered_set<VkCommandBuffer> in_flight;
#endif
unsigned index = 0;
unsigned secondary_index = 0;
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,458 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_headers.hpp"
#include "logging.hpp"
#include "vulkan_common.hpp"
#include <memory>
#include <functional>
#ifdef GRANITE_VULKAN_FOSSILIZE
#include "cli/fossilize_feature_filter.hpp"
#endif
namespace Util
{
class TimelineTraceFile;
}
namespace Granite
{
class Filesystem;
class ThreadGroup;
class AssetManager;
}
namespace Vulkan
{
struct DeviceFeatures
{
bool supports_debug_utils = false;
bool supports_external_memory_host = false;
bool supports_surface_capabilities2 = false;
bool supports_full_screen_exclusive = false;
bool supports_conservative_rasterization = false;
bool supports_calibrated_timestamps = false;
bool supports_memory_budget = false;
bool supports_video_queue = false;
bool supports_driver_properties = false;
bool supports_video_decode_queue = false;
bool supports_video_decode_h264 = false;
bool supports_video_decode_h265 = false;
bool supports_video_decode_av1 = false;
bool supports_astc_decode_mode = false;
bool supports_image_format_list = false;
bool supports_format_feature_flags2 = false;
bool supports_video_encode_queue = false;
bool supports_video_encode_h264 = false;
bool supports_video_encode_h265 = false;
bool supports_video_encode_av1 = false;
bool supports_external = false;
bool supports_tooling_info = false;
bool supports_hdr_metadata = false;
bool supports_swapchain_colorspace = false;
bool supports_surface_maintenance1 = false;
bool supports_store_op_none = false;
bool supports_low_latency2_nv = false;
bool supports_drm_modifiers = false;
bool supports_descriptor_buffer = false;
bool supports_amd_buffer_marker = false;
bool supports_nv_checkpoints = false;
bool supports_post_mortem = false;
bool supports_descriptor_buffer_or_heap = false;
uint32_t resource_heap_offset_alignment = 0;
uint32_t resource_heap_resource_desc_size = 0;
uint32_t resource_heap_resource_desc_size_log2 = 0;
VkPhysicalDeviceFeatures enabled_features = {};
VkPhysicalDeviceVulkan11Features vk11_features = {};
VkPhysicalDeviceVulkan12Features vk12_features = {};
VkPhysicalDeviceVulkan13Features vk13_features = {};
VkPhysicalDeviceVulkan14Features vk14_features = {};
VkPhysicalDeviceVulkan11Properties vk11_props = {};
VkPhysicalDeviceVulkan12Properties vk12_props = {};
VkPhysicalDeviceVulkan13Properties vk13_props = {};
VkPhysicalDeviceVulkan14Properties vk14_props = {};
// KHR
VkPhysicalDeviceComputeShaderDerivativesFeaturesKHR compute_shader_derivative_features = {};
VkPhysicalDevicePerformanceQueryFeaturesKHR performance_query_features = {};
VkPhysicalDevicePresentIdFeaturesKHR present_id_features = {};
VkPhysicalDevicePresentId2FeaturesKHR present_id2_features = {};
VkPhysicalDevicePresentWaitFeaturesKHR present_wait_features = {};
VkPhysicalDevicePresentWait2FeaturesKHR present_wait2_features = {};
VkPhysicalDeviceFragmentShaderBarycentricFeaturesKHR barycentric_features = {};
VkPhysicalDeviceVideoMaintenance1FeaturesKHR video_maintenance1_features = {};
VkPhysicalDevicePipelineBinaryFeaturesKHR pipeline_binary_features = {};
VkPhysicalDevicePipelineBinaryPropertiesKHR pipeline_binary_properties = {};
VkDevicePipelineBinaryInternalCacheControlKHR pipeline_binary_internal_cache_control = {};
VkPhysicalDeviceMaintenance5FeaturesKHR maintenance5_features = {};
VkPhysicalDeviceVideoEncodeAV1FeaturesKHR av1_features = {};
VkPhysicalDeviceAccelerationStructureFeaturesKHR rtas_features = {};
VkPhysicalDeviceAccelerationStructurePropertiesKHR rtas_properties = {};
VkPhysicalDeviceRayQueryFeaturesKHR ray_query_features = {};
VkPhysicalDeviceShaderUntypedPointersFeaturesKHR untyped_pointers_features = {};
VkPhysicalDeviceFaultFeaturesKHR fault_features_khr = {};
VkPhysicalDeviceCooperativeMatrixFeaturesKHR cooperative_matrix_features = {};
// EXT
VkPhysicalDeviceExternalMemoryHostPropertiesEXT host_memory_properties = {};
VkPhysicalDeviceConservativeRasterizationPropertiesEXT conservative_rasterization_properties = {};
VkPhysicalDeviceMemoryPriorityFeaturesEXT memory_priority_features = {};
VkPhysicalDeviceASTCDecodeFeaturesEXT astc_decode_features = {};
VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR swapchain_maintenance1_features = {};
VkPhysicalDevicePageableDeviceLocalMemoryFeaturesEXT pageable_device_local_memory_features = {};
VkPhysicalDeviceMeshShaderFeaturesEXT mesh_shader_features = {};
VkPhysicalDeviceMeshShaderPropertiesEXT mesh_shader_properties = {};
VkPhysicalDeviceIndexTypeUint8FeaturesEXT index_type_uint8_features = {};
VkPhysicalDeviceRGBA10X6FormatsFeaturesEXT rgba10x6_formats_features = {};
VkPhysicalDeviceImageCompressionControlFeaturesEXT image_compression_control_features = {};
VkPhysicalDeviceImageCompressionControlSwapchainFeaturesEXT image_compression_control_swapchain_features = {};
VkPhysicalDeviceDeviceGeneratedCommandsFeaturesEXT device_generated_commands_features = {};
VkPhysicalDeviceDeviceGeneratedCommandsPropertiesEXT device_generated_commands_properties = {};
VkPhysicalDeviceRobustness2FeaturesEXT robustness2_features = {};
VkPhysicalDeviceDescriptorBufferFeaturesEXT descriptor_buffer_features = {};
VkPhysicalDeviceDescriptorBufferPropertiesEXT descriptor_buffer_properties = {};
VkPhysicalDevicePresentTimingFeaturesEXT present_timing_features = {};
VkPhysicalDeviceDescriptorHeapFeaturesEXT descriptor_heap_features = {};
VkPhysicalDeviceDescriptorHeapPropertiesEXT descriptor_heap_properties = {};
VkPhysicalDeviceFaultFeaturesEXT fault_features_ext = {};
// Vendor
VkPhysicalDeviceDescriptorPoolOverallocationFeaturesNV descriptor_pool_overallocation_features = {};
VkPhysicalDeviceAntiLagFeaturesAMD anti_lag_features = {};
VkPhysicalDeviceCoherentMemoryFeaturesAMD coherent_memory_features = {};
VkPhysicalDeviceShaderMixedFloatDotProductFeaturesVALVE shader_mixed_float_dot_product_features = {};
// Fallback feature structs (Vulkan 1.1)
VkPhysicalDeviceHostQueryResetFeatures host_query_reset_features = {};
VkPhysicalDevice16BitStorageFeaturesKHR storage_16bit_features = {};
// Fallback feature structs (Vulkan 1.2)
VkPhysicalDeviceFloat16Int8FeaturesKHR float16_int8_features = {};
VkPhysicalDevice8BitStorageFeaturesKHR storage_8bit_features = {};
// Fallback feature structs (Vulkan 1.3)
VkPhysicalDeviceSubgroupSizeControlFeatures subgroup_size_control_features = {};
VkPhysicalDeviceSynchronization2Features sync2_features = {};
VkDriverId driver_id = {};
// References Vulkan::Context.
const VkPhysicalDeviceFeatures2 *pdf2 = nullptr;
const char * const * instance_extensions = nullptr;
uint32_t num_instance_extensions = 0;
const char * const * device_extensions = nullptr;
uint32_t num_device_extensions = 0;
uint32_t instance_api_core_version = VK_API_VERSION_1_1;
uint32_t device_api_core_version = VK_API_VERSION_1_1;
};
enum VendorID
{
VENDOR_ID_AMD = 0x1002,
VENDOR_ID_NVIDIA = 0x10de,
VENDOR_ID_INTEL = 0x8086,
VENDOR_ID_ARM = 0x13b5,
VENDOR_ID_QCOM = 0x5143
};
enum ContextCreationFlagBits
{
CONTEXT_CREATION_ENABLE_ADVANCED_WSI_BIT = 1 << 0,
CONTEXT_CREATION_ENABLE_VIDEO_DECODE_BIT = 1 << 1,
CONTEXT_CREATION_ENABLE_VIDEO_ENCODE_BIT = 1 << 2,
CONTEXT_CREATION_ENABLE_VIDEO_H264_BIT = 1 << 3,
CONTEXT_CREATION_ENABLE_VIDEO_H265_BIT = 1 << 4,
CONTEXT_CREATION_ENABLE_PIPELINE_BINARY_BIT = 1 << 5,
CONTEXT_CREATION_ENABLE_PUSH_DESCRIPTOR_BIT = 1 << 6,
CONTEXT_CREATION_ENABLE_ROBUSTNESS_2_BIT = 1 << 7,
CONTEXT_CREATION_ENABLE_VIDEO_AV1_BIT = 1 << 8,
CONTEXT_CREATION_ENABLE_DESCRIPTOR_BUFFER_BIT = 1 << 9,
CONTEXT_CREATION_ENABLE_DESCRIPTOR_HEAP_BIT = 1 << 10,
CONTEXT_CREATION_ENABLE_POST_MORTEM_BIT = 1 << 11,
CONTEXT_CREATION_ENABLE_VIDEO_FEATURE_ONLY_BIT = 1 << 12,
};
using ContextCreationFlags = uint32_t;
struct QueueInfo
{
QueueInfo();
VkQueue queues[QUEUE_INDEX_COUNT] = {};
uint32_t family_indices[QUEUE_INDEX_COUNT];
uint32_t counts[QUEUE_INDEX_COUNT] = {};
uint32_t timestamp_valid_bits = 0;
};
struct InstanceFactory
{
virtual ~InstanceFactory() = default;
virtual VkInstance create_instance(const VkInstanceCreateInfo *info) = 0;
// Lifetime of any data in create info must remain as long as Context is alive.
virtual const VkInstanceCreateInfo *get_existing_create_info();
virtual bool factory_owns_created_instance();
};
struct DeviceFactory
{
virtual ~DeviceFactory() = default;
virtual VkDevice create_device(VkPhysicalDevice gpu, const VkDeviceCreateInfo *info) = 0;
// Lifetime of any data in create info must remain as long as Context is alive.
// If pNext is not NULL, the first link in the pNext chain must be VkPhysicalDeviceFeatures2.
virtual const VkDeviceCreateInfo *get_existing_create_info();
virtual bool factory_owns_created_device();
virtual VkQueue get_queue(uint32_t family_index, uint32_t index);
};
class CopiedApplicationInfo
{
public:
CopiedApplicationInfo();
const VkApplicationInfo &get_application_info() const;
void copy_assign(const VkApplicationInfo *info);
private:
std::string application;
std::string engine;
VkApplicationInfo app = {
VK_STRUCTURE_TYPE_APPLICATION_INFO, nullptr, "Granite", 0, "Granite", 0, VK_API_VERSION_1_1,
};
void set_default_app();
};
class Context
: public Util::IntrusivePtrEnabled<Context, std::default_delete<Context>, HandleCounter>
#ifdef GRANITE_VULKAN_FOSSILIZE
, public Fossilize::DeviceQueryInterface
#endif
{
public:
// If these interface are set, factory->create() calls are used instead of global vkCreateInstance and vkCreateDevice.
// For deeper API interop scenarios.
void set_instance_factory(InstanceFactory *factory);
void set_device_factory(DeviceFactory *factory);
// Only takes effect if profiles are enabled in build. (GRANITE_VULKAN_PROFILES)
// If profile is non-null, forces a specific profile.
// If not supported, initialization fails.
// If not set, ignore profiles.
// If strict is false, the profile should be seen as a baseline and Granite will augment features on top.
// If true, the profile is a strict limit for device functionality. For validation purposes.
void set_required_profile(const char *profile, bool strict);
// Call before initializing instances. app_info may be freed after returning.
// API_VERSION must be at least 1.1.
// By default, a Vulkan 1.1 to 1.4 instance is created depending on support.
void set_application_info(const VkApplicationInfo *app_info);
// Recommended interface.
// InstanceFactory can be used to override enabled instance layers and extensions.
// For simple WSI use, it is enough to just enable VK_KHR_surface and the platform.
bool init_instance(const char * const *instance_ext, uint32_t instance_ext_count,
ContextCreationFlags flags = 0);
// DeviceFactory can be used to override enabled device extensions.
// For simple WSI use, it is enough to just enable VK_KHR_swapchain.
bool init_device(VkPhysicalDevice gpu, VkSurfaceKHR surface_compat,
const char * const *device_ext, uint32_t device_ext_count,
ContextCreationFlags flags = 0);
// Simplified initialization which calls init_instance and init_device in succession with NULL GPU and surface.
// Provided for compat with older code.
bool init_instance_and_device(const char * const *instance_ext, uint32_t instance_ext_count,
const char * const *device_ext, uint32_t device_ext_count,
ContextCreationFlags flags = 0);
// Deprecated. For libretro Vulkan context negotiation v1.
// Use InstanceFactory and DeviceFactory for more advanced scenarios in v2.
bool init_device_from_instance(VkInstance instance, VkPhysicalDevice gpu, VkSurfaceKHR surface,
const char **required_device_extensions,
unsigned num_required_device_extensions,
const VkPhysicalDeviceFeatures *required_features,
ContextCreationFlags flags = 0);
Context();
Context(const Context &) = delete;
void operator=(const Context &) = delete;
static bool init_loader(PFN_vkGetInstanceProcAddr addr, bool force_reload = false);
static PFN_vkGetInstanceProcAddr get_instance_proc_addr();
~Context();
VkInstance get_instance() const
{
return instance;
}
VkPhysicalDevice get_gpu() const
{
return gpu;
}
VkDevice get_device() const
{
return device;
}
const QueueInfo &get_queue_info() const
{
return queue_info;
}
const VkPhysicalDeviceProperties &get_gpu_props() const
{
return gpu_props;
}
const VkPhysicalDeviceMemoryProperties &get_mem_props() const
{
return mem_props;
}
void release_instance()
{
owned_instance = false;
}
void release_device()
{
owned_device = false;
}
const DeviceFeatures &get_enabled_device_features() const
{
return ext;
}
const VkApplicationInfo &get_application_info() const;
void notify_validation_error(const char *msg);
void set_notification_callback(std::function<void (const char *)> func);
void set_num_thread_indices(unsigned indices)
{
num_thread_indices = indices;
}
unsigned get_num_thread_indices() const
{
return num_thread_indices;
}
const VolkDeviceTable &get_device_table() const
{
return device_table;
}
struct SystemHandles
{
Util::TimelineTraceFile *timeline_trace_file = nullptr;
Granite::Filesystem *filesystem = nullptr;
Granite::ThreadGroup *thread_group = nullptr;
Granite::AssetManager *asset_manager = nullptr;
};
void set_system_handles(const SystemHandles &handles_)
{
handles = handles_;
}
const SystemHandles &get_system_handles() const
{
return handles;
}
#ifdef GRANITE_VULKAN_FOSSILIZE
const Fossilize::FeatureFilter &get_feature_filter() const
{
return feature_filter;
}
#endif
const VkPhysicalDeviceFeatures2 &get_physical_device_features() const
{
return pdf2;
}
private:
InstanceFactory *instance_factory = nullptr;
DeviceFactory *device_factory = nullptr;
VkDevice device = VK_NULL_HANDLE;
VkInstance instance = VK_NULL_HANDLE;
VkPhysicalDevice gpu = VK_NULL_HANDLE;
VolkDeviceTable device_table = {};
SystemHandles handles;
VkPhysicalDeviceProperties gpu_props = {};
VkPhysicalDeviceMemoryProperties mem_props = {};
CopiedApplicationInfo user_application_info;
QueueInfo queue_info;
unsigned num_thread_indices = 1;
bool create_instance(const char * const *instance_ext, uint32_t instance_ext_count, ContextCreationFlags flags);
bool create_device(VkPhysicalDevice gpu, VkSurfaceKHR surface,
const char * const *required_device_extensions, uint32_t num_required_device_extensions,
const VkPhysicalDeviceFeatures *required_features, ContextCreationFlags flags);
bool owned_instance = false;
bool owned_device = false;
DeviceFeatures ext;
VkPhysicalDeviceFeatures2 pdf2;
std::vector<const char *> enabled_device_extensions;
std::vector<const char *> enabled_instance_extensions;
std::string required_profile;
bool required_profile_strict = false;
#ifdef VULKAN_DEBUG
VkDebugUtilsMessengerEXT debug_messenger = VK_NULL_HANDLE;
bool force_no_validation = false;
#endif
std::function<void (const char *)> message_callback;
void destroy_instance();
void destroy_device();
bool physical_device_supports_surface_and_profile(VkPhysicalDevice candidate_gpu, VkSurfaceKHR surface) const;
#ifdef GRANITE_VULKAN_FOSSILIZE
Fossilize::FeatureFilter feature_filter;
bool format_is_supported(VkFormat format, VkFormatFeatureFlags features) override;
bool descriptor_set_layout_is_supported(const VkDescriptorSetLayoutCreateInfo *set_layout) override;
void physical_device_feature_query(VkPhysicalDeviceFeatures2 *pdf2) override;
#endif
bool init_profile();
VkResult create_instance_from_profile(const VkInstanceCreateInfo &info, VkInstance *pInstance);
VkResult create_device_from_profile(const VkDeviceCreateInfo &info, VkDevice *pDevice);
VkApplicationInfo get_promoted_application_info() const;
};
using ContextHandle = Util::IntrusivePtr<Context>;
}
@@ -0,0 +1,32 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "cookie.hpp"
#include "device.hpp"
namespace Vulkan
{
Cookie::Cookie(Device *device)
: cookie(device->allocate_cookie())
{
}
}
@@ -0,0 +1,61 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <stdint.h>
#include "hash.hpp"
#include "intrusive_hash_map.hpp"
namespace Vulkan
{
class Device;
class Cookie
{
public:
Cookie(Device *device);
uint64_t get_cookie() const
{
return cookie;
}
private:
uint64_t cookie;
};
template <typename T>
using HashedObject = Util::IntrusiveHashMapEnabled<T>;
class InternalSyncEnabled
{
public:
void set_internal_sync_object()
{
internal_sync = true;
}
protected:
bool internal_sync = false;
};
}
@@ -0,0 +1,672 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#define NOMINMAX
#include "descriptor_set.hpp"
#include "device.hpp"
#include <vector>
using namespace Util;
namespace Vulkan
{
DescriptorSetAllocator::DescriptorSetAllocator(Hash hash, Device *device_, const DescriptorSetLayout &layout,
const uint32_t *stages_for_binds,
const ImmutableSampler * const *immutable_samplers)
: IntrusiveHashMapEnabled<DescriptorSetAllocator>(hash)
, device(device_)
, table(device_->get_device_table())
{
bindless = layout.meta[0].array_size == DescriptorSetLayout::UNSIZED_ARRAY;
if (!bindless)
{
unsigned count = device_->num_thread_indices * device_->per_frame.size();
per_thread_and_frame.resize(count);
}
if (bindless && !device->get_device_features().vk12_features.descriptorIndexing)
{
LOGE("Cannot support descriptor indexing on this device.\n");
return;
}
VkDescriptorSetLayoutCreateInfo info = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO };
VkDescriptorSetLayoutBindingFlagsCreateInfoEXT flags = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO_EXT };
VkSampler vk_immutable_samplers[VULKAN_NUM_BINDINGS] = {};
std::vector<VkDescriptorSetLayoutBinding> bindings;
VkDescriptorBindingFlagsEXT binding_flags = 0;
if (bindless)
{
if (!device->ext.supports_descriptor_buffer)
info.flags |= VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT;
info.pNext = &flags;
flags.bindingCount = 1;
flags.pBindingFlags = &binding_flags;
binding_flags = VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT;
if (!device->ext.supports_descriptor_buffer)
{
// These flags are implied when using descriptor buffer.
binding_flags |= VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT |
VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT;
}
}
if (device->ext.supports_descriptor_buffer)
info.flags |= VK_DESCRIPTOR_SET_LAYOUT_CREATE_DESCRIPTOR_BUFFER_BIT_EXT;
for (unsigned i = 0; i < VULKAN_NUM_BINDINGS; i++)
{
auto stages = stages_for_binds[i];
if (stages == 0)
continue;
unsigned array_size = layout.meta[i].array_size;
unsigned pool_array_size;
if (array_size == DescriptorSetLayout::UNSIZED_ARRAY)
{
array_size = VULKAN_NUM_BINDINGS_BINDLESS_VARYING;
pool_array_size = array_size;
}
else
pool_array_size = array_size * VULKAN_NUM_SETS_PER_POOL;
unsigned types = 0;
if (layout.sampled_image_mask & (1u << i))
{
if ((layout.immutable_sampler_mask & (1u << i)) && immutable_samplers && immutable_samplers[i])
vk_immutable_samplers[i] = immutable_samplers[i]->get_sampler().get_sampler();
bindings.push_back({ i, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, array_size, stages,
vk_immutable_samplers[i] != VK_NULL_HANDLE ? &vk_immutable_samplers[i] : nullptr });
pool_size.push_back({ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pool_array_size });
types++;
}
if (layout.sampled_texel_buffer_mask & (1u << i))
{
bindings.push_back({ i, VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, array_size, stages, nullptr });
pool_size.push_back({ VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, pool_array_size });
types++;
}
if (layout.storage_texel_buffer_mask & (1u << i))
{
bindings.push_back({ i, VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, array_size, stages, nullptr });
pool_size.push_back({ VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, pool_array_size });
types++;
}
if (layout.storage_image_mask & (1u << i))
{
bindings.push_back({ i, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, array_size, stages, nullptr });
pool_size.push_back({ VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, pool_array_size });
types++;
}
if (layout.uniform_buffer_mask & (1u << i))
{
auto type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
bindings.push_back({ i, type, array_size, stages, nullptr });
pool_size.push_back({ type, pool_array_size });
types++;
}
if (layout.storage_buffer_mask & (1u << i))
{
bindings.push_back({ i, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, array_size, stages, nullptr });
pool_size.push_back({ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, pool_array_size });
types++;
}
if (layout.rtas_mask & (1u << i))
{
bindings.push_back({ i, VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR, array_size, stages, nullptr });
pool_size.push_back({ VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR, pool_array_size });
types++;
}
if (layout.input_attachment_mask & (1u << i))
{
bindings.push_back({ i, VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, array_size, stages, nullptr });
pool_size.push_back({ VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, pool_array_size });
types++;
}
if (layout.separate_image_mask & (1u << i))
{
bindings.push_back({ i, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, array_size, stages, nullptr });
pool_size.push_back({ VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, pool_array_size });
types++;
}
if (layout.sampler_mask & (1u << i))
{
if ((layout.immutable_sampler_mask & (1u << i)) && immutable_samplers && immutable_samplers[i])
{
if (!device->get_device_features().supports_descriptor_buffer)
vk_immutable_samplers[i] = immutable_samplers[i]->get_sampler().get_sampler();
else
LOGE("Cannot use immutable samplers with descriptor buffer. Ignoring.\n");
}
bindings.push_back({ i, VK_DESCRIPTOR_TYPE_SAMPLER, array_size, stages,
vk_immutable_samplers[i] != VK_NULL_HANDLE ? &vk_immutable_samplers[i] : nullptr });
pool_size.push_back({ VK_DESCRIPTOR_TYPE_SAMPLER, pool_array_size });
types++;
}
(void)types;
VK_ASSERT(types <= 1 && "Descriptor set aliasing!");
}
if (!bindings.empty())
{
info.bindingCount = bindings.size();
info.pBindings = bindings.data();
if (bindless && bindings.size() != 1)
{
LOGE("Using bindless but have bindingCount != 1.\n");
return;
}
}
bool heap = device->get_device_features().descriptor_heap_features.descriptorHeap == VK_TRUE;
if (!heap)
{
#ifdef VULKAN_DEBUG
LOGI("Creating descriptor set layout.\n");
#endif
if (table.vkCreateDescriptorSetLayout(device->get_device(), &info, nullptr, &set_layout_pool) != VK_SUCCESS)
LOGE("Failed to create descriptor set layout.");
}
if (device->ext.supports_descriptor_buffer && !heap)
{
// Query the memory layout.
table.vkGetDescriptorSetLayoutSizeEXT(device->get_device(), set_layout_pool, &desc_set_size);
if (bindless)
{
table.vkGetDescriptorSetLayoutBindingOffsetEXT(
device->get_device(), set_layout_pool, 0, &desc_set_variable_offset);
}
else
{
for (auto &bind : bindings)
{
VkDeviceSize offset = 0;
VkDeviceSize stride = device->managers.descriptor_buffer.get_descriptor_size_for_type(bind.descriptorType);
table.vkGetDescriptorSetLayoutBindingOffsetEXT(
device->get_device(), set_layout_pool, bind.binding, &offset);
for (uint32_t i = 0; i < bind.descriptorCount; i++)
desc_offsets[bind.binding + i] = offset + i * stride;
}
}
}
#ifdef GRANITE_VULKAN_FOSSILIZE
if (device->ext.supports_descriptor_buffer && !heap)
{
// Normalize the recorded flags.
if (bindless)
{
info.flags |= VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT;
binding_flags |= VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT |
VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT;
}
info.flags &= ~VK_DESCRIPTOR_SET_LAYOUT_CREATE_DESCRIPTOR_BUFFER_BIT_EXT;
}
if (set_layout_pool)
device->register_descriptor_set_layout(set_layout_pool, get_hash(), info);
#endif
// Push descriptors is not used with descriptor buffer.
if (!bindless && device->get_device_features().vk14_features.pushDescriptor &&
!heap && !device->get_device_features().descriptor_buffer_features.descriptorBuffer)
{
info.flags |= VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT;
if (table.vkCreateDescriptorSetLayout(device->get_device(), &info, nullptr, &set_layout_push) != VK_SUCCESS)
LOGE("Failed to create descriptor set layout.");
#ifdef GRANITE_VULKAN_FOSSILIZE
if (set_layout_push)
device->register_descriptor_set_layout(set_layout_push, get_hash(), info);
#endif
}
}
void DescriptorSetAllocator::reset_bindless_pool(VkDescriptorPool pool)
{
table.vkResetDescriptorPool(device->get_device(), pool, 0);
}
BindlessDescriptorSet DescriptorSetAllocator::allocate_bindless_set(VkDescriptorPool pool, unsigned num_descriptors)
{
if (!pool || !bindless)
return {};
VkDescriptorSetAllocateInfo info = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO };
info.descriptorPool = pool;
info.descriptorSetCount = 1;
info.pSetLayouts = &set_layout_pool;
VkDescriptorSetVariableDescriptorCountAllocateInfoEXT count_info =
{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO_EXT };
uint32_t num_desc = num_descriptors;
count_info.descriptorSetCount = 1;
count_info.pDescriptorCounts = &num_desc;
info.pNext = &count_info;
BindlessDescriptorSet desc_set;
if (table.vkAllocateDescriptorSets(device->get_device(), &info, &desc_set.handle.set) != VK_SUCCESS)
return {};
desc_set.valid = true;
return desc_set;
}
DescriptorBufferAllocation DescriptorSetAllocator::allocate_bindless_buffer(unsigned num_sets, unsigned num_descriptors)
{
if (!bindless)
return {};
VkDeviceSize size = get_variable_offset() * num_sets +
device->managers.descriptor_buffer.get_descriptor_size_for_type(pool_size[0].type) *
num_descriptors;
size += (std::max<uint32_t>(num_sets, 1u) - 1u) *
std::max<VkDeviceSize>(
device->get_device_features().resource_heap_offset_alignment,
device->get_device_features().resource_heap_resource_desc_size);
return device->managers.descriptor_buffer.allocate(size);
}
VkDeviceSize DescriptorSetAllocator::get_variable_size(unsigned count) const
{
return get_variable_offset() +
device->managers.descriptor_buffer.get_descriptor_size_for_type(pool_size[0].type) *
count;
}
VkDescriptorPool DescriptorSetAllocator::allocate_bindless_pool(unsigned num_sets, unsigned num_descriptors)
{
if (!bindless)
return VK_NULL_HANDLE;
VkDescriptorPool pool = VK_NULL_HANDLE;
VkDescriptorPoolCreateInfo info = { VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO };
info.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT_EXT;
info.maxSets = num_sets;
info.poolSizeCount = 1;
VkDescriptorPoolSize size = pool_size[0];
size.descriptorCount = num_descriptors;
info.pPoolSizes = &size;
if (table.vkCreateDescriptorPool(device->get_device(), &info, nullptr, &pool) != VK_SUCCESS)
{
LOGE("Failed to create descriptor pool.\n");
return VK_NULL_HANDLE;
}
return pool;
}
void DescriptorSetAllocator::begin_frame()
{
if (!bindless)
{
// This can only be called in a situation where no command buffers are alive,
// so we don't need to consider any locks here.
if (device->per_frame.size() * device->num_thread_indices != per_thread_and_frame.size())
per_thread_and_frame.resize(device->per_frame.size() * device->num_thread_indices);
// It would be safe to set all offsets to 0 here, but that's a little wasteful.
for (uint32_t i = 0; i < device->num_thread_indices; i++)
per_thread_and_frame[i * device->per_frame.size() + device->frame_context_index].offset = 0;
}
}
VkDescriptorSet DescriptorSetAllocator::request_descriptor_set(unsigned thread_index, unsigned frame_index)
{
VK_ASSERT(!bindless);
size_t flattened_index = thread_index * device->per_frame.size() + frame_index;
auto &state = per_thread_and_frame[flattened_index];
unsigned pool_index = state.offset / VULKAN_NUM_SETS_PER_POOL;
unsigned pool_offset = state.offset % VULKAN_NUM_SETS_PER_POOL;
if (pool_index >= state.pools.size())
{
Pool *pool = state.object_pool.allocate();
VkDescriptorPoolCreateInfo info = { VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO };
info.maxSets = VULKAN_NUM_SETS_PER_POOL;
if (!pool_size.empty())
{
info.poolSizeCount = pool_size.size();
info.pPoolSizes = pool_size.data();
}
bool overallocation =
device->get_device_features().descriptor_pool_overallocation_features.descriptorPoolOverallocation ==
VK_TRUE;
if (overallocation)
{
// No point in allocating new pools if we can keep using the existing one.
info.flags |= VK_DESCRIPTOR_POOL_CREATE_ALLOW_OVERALLOCATION_POOLS_BIT_NV |
VK_DESCRIPTOR_POOL_CREATE_ALLOW_OVERALLOCATION_SETS_BIT_NV;
}
bool need_alloc = !overallocation || state.pools.empty();
pool->pool = VK_NULL_HANDLE;
if (need_alloc && table.vkCreateDescriptorPool(device->get_device(), &info, nullptr, &pool->pool) != VK_SUCCESS)
{
LOGE("Failed to create descriptor pool.\n");
state.object_pool.free(pool);
return VK_NULL_HANDLE;
}
VkDescriptorSetLayout layouts[VULKAN_NUM_SETS_PER_POOL];
std::fill(std::begin(layouts), std::end(layouts), set_layout_pool);
VkDescriptorSetAllocateInfo alloc = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO };
alloc.descriptorPool = pool->pool != VK_NULL_HANDLE ? pool->pool : state.pools.front()->pool;
alloc.descriptorSetCount = VULKAN_NUM_SETS_PER_POOL;
alloc.pSetLayouts = layouts;
if (table.vkAllocateDescriptorSets(device->get_device(), &alloc, pool->sets) != VK_SUCCESS)
LOGE("Failed to allocate descriptor sets.\n");
state.pools.push_back(pool);
}
VkDescriptorSet vk_set = state.pools[pool_index]->sets[pool_offset];
state.offset++;
return vk_set;
}
void DescriptorSetAllocator::clear()
{
for (auto &state : per_thread_and_frame)
{
for (auto *obj : state.pools)
{
table.vkDestroyDescriptorPool(device->get_device(), obj->pool, nullptr);
state.object_pool.free(obj);
}
state.pools.clear();
state.offset = 0;
state.object_pool = {};
}
}
DescriptorSetAllocator::~DescriptorSetAllocator()
{
table.vkDestroyDescriptorSetLayout(device->get_device(), set_layout_pool, nullptr);
table.vkDestroyDescriptorSetLayout(device->get_device(), set_layout_push, nullptr);
clear();
}
BindlessDescriptorPool::BindlessDescriptorPool(Device *device_, DescriptorSetAllocator *allocator_,
VkDescriptorPool pool, uint32_t num_sets, uint32_t num_desc)
: device(device_), allocator(allocator_), desc_pool(pool), total_sets(num_sets), total_descriptors(num_desc)
{
if (!desc_pool)
bindless_buffer = allocator->allocate_bindless_buffer(num_sets, num_desc);
}
BindlessDescriptorPool::~BindlessDescriptorPool()
{
if (desc_pool)
{
if (internal_sync)
device->destroy_descriptor_pool_nolock(desc_pool);
else
device->destroy_descriptor_pool(desc_pool);
}
if (bindless_buffer.get_size() != 0)
{
if (internal_sync)
device->free_descriptor_buffer_allocation_nolock(bindless_buffer);
else
device->free_descriptor_buffer_allocation(bindless_buffer);
}
}
BindlessDescriptorSet BindlessDescriptorPool::get_descriptor_set() const
{
return desc_set;
}
void BindlessDescriptorPool::reset()
{
if (desc_pool != VK_NULL_HANDLE)
allocator->reset_bindless_pool(desc_pool);
desc_set = {};
allocated_descriptor_count = 0;
allocated_sets = 0;
bindless_buffer_offset = 0;
}
bool BindlessDescriptorPool::allocate_descriptors(unsigned count)
{
if (device->get_device_features().supports_descriptor_buffer_or_heap)
{
auto alignment = std::max<VkDeviceSize>(
device->get_device_features().resource_heap_offset_alignment,
device->get_device_features().resource_heap_resource_desc_size);
bindless_buffer_offset = (bindless_buffer_offset + alignment - 1) & ~(alignment - 1);
VkDeviceSize size = allocator->get_variable_size(count);
desc_set = {};
if (bindless_buffer_offset + size <= bindless_buffer.get_size())
{
desc_set.handle.offset = bindless_buffer_offset + bindless_buffer.get_offset();
desc_set.valid = true;
bindless_buffer_offset += size;
allocated_descriptor_count += count;
allocated_sets++;
}
info_ptrs.reserve(count);
}
else
{
// Not all drivers will exhaust the pool for us, so make sure we don't allocate more than expected.
if (allocated_sets == total_sets)
return false;
if (allocated_descriptor_count + count > total_descriptors)
return false;
allocated_descriptor_count += count;
allocated_sets++;
desc_set = allocator->allocate_bindless_set(desc_pool, count);
infos.reserve(count);
}
write_count = 0;
return bool(desc_set);
}
void BindlessDescriptorPool::push_texture(const ImageView &view)
{
// TODO: Deal with integer view for depth-stencil images?
if (!desc_pool)
push_texture(view.get_float_view().sampled.ptr);
else
push_texture(view.get_float_view().view, view.get_image().get_layout(VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL));
}
void BindlessDescriptorPool::push_texture_unorm(const ImageView &view)
{
if (!desc_pool)
push_texture(view.get_unorm_view().sampled.ptr);
else
push_texture(view.get_unorm_view().view, view.get_image().get_layout(VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL));
}
void BindlessDescriptorPool::push_texture_srgb(const ImageView &view)
{
if (!desc_pool)
push_texture(view.get_srgb_view().sampled.ptr);
else
push_texture(view.get_srgb_view().view, view.get_image().get_layout(VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL));
}
void BindlessDescriptorPool::push_texture(VkImageView view, VkImageLayout layout)
{
VK_ASSERT(write_count < infos.get_capacity());
auto &image_info = infos[write_count];
image_info = { VK_NULL_HANDLE, view, layout };
write_count++;
}
void BindlessDescriptorPool::push_texture(const uint8_t *ptr)
{
VK_ASSERT(write_count < info_ptrs.get_capacity());
info_ptrs[write_count++] = ptr;
}
void BindlessDescriptorPool::update()
{
if (device->get_device_features().supports_descriptor_buffer_or_heap)
{
device->managers.descriptor_buffer.copy_sampled_image_n(
device->managers.descriptor_buffer.get_resource_heap().mapped +
desc_set.handle.offset + allocator->get_variable_offset(),
info_ptrs.data(), write_count);
}
else
{
VkWriteDescriptorSet desc = { VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET };
desc.descriptorCount = write_count;
desc.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
desc.dstSet = desc_set.handle.set;
desc.pImageInfo = infos.data();
desc.pBufferInfo = nullptr;
desc.pTexelBufferView = nullptr;
if (write_count)
{
auto &table = device->get_device_table();
table.vkUpdateDescriptorSets(device->get_device(), 1, &desc, 0, nullptr);
}
}
}
void BindlessDescriptorPoolDeleter::operator()(BindlessDescriptorPool *pool)
{
pool->device->handle_pool.bindless_descriptor_pool.free(pool);
}
unsigned BindlessAllocator::push(const ImageView &view)
{
auto ret = unsigned(views.size());
views.push_back(&view);
if (views.size() > VULKAN_NUM_BINDINGS_BINDLESS_VARYING)
{
LOGE("Exceeding maximum number of bindless resources per set (%u >= %u).\n",
unsigned(views.size()), VULKAN_NUM_BINDINGS_BINDLESS_VARYING);
}
return ret;
}
void BindlessAllocator::begin()
{
views.clear();
}
void BindlessAllocator::reset()
{
descriptor_pool.reset();
}
unsigned BindlessAllocator::get_next_offset() const
{
return unsigned(views.size());
}
void BindlessAllocator::reserve_max_resources_per_pool(unsigned set_count, unsigned descriptor_count)
{
max_sets_per_pool = std::max(max_sets_per_pool, set_count);
max_descriptors_per_pool = std::max(max_descriptors_per_pool, descriptor_count);
views.reserve(max_descriptors_per_pool);
}
void BindlessAllocator::set_bindless_resource_type(BindlessResourceType type)
{
resource_type = type;
}
BindlessDescriptorSet BindlessAllocator::commit(Device &device)
{
max_sets_per_pool = std::max(1u, max_sets_per_pool);
max_descriptors_per_pool = std::max<unsigned>(views.size(), max_descriptors_per_pool);
max_descriptors_per_pool = std::max<unsigned>(1u, max_descriptors_per_pool);
max_descriptors_per_pool = std::min(max_descriptors_per_pool, VULKAN_NUM_BINDINGS_BINDLESS_VARYING);
unsigned to_allocate = std::max<unsigned>(views.size(), 1u);
if (!descriptor_pool)
{
descriptor_pool = device.create_bindless_descriptor_pool(
resource_type, max_sets_per_pool, max_descriptors_per_pool);
}
if (!descriptor_pool->allocate_descriptors(to_allocate))
{
descriptor_pool = device.create_bindless_descriptor_pool(
resource_type, max_sets_per_pool, max_descriptors_per_pool);
if (!descriptor_pool->allocate_descriptors(to_allocate))
{
LOGE("Failed to allocate descriptors on a fresh descriptor pool!\n");
return {};
}
}
for (size_t i = 0, n = views.size(); i < n; i++)
descriptor_pool->push_texture(*views[i]);
descriptor_pool->update();
return descriptor_pool->get_descriptor_set();
}
}
@@ -0,0 +1,243 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "hash.hpp"
#include "object_pool.hpp"
#include "temporary_hashmap.hpp"
#include "vulkan_headers.hpp"
#include "sampler.hpp"
#include "limits.hpp"
#include "dynamic_array.hpp"
#include <utility>
#include <vector>
#include "cookie.hpp"
#include "memory_allocator.hpp"
namespace Vulkan
{
class Device;
struct ArraySizeAccessMeta
{
uint8_t array_size : 7;
uint8_t requires_descriptor_size : 1;
};
static_assert(sizeof(ArraySizeAccessMeta) == sizeof(uint8_t), "Unexpected bitfield padding.");
struct DescriptorSetLayout
{
uint32_t sampled_image_mask = 0;
uint32_t storage_image_mask = 0;
uint32_t uniform_buffer_mask = 0;
uint32_t storage_buffer_mask = 0;
uint32_t rtas_mask = 0;
uint32_t sampled_texel_buffer_mask = 0;
uint32_t storage_texel_buffer_mask = 0;
uint32_t input_attachment_mask = 0;
uint32_t sampler_mask = 0;
uint32_t separate_image_mask = 0;
uint32_t fp_mask = 0;
uint32_t immutable_sampler_mask = 0;
ArraySizeAccessMeta meta[VULKAN_NUM_BINDINGS] = {};
enum { UNSIZED_ARRAY = 0x7f };
};
// Avoid -Wclass-memaccess warnings since we hash DescriptorSetLayout.
static const unsigned VULKAN_NUM_SETS_PER_POOL = 64;
static const unsigned VULKAN_DESCRIPTOR_RING_SIZE = 16;
class DescriptorSetAllocator;
class BindlessDescriptorPool;
class ImageView;
struct BindlessDescriptorPoolDeleter
{
void operator()(BindlessDescriptorPool *pool);
};
struct BindlessDescriptorSet
{
union Handle
{
VkDescriptorSet set;
VkDeviceSize offset;
} handle = {};
bool valid = false;
explicit operator bool() const { return valid; }
};
class BindlessDescriptorPool : public Util::IntrusivePtrEnabled<BindlessDescriptorPool, BindlessDescriptorPoolDeleter, HandleCounter>,
public InternalSyncEnabled
{
public:
friend struct BindlessDescriptorPoolDeleter;
explicit BindlessDescriptorPool(Device *device, DescriptorSetAllocator *allocator, VkDescriptorPool pool,
uint32_t total_sets, uint32_t total_descriptors);
~BindlessDescriptorPool();
void operator=(const BindlessDescriptorPool &) = delete;
BindlessDescriptorPool(const BindlessDescriptorPool &) = delete;
void reset();
bool allocate_descriptors(unsigned count);
BindlessDescriptorSet get_descriptor_set() const;
void push_texture(const ImageView &view);
void push_texture_unorm(const ImageView &view);
void push_texture_srgb(const ImageView &view);
void update();
private:
Device *device;
DescriptorSetAllocator *allocator;
VkDescriptorPool desc_pool;
DescriptorBufferAllocation bindless_buffer;
VkDeviceSize bindless_buffer_offset = 0;
BindlessDescriptorSet desc_set;
uint32_t allocated_sets = 0;
uint32_t total_sets = 0;
uint32_t allocated_descriptor_count = 0;
uint32_t total_descriptors = 0;
void push_texture(VkImageView view, VkImageLayout layout);
void push_texture(const uint8_t *ptr);
Util::DynamicArray<VkDescriptorImageInfo> infos;
Util::DynamicArray<const uint8_t *> info_ptrs;
uint32_t write_count = 0;
};
using BindlessDescriptorPoolHandle = Util::IntrusivePtr<BindlessDescriptorPool>;
enum class BindlessResourceType
{
Image
};
class DescriptorSetAllocator : public HashedObject<DescriptorSetAllocator>
{
public:
DescriptorSetAllocator(Util::Hash hash, Device *device, const DescriptorSetLayout &layout,
const uint32_t *stages_for_bindings,
const ImmutableSampler * const *immutable_samplers);
~DescriptorSetAllocator();
void operator=(const DescriptorSetAllocator &) = delete;
DescriptorSetAllocator(const DescriptorSetAllocator &) = delete;
void begin_frame();
VkDescriptorSet request_descriptor_set(unsigned thread_index, unsigned frame_context);
VkDescriptorSetLayout get_layout_for_pool() const
{
return set_layout_pool;
}
VkDescriptorSetLayout get_layout_for_push() const
{
return set_layout_push;
}
void clear();
bool is_bindless() const
{
return bindless;
}
// Legacy descriptors.
VkDescriptorPool allocate_bindless_pool(unsigned num_sets, unsigned num_descriptors);
BindlessDescriptorSet allocate_bindless_set(VkDescriptorPool pool, unsigned num_descriptors);
void reset_bindless_pool(VkDescriptorPool pool);
// Descriptor buffer integration.
DescriptorBufferAllocation allocate_bindless_buffer(unsigned num_sets, unsigned num_descriptors);
VkDeviceSize get_resource_heap_size() const
{
return desc_set_size;
}
VkDeviceSize get_variable_offset() const
{
return desc_set_variable_offset;
}
VkDeviceSize get_variable_size(unsigned count) const;
uint32_t get_binding_offset(uint32_t binding) const
{
return desc_offsets[binding];
}
private:
Device *device;
const VolkDeviceTable &table;
VkDescriptorSetLayout set_layout_pool = VK_NULL_HANDLE;
VkDescriptorSetLayout set_layout_push = VK_NULL_HANDLE;
VkDeviceSize desc_set_size = 0;
VkDeviceSize desc_set_variable_offset = 0;
uint32_t desc_offsets[VULKAN_NUM_BINDINGS] = {};
struct Pool
{
VkDescriptorPool pool;
VkDescriptorSet sets[VULKAN_NUM_SETS_PER_POOL];
};
struct PerThreadAndFrame
{
std::vector<Pool *> pools;
Util::ObjectPool<Pool> object_pool;
uint32_t offset = 0;
};
std::vector<PerThreadAndFrame> per_thread_and_frame;
std::vector<VkDescriptorPoolSize> pool_size;
bool bindless = false;
};
class BindlessAllocator
{
public:
void reserve_max_resources_per_pool(unsigned set_count, unsigned descriptor_count);
void set_bindless_resource_type(BindlessResourceType type);
void begin();
unsigned push(const ImageView &view);
BindlessDescriptorSet commit(Device &device);
unsigned get_next_offset() const;
void reset();
private:
BindlessDescriptorPoolHandle descriptor_pool;
unsigned max_sets_per_pool = 0;
unsigned max_descriptors_per_pool = 0;
BindlessResourceType resource_type = BindlessResourceType::Image;
std::vector<const ImageView *> views;
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,973 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "buffer.hpp"
#include "command_buffer.hpp"
#include "command_pool.hpp"
#include "fence.hpp"
#include "fence_manager.hpp"
#include "image.hpp"
#include "memory_allocator.hpp"
#include "render_pass.hpp"
#include "sampler.hpp"
#include "semaphore.hpp"
#include "semaphore_manager.hpp"
#include "event_manager.hpp"
#include "shader.hpp"
#include "context.hpp"
#include "query_pool.hpp"
#include "buffer_pool.hpp"
#include "indirect_layout.hpp"
#include "pipeline_cache.hpp"
#include "breadcrumbs.hpp"
#include <memory>
#include <vector>
#include <functional>
#include <unordered_map>
#include <stdio.h>
#ifdef GRANITE_VULKAN_SYSTEM_HANDLES
#include "shader_manager.hpp"
#include "resource_manager.hpp"
#endif
#include <atomic>
#include <mutex>
#include <condition_variable>
#ifdef GRANITE_VULKAN_FOSSILIZE
#include "fossilize.hpp"
#endif
#include "quirks.hpp"
#include "small_vector.hpp"
namespace Util
{
class TimelineTraceFile;
}
namespace Granite
{
struct TaskGroup;
}
namespace Vulkan
{
enum class SwapchainRenderPass
{
ColorOnly,
Depth,
DepthStencil
};
struct HostReference
{
const void *data;
size_t size;
};
struct InitialImageBuffer
{
// Either buffer or host is used. Ideally host is used so that host image copy can be used for uploads.
BufferHandle buffer;
HostReference host;
Util::SmallVector<VkBufferImageCopy, 32> blits;
};
struct HandlePool
{
VulkanObjectPool<Buffer> buffers;
VulkanObjectPool<RTAS> rtas;
VulkanObjectPool<Image> images;
VulkanObjectPool<LinearHostImage> linear_images;
VulkanObjectPool<ImageView> image_views;
VulkanObjectPool<BufferView> buffer_views;
VulkanObjectPool<Sampler> samplers;
VulkanObjectPool<FenceHolder> fences;
VulkanObjectPool<SemaphoreHolder> semaphores;
VulkanObjectPool<EventHolder> events;
VulkanObjectPool<QueryPoolResult> query;
VulkanObjectPool<CommandBuffer> command_buffers;
VulkanObjectPool<BindlessDescriptorPool> bindless_descriptor_pool;
VulkanObjectPool<DeviceAllocationOwner> allocations;
};
class DebugChannelInterface
{
public:
union Word
{
uint32_t u32;
int32_t s32;
float f32;
};
virtual void message(const std::string &tag, uint32_t code, uint32_t x, uint32_t y, uint32_t z,
uint32_t word_count, const Word *words) = 0;
};
namespace Helper
{
struct WaitSemaphores
{
Util::SmallVector<VkSemaphoreSubmitInfo> binary_waits;
Util::SmallVector<VkSemaphoreSubmitInfo> timeline_waits;
};
class BatchComposer
{
public:
enum { MaxSubmissions = 8 };
explicit BatchComposer(uint64_t present_id_nv);
void add_wait_submissions(WaitSemaphores &sem);
void add_wait_semaphore(SemaphoreHolder &sem, VkPipelineStageFlags2 stage);
void add_wait_semaphore(VkSemaphore sem, VkPipelineStageFlags2 stage);
void add_signal_semaphore(VkSemaphore sem, VkPipelineStageFlags2 stage, uint64_t count);
void add_command_buffer(VkCommandBuffer cmd);
void begin_batch();
Util::SmallVector<VkSubmitInfo2, MaxSubmissions> &bake(int profiling_iteration = -1);
private:
Util::SmallVector<VkSubmitInfo2, MaxSubmissions> submits;
VkPerformanceQuerySubmitInfoKHR profiling_infos[Helper::BatchComposer::MaxSubmissions];
Util::SmallVector<VkLatencySubmissionPresentIdNV> present_ids_nv;
Util::SmallVector<VkSemaphoreSubmitInfo> waits[MaxSubmissions];
Util::SmallVector<VkSemaphoreSubmitInfo> signals[MaxSubmissions];
Util::SmallVector<VkCommandBufferSubmitInfo> cmds[MaxSubmissions];
uint64_t present_id_nv = 0;
unsigned submit_index = 0;
};
}
class Device
: public Util::IntrusivePtrEnabled<Device, std::default_delete<Device>, HandleCounter>
#ifdef GRANITE_VULKAN_FOSSILIZE
, public Fossilize::StateCreatorInterface
#endif
{
public:
// Device-based objects which need to poke at internal data structures when their lifetimes end.
// Don't want to expose a lot of internal guts to make this work.
friend class QueryPool;
friend struct QueryPoolResultDeleter;
friend class EventHolder;
friend struct EventHolderDeleter;
friend class SemaphoreHolder;
friend struct SemaphoreHolderDeleter;
friend class FenceHolder;
friend struct FenceHolderDeleter;
friend class Sampler;
friend struct SamplerDeleter;
friend class ImmutableSampler;
friend class ImmutableYcbcrConversion;
friend class Buffer;
friend struct BufferDeleter;
friend class RTAS;
friend struct RTASDeleter;
friend class BufferView;
friend struct BufferViewDeleter;
friend class ImageView;
friend struct ImageViewDeleter;
friend class Image;
friend struct ImageDeleter;
friend struct LinearHostImageDeleter;
friend class CommandBuffer;
friend struct CommandBufferDeleter;
friend class BindlessDescriptorPool;
friend struct BindlessDescriptorPoolDeleter;
friend class Program;
friend class WSI;
friend class Cookie;
friend class Framebuffer;
friend class PipelineLayout;
friend class FramebufferAllocator;
friend class RenderPass;
friend class Texture;
friend class DescriptorSetAllocator;
friend class Shader;
friend class ImageResourceHolder;
friend class DeviceAllocationOwner;
friend struct DeviceAllocationDeleter;
Device();
~Device();
// No move-copy.
void operator=(Device &&) = delete;
Device(Device &&) = delete;
// Only called by main thread, during setup phase.
void set_context(const Context &context);
// This is asynchronous in nature. See query_initialization_progress().
// Kicks off Fossilize and shader manager caching.
void begin_shader_caches();
// For debug or trivial applications, blocks until all shader cache work is done.
void wait_shader_caches();
void init_swapchain(const std::vector<VkImage> &swapchain_images, unsigned width, unsigned height, VkFormat format,
VkSurfaceTransformFlagBitsKHR transform, VkImageUsageFlags usage, VkImageLayout layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
void set_swapchain_queue_family_support(uint32_t queue_family_support);
bool can_touch_swapchain_in_command_buffer(CommandBuffer::Type type) const;
void init_external_swapchain(const std::vector<ImageHandle> &swapchain_images);
void init_frame_contexts(unsigned count);
const VolkDeviceTable &get_device_table() const;
// Profiling
bool init_performance_counters(CommandBuffer::Type type, const std::vector<std::string> &names);
bool acquire_profiling();
void release_profiling();
void query_available_performance_counters(CommandBuffer::Type type,
uint32_t *count,
const VkPerformanceCounterKHR **counters,
const VkPerformanceCounterDescriptionKHR **desc);
ImageView &get_swapchain_view();
ImageView &get_swapchain_view(unsigned index);
unsigned get_num_swapchain_images() const;
unsigned get_num_frame_contexts() const;
unsigned get_swapchain_index() const;
unsigned get_current_frame_context() const;
size_t get_pipeline_cache_size();
bool get_pipeline_cache_data(uint8_t *data, size_t size);
// If persistent_mapping is true, the data pointer lifetime is live as long as the device is.
// Useful for read-only file mmap.
bool init_pipeline_cache(const uint8_t *data, size_t size, bool persistent_mapping = false);
// Frame-pushing interface.
void next_frame_context();
bool next_frame_context_is_non_blocking();
// Normally, the main thread ensures forward progress of the frame context
// so that async tasks don't have to care about it,
// but in the case where async threads are continuously pumping Vulkan work
// in the background, they need to reclaim memory if WSI goes to sleep for a long period of time.
void next_frame_context_in_async_thread();
void set_enable_async_thread_frame_context(bool enable);
void wait_idle();
void end_frame_context();
// RenderDoc integration API for app-guided captures.
static bool init_renderdoc_capture();
// Calls next_frame_context() and begins a renderdoc capture.
void begin_renderdoc_capture();
// Calls next_frame_context() and ends the renderdoc capture.
void end_renderdoc_capture();
// Set names for objects for debuggers and profilers.
void set_name(const Buffer &buffer, const char *name);
void set_name(const Image &image, const char *name);
void set_name(const CommandBuffer &cmd, const char *name);
// Generic version.
void set_name(uint64_t object, VkObjectType type, const char *name);
// Submission interface, may be called from any thread at any time.
void flush_frame();
CommandBufferHandle request_command_buffer(CommandBuffer::Type type = CommandBuffer::Type::Generic);
CommandBufferHandle request_command_buffer_for_thread(unsigned thread_index, CommandBuffer::Type type = CommandBuffer::Type::Generic);
// Must be given back with submit_discard().
CommandBufferHandle request_borrowed_command_buffer(VkCommandBuffer cmd);
CommandBufferHandle request_profiled_command_buffer(CommandBuffer::Type type = CommandBuffer::Type::Generic);
CommandBufferHandle request_profiled_command_buffer_for_thread(unsigned thread_index, CommandBuffer::Type type = CommandBuffer::Type::Generic);
void submit(CommandBufferHandle &cmd, Fence *fence = nullptr,
unsigned semaphore_count = 0, Semaphore *semaphore = nullptr);
void submit_empty(CommandBuffer::Type type,
Fence *fence = nullptr,
SemaphoreHolder *semaphore = nullptr);
// Mark that there have been work submitted in this frame context outside our control
// that accesses resources Vulkan::Device owns.
void submit_external(CommandBuffer::Type type);
void submit_discard(CommandBufferHandle &cmd);
QueueIndices get_physical_queue_type(CommandBuffer::Type queue_type) const;
void register_time_interval(std::string tid, QueryPoolHandle start_ts, QueryPoolHandle end_ts,
const std::string &tag);
// Request shaders and programs. These objects are owned by the Device.
Shader *request_shader(const uint32_t *code, size_t size, const ResourceLayout *layout = nullptr);
Shader *request_shader_by_hash(Util::Hash hash);
Program *request_program(const uint32_t *task_data, size_t task_size,
const uint32_t *mesh_data, size_t mesh_size,
const uint32_t *fragment_data, size_t fragment_size,
const ResourceLayout *task_layout = nullptr,
const ResourceLayout *mesh_layout = nullptr,
const ResourceLayout *fragment_layout = nullptr);
Program *request_program(const uint32_t *vertex_data, size_t vertex_size,
const uint32_t *fragment_data, size_t fragment_size,
const ResourceLayout *vertex_layout = nullptr,
const ResourceLayout *fragment_layout = nullptr);
Program *request_program(const uint32_t *compute_data, size_t compute_size,
const ResourceLayout *layout = nullptr);
Program *request_program(Shader *task, Shader *mesh, Shader *fragment, const ImmutableSamplerBank *sampler_bank = nullptr);
Program *request_program(Shader *vertex, Shader *fragment, const ImmutableSamplerBank *sampler_bank = nullptr);
Program *request_program(Shader *compute, const ImmutableSamplerBank *sampler_bank = nullptr);
const IndirectLayout *request_indirect_layout(const PipelineLayout *layout, const IndirectLayoutToken *tokens,
uint32_t num_tokens, uint32_t stride);
const ImmutableYcbcrConversion *request_immutable_ycbcr_conversion(const VkSamplerYcbcrConversionCreateInfo &info);
const ImmutableSampler *request_immutable_sampler(const SamplerCreateInfo &info, const ImmutableYcbcrConversion *ycbcr);
// Map and unmap buffer objects.
void *map_host_buffer(const Buffer &buffer, MemoryAccessFlags access);
void unmap_host_buffer(const Buffer &buffer, MemoryAccessFlags access);
void *map_host_buffer(const Buffer &buffer, MemoryAccessFlags access, VkDeviceSize offset, VkDeviceSize length);
void unmap_host_buffer(const Buffer &buffer, MemoryAccessFlags access, VkDeviceSize offset, VkDeviceSize length);
void *map_linear_host_image(const LinearHostImage &image, MemoryAccessFlags access);
void unmap_linear_host_image_and_sync(const LinearHostImage &image, MemoryAccessFlags access);
// Create buffers and images.
BufferHandle create_buffer(const BufferCreateInfo &info, const void *initial = nullptr);
BufferHandle create_imported_host_buffer(const BufferCreateInfo &info, VkExternalMemoryHandleTypeFlagBits type, void *host_buffer);
ImageHandle create_image(const ImageCreateInfo &info, const ImageInitialData *initial = nullptr);
ImageHandle create_image_from_staging_buffer(const ImageCreateInfo &info, const InitialImageBuffer *buffer);
LinearHostImageHandle create_linear_host_image(const LinearHostImageCreateInfo &info);
BufferHandle wrap_buffer(const BufferCreateInfo &info, VkBuffer buffer, bool supports_bda = true);
// Does not create any default image views. Only wraps the VkImage
// as a non-owned handle for purposes of API interop.
ImageHandle wrap_image(const ImageCreateInfo &info, VkImage img);
DeviceAllocationOwnerHandle take_device_allocation_ownership(Image &image);
DeviceAllocationOwnerHandle allocate_memory(const MemoryAllocateInfo &info);
// If cmd is not null, the RTAS is immediately built.
// If compacted_size is not null, a compacted size query will be made. info.mode must be compatible with compaction.
RTASHandle create_rtas(const BottomRTASCreateInfo &info, CommandBuffer *cmd, QueryPoolHandle *compacted_size);
RTASHandle create_rtas(const TopRTASCreateInfo &info, CommandBuffer *cmd);
// Generic creation methods.
RTASHandle create_rtas(VkAccelerationStructureTypeKHR type, VkDeviceSize size);
RTASHandle create_rtas(VkAccelerationStructureTypeKHR type, BufferHandle buffer, VkDeviceSize offset, VkDeviceSize size);
// Create staging buffers for images.
// This is deprecated and considered slow path.
// If number of subresources is 1, the fast path can be taken.
InitialImageBuffer create_image_staging_buffer(const ImageCreateInfo &info, const ImageInitialData *initial);
// Only takes a reference to the layout.
// Ideal path when uploading resources since it's compatible with host image copy, etc.
InitialImageBuffer create_image_staging_buffer(const TextureFormatLayout &layout);
// Create image view, buffer views and samplers.
ImageViewHandle create_image_view(const ImageViewCreateInfo &view_info);
BufferViewHandle create_buffer_view(const BufferViewCreateInfo &view_info);
SamplerHandle create_sampler(const SamplerCreateInfo &info);
BindlessDescriptorPoolHandle create_bindless_descriptor_pool(BindlessResourceType type,
unsigned num_sets, unsigned num_descriptors);
// Render pass helpers.
bool image_format_is_supported(VkFormat format, VkFormatFeatureFlags2KHR required, VkImageTiling tiling = VK_IMAGE_TILING_OPTIMAL) const;
void get_format_properties(VkFormat format, VkFormatProperties3KHR *properties) const;
bool get_image_format_properties(VkFormat format, VkImageType type, VkImageTiling tiling,
VkImageUsageFlags usage, VkImageCreateFlags flags,
const void *pNext,
VkImageFormatProperties2 *properties2) const;
VkFormat get_default_depth_stencil_format() const;
VkFormat get_default_depth_format() const;
ImageHandle get_transient_attachment(unsigned width, unsigned height, VkFormat format,
unsigned index = 0, unsigned samples = 1, unsigned layers = 1);
RenderPassInfo get_swapchain_render_pass(SwapchainRenderPass style);
// Semaphore API:
// Semaphores in Granite are abstracted to support both binary and timeline semaphores
// internally.
// In practice this means that semaphores behave like single-use binary semaphores,
// with one signal and one wait.
// A single semaphore handle is not reused for multiple submissions, and they must be recycled through
// the device. The intended use is device.submit(&sem), device.add_wait_semaphore(sem); dispose(sem);
// For timeline semaphores, the semaphore is just a proxy object which
// holds the internally owned VkSemaphore + timeline value and is otherwise lightweight.
//
// However, there are various use cases where we explicitly need semaphore objects:
// - Interoperate with other code that only accepts VkSemaphore.
// - Interoperate with external objects. We need to know whether to use binary or timeline.
// For timelines, we need to know which handle type to use (OPAQUE or ID3D12Fence).
// Binary external semaphore is always opaque with TEMPORARY semantics.
void add_wait_semaphore(CommandBuffer::Type type, Semaphore semaphore, VkPipelineStageFlags2 stages, bool flush);
// If transfer_ownership is set, Semaphore owns the VkSemaphore. Otherwise, application must
// free the semaphore when GPU usage of it is complete.
Semaphore request_semaphore(VkSemaphoreTypeKHR type, VkSemaphore handle = VK_NULL_HANDLE, bool transfer_ownership = false);
// Requests a binary or timeline semaphore that can be used to import/export.
// These semaphores cannot be used directly by add_wait_semaphore() and submit_empty().
// See request_timeline_semaphore_as_binary() for how to use timelines.
Semaphore request_semaphore_external(VkSemaphoreTypeKHR type,
VkExternalSemaphoreHandleTypeFlagBits handle_type);
// The created semaphore does not hold ownership of the VkSemaphore object.
// This is used when we want to wait on or signal an external timeline semaphore at a specific timeline value.
// We must collapse the timeline to a "binary" semaphore before we can call submit_empty or add_wait_semaphore().
Semaphore request_timeline_semaphore_as_binary(const SemaphoreHolder &holder, uint64_t value);
// A proxy semaphore which lets us grab a semaphore handle before we signal it.
// Move assignment can be used to move a payload.
// Mostly useful to deal better with render graph implementation.
// For time being however, we'll support moving the payload over to the proxy object.
Semaphore request_proxy_semaphore();
// For compat with existing code that uses this entry point.
inline Semaphore request_legacy_semaphore() { return request_semaphore(VK_SEMAPHORE_TYPE_BINARY_KHR); }
inline VkDevice get_device() const
{
return device;
}
inline VkPhysicalDevice get_physical_device() const
{
return gpu;
}
inline VkInstance get_instance() const
{
return instance;
}
inline const VkPhysicalDeviceMemoryProperties &get_memory_properties() const
{
return mem_props;
}
inline const VkPhysicalDeviceProperties &get_gpu_properties() const
{
return gpu_props;
}
void get_memory_budget(HeapBudget *budget);
const Sampler &get_stock_sampler(StockSampler sampler) const;
#ifdef GRANITE_VULKAN_SYSTEM_HANDLES
// To obtain ShaderManager, ShaderModules must be observed to be complete
// in query_initialization_progress().
ShaderManager &get_shader_manager();
ResourceManager &get_resource_manager();
Granite::FileMappingHandle persistent_pipeline_cache;
#endif
// Useful for loading screens or otherwise figuring out
// when we can start rendering in a stable state.
enum class InitializationStage
{
CacheMaintenance,
// When this is done, shader modules and the shader manager have been populated.
// At this stage it is safe to use shaders in a configuration where we
// don't have SPIRV-Cross and/or shaderc to do on the fly compilation.
// For shipping configurations. We can still compile pipelines, but it may stutter.
ShaderModules,
// When this is done, pipelines should never stutter if Fossilize knows about the pipeline.
Pipelines
};
// 0 -> not started
// [1, 99] rough percentage of completion
// >= 100 done
unsigned query_initialization_progress(InitializationStage status) const;
// For some platforms, the device and queue might be shared, possibly across threads, so need some mechanism to
// lock the global device and queue.
void set_queue_lock(std::function<void ()> lock_callback,
std::function<void ()> unlock_callback);
// Alternative form, when we have to provide lock callbacks to external APIs.
void external_queue_lock();
void external_queue_unlock();
const ImplementationWorkarounds &get_workarounds() const
{
return workarounds;
}
const DeviceFeatures &get_device_features() const
{
return ext;
}
bool consumes_debug_markers() const
{
return debug_marker_sensitive;
}
bool swapchain_touched() const;
double convert_device_timestamp_delta(uint64_t start_ticks, uint64_t end_ticks) const;
int64_t convert_timestamp_to_absolute_nsec(const QueryPoolResult &handle);
// Writes a timestamp on host side, which is calibrated to the GPU timebase.
QueryPoolHandle write_calibrated_timestamp();
// A split version of VkEvent handling which lets us record a wait command before signal is recorded.
PipelineEvent begin_signal_event();
const Context::SystemHandles &get_system_handles() const
{
return system_handles;
}
void configure_default_geometry_samplers(float max_aniso, float lod_bias);
bool supports_subgroup_size_log2(bool subgroup_full_group,
uint8_t subgroup_minimum_size_log2,
uint8_t subgroup_maximum_size_log2,
VkShaderStageFlagBits stage = VK_SHADER_STAGE_COMPUTE_BIT) const;
const QueueInfo &get_queue_info() const;
void timestamp_log_reset();
void timestamp_log(const TimestampIntervalReportCallback &cb) const;
private:
VkInstance instance = VK_NULL_HANDLE;
VkPhysicalDevice gpu = VK_NULL_HANDLE;
VkDevice device = VK_NULL_HANDLE;
const VolkDeviceTable *table = nullptr;
const Context *ctx = nullptr;
QueueInfo queue_info;
unsigned num_thread_indices = 1;
std::atomic_uint64_t cookie;
uint64_t allocate_cookie();
void bake_program(Program &program, const ImmutableSamplerBank *sampler_bank);
void merge_combined_resource_layout(CombinedResourceLayout &layout, const Program &program);
void request_vertex_block(BufferBlock &block, VkDeviceSize size);
void request_index_block(BufferBlock &block, VkDeviceSize size);
void request_uniform_block(BufferBlock &block, VkDeviceSize size);
void request_staging_block(BufferBlock &block, VkDeviceSize size);
QueryPoolHandle write_timestamp(VkCommandBuffer cmd, VkPipelineStageFlags2 stage);
void set_acquire_semaphore(unsigned index, Semaphore acquire);
void set_present_id(VkSwapchainKHR low_latency_swapchain, uint64_t present_id);
Semaphore consume_release_semaphore();
VkQueue get_current_present_queue() const;
CommandBuffer::Type get_current_present_queue_type() const;
const PipelineLayout *request_pipeline_layout(const CombinedResourceLayout &layout,
const ImmutableSamplerBank *immutable_samplers);
DescriptorSetAllocator *request_descriptor_set_allocator(const DescriptorSetLayout &layout,
const uint32_t *stages_for_sets,
const ImmutableSampler * const *immutable_samplers);
const Framebuffer &request_framebuffer(const RenderPassInfo &info);
const RenderPass &request_render_pass(const RenderPassInfo &info, bool compatible);
VkPhysicalDeviceMemoryProperties mem_props;
VkPhysicalDeviceProperties gpu_props;
DeviceFeatures ext;
bool debug_marker_sensitive = false;
void init_stock_samplers();
void init_stock_sampler(StockSampler sampler, float max_aniso, float lod_bias);
void init_timeline_semaphores();
void deinit_timeline_semaphores();
uint64_t update_wrapped_device_timestamp(uint64_t ts);
Context::SystemHandles system_handles;
QueryPoolHandle write_timestamp_nolock(VkCommandBuffer cmd, VkPipelineStageFlags2 stage);
QueryPoolHandle write_calibrated_timestamp_nolock();
void register_time_interval_nolock(std::string tid, QueryPoolHandle start_ts, QueryPoolHandle end_ts,
const std::string &tag);
// Make sure this is deleted last.
HandlePool handle_pool;
// Calibrated timestamps.
void init_calibrated_timestamps();
void recalibrate_timestamps();
bool resample_calibrated_timestamps();
VkTimeDomainEXT calibrated_time_domain = VK_TIME_DOMAIN_DEVICE_EXT;
int64_t calibrated_timestamp_device = 0;
int64_t calibrated_timestamp_host = 0;
int64_t calibrated_timestamp_device_accum = 0;
unsigned timestamp_calibration_counter = 0;
Vulkan::QueryPoolHandle frame_context_begin_ts;
struct Managers
{
DeviceAllocator memory;
FenceManager fence;
SemaphoreManager semaphore;
EventManager event;
BufferPool vbo, ibo, ubo, staging;
TimestampIntervalManager timestamps;
DescriptorBufferAllocator descriptor_buffer;
BreadcrumbsTracker breadcrumbs;
};
Managers managers;
struct
{
std::mutex memory_lock;
std::mutex lock;
std::condition_variable cond;
Util::RWSpinLock read_only_cache;
unsigned counter = 0;
bool async_frame_context = false;
} lock;
struct PerFrame
{
PerFrame(Device *device, unsigned index);
~PerFrame();
void operator=(const PerFrame &) = delete;
PerFrame(const PerFrame &) = delete;
bool wait(uint64_t timeout);
void begin();
void trim_command_pools();
Device &device;
unsigned frame_index;
const VolkDeviceTable &table;
Managers &managers;
std::vector<CommandPool> cmd_pools[QUEUE_INDEX_COUNT];
VkSemaphore timeline_semaphores[QUEUE_INDEX_COUNT] = {};
uint64_t timeline_fences[QUEUE_INDEX_COUNT] = {};
QueryPool query_pool_ts, query_pool_rtas;
std::vector<BufferBlock> vbo_blocks;
std::vector<BufferBlock> ibo_blocks;
std::vector<BufferBlock> ubo_blocks;
std::vector<BufferBlock> staging_blocks;
std::vector<VkFence> wait_and_recycle_fences;
std::vector<DeviceAllocation> allocations;
std::vector<VkFramebuffer> destroyed_framebuffers;
std::vector<VkSampler> destroyed_samplers;
std::vector<CachedImageView> destroyed_image_views;
std::vector<CachedBufferView> destroyed_buffer_views;
std::vector<VkImage> destroyed_images;
std::vector<VkBuffer> destroyed_buffers;
std::vector<VkAccelerationStructureKHR> destroyed_rtas;
std::vector<VkDescriptorPool> destroyed_descriptor_pools;
Util::SmallVector<CommandBufferHandle> submissions[QUEUE_INDEX_COUNT];
std::vector<VkSemaphore> recycled_semaphores;
std::vector<VkEvent> recycled_events;
std::vector<VkSemaphore> destroyed_semaphores;
std::vector<VkSemaphore> consumed_semaphores;
std::vector<VkIndirectExecutionSetEXT> destroyed_execution_sets;
std::vector<DescriptorBufferAllocation> descriptor_buffer_allocs;
std::vector<CachedDescriptorPayload> cached_descriptor_payloads;
std::vector<BufferMarkerHandle> breadcrumbs;
struct DebugChannel
{
DebugChannelInterface *iface;
std::string tag;
BufferHandle buffer;
};
std::vector<DebugChannel> debug_channels;
struct TimestampIntervalHandles
{
std::string tid;
QueryPoolHandle start_ts;
QueryPoolHandle end_ts;
TimestampInterval *timestamp_tag;
};
std::vector<TimestampIntervalHandles> timestamp_intervals;
bool in_destructor = false;
};
// The per frame structure must be destroyed after
// the hashmap data structures below, so it must be declared before.
std::vector<std::unique_ptr<PerFrame>> per_frame;
struct
{
Semaphore acquire;
Semaphore release;
std::vector<ImageHandle> swapchain;
VkQueue present_queue = VK_NULL_HANDLE;
Vulkan::CommandBuffer::Type present_queue_type = {};
uint32_t queue_family_support_mask = 0;
unsigned index = 0;
bool consumed = false;
struct
{
uint64_t present_id;
bool need_submit_begin_marker;
VkSwapchainKHR swapchain;
} low_latency = {};
} wsi;
bool can_touch_swapchain_in_command_buffer(QueueIndices physical_type) const;
struct QueueData
{
Util::SmallVector<Semaphore> wait_semaphores;
Util::SmallVector<VkPipelineStageFlags2> wait_stages;
bool need_fence = false;
VkSemaphore timeline_semaphore = VK_NULL_HANDLE;
uint64_t current_timeline = 0;
PerformanceQueryPool performance_query_pool;
uint32_t implicit_sync_to_queues = 0;
uint32_t has_incoming_queue_dependencies = 0;
} queue_data[QUEUE_INDEX_COUNT];
struct InternalFence
{
VkFence fence;
VkSemaphore timeline;
uint64_t value;
};
void submit_queue(QueueIndices physical_type, InternalFence *fence,
SemaphoreHolder *external_semaphore = nullptr,
unsigned semaphore_count = 0,
Semaphore *semaphore = nullptr,
int profiled_iteration = -1);
PerFrame &frame()
{
VK_ASSERT(frame_context_index < per_frame.size());
VK_ASSERT(per_frame[frame_context_index]);
return *per_frame[frame_context_index];
}
const PerFrame &frame() const
{
VK_ASSERT(frame_context_index < per_frame.size());
VK_ASSERT(per_frame[frame_context_index]);
return *per_frame[frame_context_index];
}
unsigned frame_context_index = 0;
uint32_t find_memory_type(BufferDomain domain, uint32_t mask) const;
uint32_t find_memory_type(ImageDomain domain, uint32_t mask) const;
uint32_t find_memory_type(uint32_t required, uint32_t mask) const;
bool memory_type_is_device_optimal(uint32_t type) const;
bool memory_type_is_host_visible(uint32_t type) const;
const ImmutableSampler *samplers[static_cast<unsigned>(StockSampler::Count)] = {};
VulkanCache<PipelineLayout> pipeline_layouts;
VulkanCache<DescriptorSetAllocator> descriptor_set_allocators;
VulkanCache<RenderPass> render_passes;
VulkanCache<Shader> shaders;
VulkanCache<Program> programs;
VulkanCache<ImmutableSampler> immutable_samplers;
VulkanCache<ImmutableYcbcrConversion> immutable_ycbcr_conversions;
VulkanCache<IndirectLayout> indirect_layouts;
FramebufferAllocator framebuffer_allocator;
TransientAttachmentAllocator transient_allocator;
VkPipelineCache legacy_pipeline_cache = VK_NULL_HANDLE;
PipelineCache pipeline_binary_cache;
void init_pipeline_cache();
void flush_pipeline_cache();
PerformanceQueryPool &get_performance_query_pool(QueueIndices physical_type);
PipelineEvent request_pipeline_event();
std::function<void ()> queue_lock_callback;
std::function<void ()> queue_unlock_callback;
void flush_frame_nolock(QueueIndices physical_type);
void submit_empty_inner(QueueIndices type, InternalFence *fence,
SemaphoreHolder *external_semaphore,
unsigned semaphore_count,
Semaphore *semaphore);
void collect_wait_semaphores(QueueData &data, Helper::WaitSemaphores &semaphores);
void emit_queue_signals(Helper::BatchComposer &composer,
SemaphoreHolder *external_semaphore,
VkSemaphore sem, uint64_t timeline, InternalFence *fence,
unsigned semaphore_count, Semaphore *semaphores);
void emit_implicit_sync_to_queues(QueueIndices physical_type);
VkResult submit_batches(Helper::BatchComposer &composer, VkQueue queue, VkFence fence,
int profiling_iteration = -1);
VkResult queue_submit(VkQueue queue, uint32_t count, const VkSubmitInfo2 *submits, VkFence fence);
void destroy_buffer(VkBuffer buffer);
void destroy_rtas(VkAccelerationStructureKHR rtas);
void destroy_image(VkImage image);
void destroy_image_view(const CachedImageView &view);
void destroy_buffer_view(const CachedBufferView &view);
void destroy_sampler(VkSampler sampler);
void destroy_framebuffer(VkFramebuffer framebuffer);
void destroy_semaphore(VkSemaphore semaphore);
void consume_semaphore(VkSemaphore semaphore);
void recycle_semaphore(VkSemaphore semaphore);
void destroy_event(VkEvent event);
void free_memory(const DeviceAllocation &alloc);
void reset_fence(VkFence fence, bool observed_wait);
void destroy_descriptor_pool(VkDescriptorPool desc_pool);
void destroy_indirect_execution_set(VkIndirectExecutionSetEXT exec_set);
void free_descriptor_buffer_allocation(const DescriptorBufferAllocation &alloc);
void free_cached_descriptor_payload(const CachedDescriptorPayload &payload);
void destroy_buffer_nolock(VkBuffer buffer);
void destroy_rtas_nolock(VkAccelerationStructureKHR rtas);
void destroy_image_nolock(VkImage image);
void destroy_image_view_nolock(const CachedImageView &view);
void destroy_buffer_view_nolock(const CachedBufferView &view);
void destroy_sampler_nolock(VkSampler sampler);
void destroy_framebuffer_nolock(VkFramebuffer framebuffer);
void destroy_semaphore_nolock(VkSemaphore semaphore);
void consume_semaphore_nolock(VkSemaphore semaphore);
void recycle_semaphore_nolock(VkSemaphore semaphore);
void destroy_event_nolock(VkEvent event);
void free_memory_nolock(const DeviceAllocation &alloc);
void destroy_descriptor_pool_nolock(VkDescriptorPool desc_pool);
void reset_fence_nolock(VkFence fence, bool observed_wait);
void destroy_indirect_execution_set_nolock(VkIndirectExecutionSetEXT exec_set);
void free_descriptor_buffer_allocation_nolock(const DescriptorBufferAllocation &alloc);
void free_cached_descriptor_payload_nolock(const CachedDescriptorPayload &payload);
void flush_frame_nolock();
CommandBufferHandle request_command_buffer_nolock(unsigned thread_index, CommandBuffer::Type type, bool profiled);
void submit_discard_nolock(CommandBufferHandle &cmd);
void submit_and_sync_to_queues(CommandBufferHandle &cmd, uint32_t sync_to_queues);
void submit_nolock(CommandBufferHandle cmd, Fence *fence,
unsigned semaphore_count, Semaphore *semaphore);
void submit_empty_nolock(QueueIndices physical_type, Fence *fence,
SemaphoreHolder *semaphore, int profiling_iteration);
void add_wait_semaphore_nolock(QueueIndices type, Semaphore semaphore,
VkPipelineStageFlags2 stages, bool flush);
void request_vertex_block_nolock(BufferBlock &block, VkDeviceSize size);
void request_index_block_nolock(BufferBlock &block, VkDeviceSize size);
void request_uniform_block_nolock(BufferBlock &block, VkDeviceSize size);
void request_staging_block_nolock(BufferBlock &block, VkDeviceSize size);
CommandBufferHandle request_secondary_command_buffer_for_thread(unsigned thread_index,
const Framebuffer *framebuffer,
unsigned subpass,
CommandBuffer::Type type = CommandBuffer::Type::Generic);
void add_frame_counter_nolock();
void decrement_frame_counter_nolock();
void submit_secondary(CommandBuffer &primary, CommandBuffer &secondary);
void wait_idle_nolock();
void end_frame_nolock();
void add_debug_channel_buffer(DebugChannelInterface *iface, std::string tag, BufferHandle buffer);
void parse_debug_channel(const PerFrame::DebugChannel &channel);
Fence request_legacy_fence();
#ifdef GRANITE_VULKAN_SYSTEM_HANDLES
ShaderManager shader_manager;
ResourceManager resource_manager;
void init_shader_manager_cache(Granite::TaskGroup *shader_compilation_group);
void flush_shader_manager_cache();
#endif
#ifdef GRANITE_VULKAN_FOSSILIZE
bool enqueue_create_sampler(Fossilize::Hash hash, const VkSamplerCreateInfo *create_info, VkSampler *sampler) override;
bool enqueue_create_descriptor_set_layout(Fossilize::Hash hash, const VkDescriptorSetLayoutCreateInfo *create_info, VkDescriptorSetLayout *layout) override;
bool enqueue_create_pipeline_layout(Fossilize::Hash hash, const VkPipelineLayoutCreateInfo *create_info, VkPipelineLayout *layout) override;
bool enqueue_create_shader_module(Fossilize::Hash hash, const VkShaderModuleCreateInfo *create_info, VkShaderModule *module) override;
bool enqueue_create_render_pass(Fossilize::Hash hash, const VkRenderPassCreateInfo *create_info, VkRenderPass *render_pass) override;
bool enqueue_create_render_pass2(Fossilize::Hash hash, const VkRenderPassCreateInfo2 *create_info, VkRenderPass *render_pass) override;
bool enqueue_create_compute_pipeline(Fossilize::Hash hash, const VkComputePipelineCreateInfo *create_info, VkPipeline *pipeline) override;
bool enqueue_create_graphics_pipeline(Fossilize::Hash hash, const VkGraphicsPipelineCreateInfo *create_info, VkPipeline *pipeline) override;
bool enqueue_create_raytracing_pipeline(Fossilize::Hash hash, const VkRayTracingPipelineCreateInfoKHR *create_info, VkPipeline *pipeline) override;
bool fossilize_replay_graphics_pipeline(Fossilize::Hash hash, VkGraphicsPipelineCreateInfo &info);
bool fossilize_replay_compute_pipeline(Fossilize::Hash hash, VkComputePipelineCreateInfo &info);
void replay_tag_simple(Fossilize::ResourceTag tag);
void register_graphics_pipeline(Fossilize::Hash hash, const VkGraphicsPipelineCreateInfo &info);
void register_compute_pipeline(Fossilize::Hash hash, const VkComputePipelineCreateInfo &info);
void register_render_pass(VkRenderPass render_pass, Fossilize::Hash hash, const VkRenderPassCreateInfo2KHR &info);
void register_descriptor_set_layout(VkDescriptorSetLayout layout, Fossilize::Hash hash, const VkDescriptorSetLayoutCreateInfo &info);
void register_pipeline_layout(VkPipelineLayout layout, Fossilize::Hash hash, const VkPipelineLayoutCreateInfo &info);
void register_shader_module(VkShaderModule module, Fossilize::Hash hash, const VkShaderModuleCreateInfo &info);
void register_sampler(VkSampler sampler, Fossilize::Hash hash, const VkSamplerCreateInfo &info);
void register_sampler_ycbcr_conversion(VkSamplerYcbcrConversion ycbcr, const VkSamplerYcbcrConversionCreateInfo &info);
struct RecorderState;
std::unique_ptr<RecorderState> recorder_state;
struct ReplayerState;
std::unique_ptr<ReplayerState> replayer_state;
void promote_write_cache_to_readonly() const;
void promote_readonly_db_from_assets() const;
void init_pipeline_state(const Fossilize::FeatureFilter &filter,
const VkPhysicalDeviceFeatures2 &pdf2,
const VkApplicationInfo &application_info);
void flush_pipeline_state();
void block_until_shader_module_ready();
void block_until_pipeline_ready();
#endif
ImplementationWorkarounds workarounds;
void init_workarounds();
void fill_buffer_sharing_indices(VkBufferCreateInfo &create_info, uint32_t *sharing_indices);
bool allocate_image_memory(DeviceAllocation *allocation, const ImageCreateInfo &info,
VkImage image, VkImageTiling tiling, VkImageUsageFlags usage);
void promote_read_write_caches_to_read_only();
};
// A fairly complex helper used for async queue readbacks.
// Typically used for things like headless backend which emulates WSI through readbacks + encode.
struct OwnershipTransferInfo
{
CommandBuffer::Type old_queue;
CommandBuffer::Type new_queue;
VkImageLayout old_image_layout;
VkImageLayout new_image_layout;
VkPipelineStageFlags2 dst_pipeline_stage;
VkAccessFlags2 dst_access;
};
// For an image which was last accessed in old_queue, requests a command buffer
// for new_queue. Commands will be enqueued as necessary in new_queue to ensure that a complete ownership
// transfer has taken place.
// If queue family for old_queue differs from new_queue, a release barrier is enqueued in old_queue.
// In new_queue we perform either an acquire barrier or a simple pipeline barrier to change layout if required.
// If semaphore is a valid handle, it will be waited on in either old_queue to perform release barrier
// or new_queue depending on what is required.
// If the image uses CONCURRENT sharing mode, acquire/release barriers are skipped.
CommandBufferHandle request_command_buffer_with_ownership_transfer(
Device &device,
const Vulkan::Image &image,
const OwnershipTransferInfo &info,
const Vulkan::Semaphore &semaphore);
using DeviceHandle = Util::IntrusivePtr<Device>;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,70 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "device.hpp"
#include "thread_group.hpp"
namespace Vulkan
{
struct Device::RecorderState
{
RecorderState();
~RecorderState();
std::unique_ptr<Fossilize::DatabaseInterface> db;
Fossilize::StateRecorder recorder;
std::atomic_bool recorder_ready;
};
static constexpr unsigned NumTasks = 4;
struct Device::ReplayerState
{
ReplayerState();
~ReplayerState();
std::vector<Fossilize::Hash> module_hashes;
std::vector<Fossilize::Hash> graphics_hashes;
std::vector<Fossilize::Hash> compute_hashes;
Fossilize::StateReplayer base_replayer;
Fossilize::StateReplayer graphics_replayer;
Fossilize::StateReplayer compute_replayer;
Fossilize::FeatureFilter *feature_filter = nullptr;
std::unique_ptr<Fossilize::DatabaseInterface> db;
Granite::TaskGroupHandle complete;
Granite::TaskGroupHandle module_ready;
Granite::TaskGroupHandle pipeline_ready;
std::vector<std::pair<Fossilize::Hash, VkGraphicsPipelineCreateInfo *>> graphics_pipelines;
std::vector<std::pair<Fossilize::Hash, VkComputePipelineCreateInfo *>> compute_pipelines;
struct
{
std::atomic_uint32_t pipelines;
std::atomic_uint32_t modules;
std::atomic_uint32_t prepare;
uint32_t num_pipelines = 0;
uint32_t num_modules = 0;
} progress;
};
}
@@ -0,0 +1,72 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "event_manager.hpp"
#include "device.hpp"
namespace Vulkan
{
EventManager::~EventManager()
{
if (!workaround)
for (auto &event : events)
table->vkDestroyEvent(device->get_device(), event, nullptr);
}
void EventManager::recycle(VkEvent event)
{
if (!workaround && event != VK_NULL_HANDLE)
{
table->vkResetEvent(device->get_device(), event);
events.push_back(event);
}
}
VkEvent EventManager::request_cleared_event()
{
if (workaround)
{
// Can't use reinterpret_cast because of MSVC.
return (VkEvent) ++workaround_counter;
}
else if (events.empty())
{
VkEvent event;
VkEventCreateInfo info = { VK_STRUCTURE_TYPE_EVENT_CREATE_INFO };
table->vkCreateEvent(device->get_device(), &info, nullptr, &event);
return event;
}
else
{
auto event = events.back();
events.pop_back();
return event;
}
}
void EventManager::init(Device *device_)
{
device = device_;
table = &device->get_device_table();
workaround = device_->get_workarounds().emulate_event_as_pipeline_barrier;
}
}
@@ -0,0 +1,47 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_headers.hpp"
#include <vector>
namespace Vulkan
{
class Device;
class EventManager
{
public:
void init(Device *device);
~EventManager();
VkEvent request_cleared_event();
void recycle(VkEvent event);
private:
Device *device = nullptr;
const VolkDeviceTable *table = nullptr;
std::vector<VkEvent> events;
uint64_t workaround_counter = 0;
bool workaround = false;
};
}
@@ -0,0 +1,155 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "fence.hpp"
#include "device.hpp"
namespace Vulkan
{
FenceHolder::~FenceHolder()
{
if (fence != VK_NULL_HANDLE)
{
if (internal_sync)
device->reset_fence_nolock(fence, observed_wait);
else
device->reset_fence(fence, observed_wait);
}
}
const VkFence &FenceHolder::get_fence() const
{
return fence;
}
void FenceHolder::wait()
{
auto &table = device->get_device_table();
// Waiting for the same VkFence in parallel is not allowed, and there seems to be some shenanigans on Intel
// when waiting for a timeline semaphore in parallel with same value as well.
std::lock_guard<std::mutex> holder{lock};
if (observed_wait)
return;
if (timeline_value != 0)
{
VK_ASSERT(timeline_semaphore);
VkSemaphoreWaitInfo info = { VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO };
info.semaphoreCount = 1;
info.pSemaphores = &timeline_semaphore;
info.pValues = &timeline_value;
if (device->get_device_features().supports_post_mortem)
{
VkResult vr = table.vkWaitSemaphores(device->get_device(), &info, PostMortemTimeout);
if (vr == VK_TIMEOUT)
vr = table.vkWaitSemaphores(device->get_device(), &info, 0);
if (vr != VK_SUCCESS)
{
device->managers.breadcrumbs.notify_device_hung();
return;
}
}
if (table.vkWaitSemaphores(device->get_device(), &info, UINT64_MAX) != VK_SUCCESS)
{
LOGE("Failed to wait for timeline semaphore!\n");
device->managers.breadcrumbs.notify_device_hung();
}
else
observed_wait = true;
}
else
{
if (device->get_device_features().supports_post_mortem)
{
VkResult vr = table.vkWaitForFences(device->get_device(), 1, &fence, VK_TRUE, PostMortemTimeout);
if (vr == VK_TIMEOUT)
vr = table.vkWaitForFences(device->get_device(), 1, &fence, VK_TRUE, 0);
if (vr != VK_SUCCESS)
{
device->managers.breadcrumbs.notify_device_hung();
return;
}
}
if (table.vkWaitForFences(device->get_device(), 1, &fence, VK_TRUE, UINT64_MAX) != VK_SUCCESS)
{
LOGE("Failed to wait for fence!\n");
device->managers.breadcrumbs.notify_device_hung();
}
else
observed_wait = true;
}
}
bool FenceHolder::wait_timeout(uint64_t timeout)
{
bool ret;
auto &table = device->get_device_table();
// Waiting for the same VkFence in parallel is not allowed, and there seems to be some shenanigans on Intel
// when waiting for a timeline semaphore in parallel with same value as well.
std::lock_guard<std::mutex> holder{lock};
if (observed_wait)
return true;
if (timeline_value != 0)
{
VK_ASSERT(timeline_semaphore);
if (timeout == 0)
{
uint64_t current_value = 0;
ret = table.vkGetSemaphoreCounterValue(device->get_device(), timeline_semaphore, &current_value) == VK_SUCCESS &&
current_value >= timeline_value;
}
else
{
VkSemaphoreWaitInfo info = {VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO};
info.semaphoreCount = 1;
info.pSemaphores = &timeline_semaphore;
info.pValues = &timeline_value;
ret = table.vkWaitSemaphores(device->get_device(), &info, timeout) == VK_SUCCESS;
}
}
else
{
if (timeout == 0)
ret = table.vkGetFenceStatus(device->get_device(), fence) == VK_SUCCESS;
else
ret = table.vkWaitForFences(device->get_device(), 1, &fence, VK_TRUE, timeout) == VK_SUCCESS;
}
if (ret)
observed_wait = true;
return ret;
}
void FenceHolderDeleter::operator()(Vulkan::FenceHolder *fence)
{
fence->device->handle_pool.fences.free(fence);
}
}
@@ -0,0 +1,81 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_common.hpp"
#include "vulkan_headers.hpp"
#include "object_pool.hpp"
#include "cookie.hpp"
#include <mutex>
namespace Vulkan
{
class Device;
class FenceHolder;
struct FenceHolderDeleter
{
void operator()(FenceHolder *fence);
};
class FenceHolder : public Util::IntrusivePtrEnabled<FenceHolder, FenceHolderDeleter, HandleCounter>, public InternalSyncEnabled
{
public:
friend struct FenceHolderDeleter;
friend class WSI;
~FenceHolder();
void wait();
bool wait_timeout(uint64_t nsec);
private:
friend class Util::ObjectPool<FenceHolder>;
FenceHolder(Device *device_, VkFence fence_)
: device(device_),
fence(fence_),
timeline_semaphore(VK_NULL_HANDLE),
timeline_value(0)
{
}
FenceHolder(Device *device_, uint64_t value, VkSemaphore timeline_semaphore_)
: device(device_),
fence(VK_NULL_HANDLE),
timeline_semaphore(timeline_semaphore_),
timeline_value(value)
{
VK_ASSERT(value > 0);
}
const VkFence &get_fence() const;
Device *device;
VkFence fence;
VkSemaphore timeline_semaphore;
uint64_t timeline_value;
bool observed_wait = false;
std::mutex lock;
};
using Fence = Util::IntrusivePtr<FenceHolder>;
}
@@ -0,0 +1,61 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "fence_manager.hpp"
#include "device.hpp"
namespace Vulkan
{
void FenceManager::init(Device *device_)
{
device = device_;
table = &device->get_device_table();
}
VkFence FenceManager::request_cleared_fence()
{
if (!fences.empty())
{
auto ret = fences.back();
fences.pop_back();
return ret;
}
else
{
VkFence fence;
VkFenceCreateInfo info = { VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
table->vkCreateFence(device->get_device(), &info, nullptr, &fence);
return fence;
}
}
void FenceManager::recycle_fence(VkFence fence)
{
fences.push_back(fence);
}
FenceManager::~FenceManager()
{
for (auto &fence : fences)
table->vkDestroyFence(device->get_device(), fence, nullptr);
}
}
@@ -0,0 +1,45 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_headers.hpp"
#include <vector>
namespace Vulkan
{
class Device;
class FenceManager
{
public:
void init(Device *device);
~FenceManager();
VkFence request_cleared_fence();
void recycle_fence(VkFence fence);
private:
Device *device = nullptr;
const VolkDeviceTable *table = nullptr;
std::vector<VkFence> fences;
};
}
@@ -0,0 +1,386 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_headers.hpp"
#include "texture/texture_format.hpp"
namespace Vulkan
{
enum class FormatCompressionType
{
Uncompressed,
BC,
ETC,
ASTC
};
static inline FormatCompressionType format_compression_type(VkFormat format)
{
switch (format)
{
case VK_FORMAT_BC1_RGBA_SRGB_BLOCK:
case VK_FORMAT_BC1_RGBA_UNORM_BLOCK:
case VK_FORMAT_BC1_RGB_SRGB_BLOCK:
case VK_FORMAT_BC1_RGB_UNORM_BLOCK:
case VK_FORMAT_BC2_SRGB_BLOCK:
case VK_FORMAT_BC2_UNORM_BLOCK:
case VK_FORMAT_BC3_SRGB_BLOCK:
case VK_FORMAT_BC3_UNORM_BLOCK:
case VK_FORMAT_BC4_UNORM_BLOCK:
case VK_FORMAT_BC4_SNORM_BLOCK:
case VK_FORMAT_BC5_UNORM_BLOCK:
case VK_FORMAT_BC5_SNORM_BLOCK:
case VK_FORMAT_BC6H_SFLOAT_BLOCK:
case VK_FORMAT_BC6H_UFLOAT_BLOCK:
case VK_FORMAT_BC7_SRGB_BLOCK:
case VK_FORMAT_BC7_UNORM_BLOCK:
return FormatCompressionType::BC;
case VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK:
case VK_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCK:
case VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK:
case VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK:
case VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK:
case VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK:
case VK_FORMAT_EAC_R11G11_SNORM_BLOCK:
case VK_FORMAT_EAC_R11G11_UNORM_BLOCK:
case VK_FORMAT_EAC_R11_SNORM_BLOCK:
case VK_FORMAT_EAC_R11_UNORM_BLOCK:
return FormatCompressionType::ETC;
#define astc_fmt(w, h) \
case VK_FORMAT_ASTC_##w##x##h##_UNORM_BLOCK: \
case VK_FORMAT_ASTC_##w##x##h##_SRGB_BLOCK: \
case VK_FORMAT_ASTC_##w##x##h##_SFLOAT_BLOCK_EXT
astc_fmt(4, 4):
astc_fmt(5, 4):
astc_fmt(5, 5):
astc_fmt(6, 5):
astc_fmt(6, 6):
astc_fmt(8, 5):
astc_fmt(8, 6):
astc_fmt(8, 8):
astc_fmt(10, 5):
astc_fmt(10, 6):
astc_fmt(10, 8):
astc_fmt(10, 10):
astc_fmt(12, 10):
astc_fmt(12, 12):
return FormatCompressionType::ASTC;
#undef astc_fmt
default:
return FormatCompressionType::Uncompressed;
}
}
static inline bool format_is_compressed_hdr(VkFormat format)
{
switch (format)
{
#define astc_fmt(w, h) case VK_FORMAT_ASTC_##w##x##h##_SFLOAT_BLOCK_EXT
astc_fmt(4, 4):
astc_fmt(5, 4):
astc_fmt(5, 5):
astc_fmt(6, 5):
astc_fmt(6, 6):
astc_fmt(8, 5):
astc_fmt(8, 6):
astc_fmt(8, 8):
astc_fmt(10, 5):
astc_fmt(10, 6):
astc_fmt(10, 8):
astc_fmt(10, 10):
astc_fmt(12, 10):
astc_fmt(12, 12):
#undef astc_fmt
return true;
case VK_FORMAT_BC6H_SFLOAT_BLOCK:
case VK_FORMAT_BC6H_UFLOAT_BLOCK:
return true;
default:
return false;
}
}
static inline bool format_is_srgb(VkFormat format)
{
switch (format)
{
case VK_FORMAT_A8B8G8R8_SRGB_PACK32:
case VK_FORMAT_R8G8B8A8_SRGB:
case VK_FORMAT_B8G8R8A8_SRGB:
case VK_FORMAT_R8_SRGB:
case VK_FORMAT_R8G8_SRGB:
case VK_FORMAT_R8G8B8_SRGB:
case VK_FORMAT_B8G8R8_SRGB:
case VK_FORMAT_BC1_RGB_SRGB_BLOCK:
case VK_FORMAT_BC1_RGBA_SRGB_BLOCK:
case VK_FORMAT_BC2_SRGB_BLOCK:
case VK_FORMAT_BC3_SRGB_BLOCK:
case VK_FORMAT_BC7_SRGB_BLOCK:
case VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK:
case VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK:
case VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK:
case VK_FORMAT_ASTC_4x4_SRGB_BLOCK:
case VK_FORMAT_ASTC_5x4_SRGB_BLOCK:
case VK_FORMAT_ASTC_5x5_SRGB_BLOCK:
case VK_FORMAT_ASTC_6x5_SRGB_BLOCK:
case VK_FORMAT_ASTC_6x6_SRGB_BLOCK:
case VK_FORMAT_ASTC_8x5_SRGB_BLOCK:
case VK_FORMAT_ASTC_8x6_SRGB_BLOCK:
case VK_FORMAT_ASTC_8x8_SRGB_BLOCK:
case VK_FORMAT_ASTC_10x5_SRGB_BLOCK:
case VK_FORMAT_ASTC_10x6_SRGB_BLOCK:
case VK_FORMAT_ASTC_10x8_SRGB_BLOCK:
case VK_FORMAT_ASTC_10x10_SRGB_BLOCK:
case VK_FORMAT_ASTC_12x10_SRGB_BLOCK:
case VK_FORMAT_ASTC_12x12_SRGB_BLOCK:
return true;
default:
return false;
}
}
static inline bool format_has_depth_aspect(VkFormat format)
{
switch (format)
{
case VK_FORMAT_D16_UNORM:
case VK_FORMAT_D16_UNORM_S8_UINT:
case VK_FORMAT_D24_UNORM_S8_UINT:
case VK_FORMAT_D32_SFLOAT:
case VK_FORMAT_X8_D24_UNORM_PACK32:
case VK_FORMAT_D32_SFLOAT_S8_UINT:
return true;
default:
return false;
}
}
static inline bool format_has_stencil_aspect(VkFormat format)
{
switch (format)
{
case VK_FORMAT_D16_UNORM_S8_UINT:
case VK_FORMAT_D24_UNORM_S8_UINT:
case VK_FORMAT_D32_SFLOAT_S8_UINT:
case VK_FORMAT_S8_UINT:
return true;
default:
return false;
}
}
static inline bool format_has_depth_or_stencil_aspect(VkFormat format)
{
return format_has_depth_aspect(format) || format_has_stencil_aspect(format);
}
static inline VkImageAspectFlags format_to_aspect_mask(VkFormat format)
{
switch (format)
{
case VK_FORMAT_UNDEFINED:
return 0;
case VK_FORMAT_S8_UINT:
return VK_IMAGE_ASPECT_STENCIL_BIT;
case VK_FORMAT_D16_UNORM_S8_UINT:
case VK_FORMAT_D24_UNORM_S8_UINT:
case VK_FORMAT_D32_SFLOAT_S8_UINT:
return VK_IMAGE_ASPECT_STENCIL_BIT | VK_IMAGE_ASPECT_DEPTH_BIT;
case VK_FORMAT_D16_UNORM:
case VK_FORMAT_D32_SFLOAT:
case VK_FORMAT_X8_D24_UNORM_PACK32:
return VK_IMAGE_ASPECT_DEPTH_BIT;
default:
return VK_IMAGE_ASPECT_COLOR_BIT;
}
}
static inline void format_align_dim(VkFormat format, uint32_t &width, uint32_t &height)
{
uint32_t align_width, align_height;
TextureFormatLayout::format_block_dim(format, align_width, align_height);
width = ((width + align_width - 1) / align_width) * align_width;
height = ((height + align_height - 1) / align_height) * align_height;
}
static inline void format_num_blocks(VkFormat format, uint32_t &width, uint32_t &height)
{
uint32_t align_width, align_height;
TextureFormatLayout::format_block_dim(format, align_width, align_height);
width = (width + align_width - 1) / align_width;
height = (height + align_height - 1) / align_height;
}
static inline VkDeviceSize format_get_layer_size(VkFormat format, VkImageAspectFlags aspect, unsigned width, unsigned height, unsigned depth)
{
uint32_t blocks_x = width;
uint32_t blocks_y = height;
format_num_blocks(format, blocks_x, blocks_y);
format_align_dim(format, width, height);
VkDeviceSize size = VkDeviceSize(TextureFormatLayout::format_block_size(format, aspect)) * depth * blocks_x * blocks_y;
return size;
}
static inline unsigned format_ycbcr_num_planes(VkFormat format)
{
switch (format)
{
case VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM:
case VK_FORMAT_G8_B8_R8_3PLANE_422_UNORM:
case VK_FORMAT_G8_B8_R8_3PLANE_444_UNORM:
case VK_FORMAT_G16_B16_R16_3PLANE_420_UNORM:
case VK_FORMAT_G16_B16_R16_3PLANE_422_UNORM:
case VK_FORMAT_G16_B16_R16_3PLANE_444_UNORM:
case VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_420_UNORM_3PACK16:
case VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_422_UNORM_3PACK16:
case VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_444_UNORM_3PACK16:
case VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_420_UNORM_3PACK16:
case VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_422_UNORM_3PACK16:
case VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_444_UNORM_3PACK16:
return 3;
case VK_FORMAT_G8_B8R8_2PLANE_420_UNORM:
case VK_FORMAT_G8_B8R8_2PLANE_422_UNORM:
case VK_FORMAT_G16_B16R16_2PLANE_420_UNORM:
case VK_FORMAT_G16_B16R16_2PLANE_422_UNORM:
case VK_FORMAT_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16:
case VK_FORMAT_G10X6_B10X6R10X6_2PLANE_422_UNORM_3PACK16:
case VK_FORMAT_G12X4_B12X4R12X4_2PLANE_420_UNORM_3PACK16:
case VK_FORMAT_G12X4_B12X4R12X4_2PLANE_422_UNORM_3PACK16:
return 2;
default:
return 1;
}
}
static inline void format_ycbcr_downsample_dimensions(VkFormat format, VkImageAspectFlags aspect, uint32_t &width, uint32_t &height)
{
if (aspect == VK_IMAGE_ASPECT_PLANE_0_BIT)
return;
switch (format)
{
#define fmt(x, sub0, sub1) \
case VK_FORMAT_##x: \
width >>= sub0; \
height >>= sub1; \
break
fmt(G8_B8_R8_3PLANE_420_UNORM, 1, 1);
fmt(G8_B8R8_2PLANE_420_UNORM, 1, 1);
fmt(G8_B8_R8_3PLANE_422_UNORM, 1, 0);
fmt(G8_B8R8_2PLANE_422_UNORM, 1, 0);
fmt(G8_B8_R8_3PLANE_444_UNORM, 0, 0);
fmt(G10X6_B10X6_R10X6_3PLANE_420_UNORM_3PACK16, 1, 1);
fmt(G10X6_B10X6_R10X6_3PLANE_422_UNORM_3PACK16, 1, 0);
fmt(G10X6_B10X6_R10X6_3PLANE_444_UNORM_3PACK16, 0, 0);
fmt(G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16, 1, 1);
fmt(G10X6_B10X6R10X6_2PLANE_422_UNORM_3PACK16, 1, 0);
fmt(G12X4_B12X4_R12X4_3PLANE_420_UNORM_3PACK16, 1, 1);
fmt(G12X4_B12X4_R12X4_3PLANE_422_UNORM_3PACK16, 1, 0);
fmt(G12X4_B12X4_R12X4_3PLANE_444_UNORM_3PACK16, 0, 0);
fmt(G12X4_B12X4R12X4_2PLANE_420_UNORM_3PACK16, 1, 1);
fmt(G12X4_B12X4R12X4_2PLANE_422_UNORM_3PACK16, 1, 0);
fmt(G16_B16_R16_3PLANE_420_UNORM, 1, 1);
fmt(G16_B16_R16_3PLANE_422_UNORM, 1, 0);
fmt(G16_B16_R16_3PLANE_444_UNORM, 0, 0);
fmt(G16_B16R16_2PLANE_420_UNORM, 1, 1);
fmt(G16_B16R16_2PLANE_422_UNORM, 1, 0);
default:
break;
}
#undef fmt
}
static inline bool format_supports_storage_image_read_write_without_format(VkFormat format)
{
/* from https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#formats-without-shader-storage-format */
static const VkFormat supported_formats[] =
{
VK_FORMAT_R8G8B8A8_UNORM,
VK_FORMAT_R8G8B8A8_SNORM,
VK_FORMAT_R8G8B8A8_UINT,
VK_FORMAT_R8G8B8A8_SINT,
VK_FORMAT_R32_UINT,
VK_FORMAT_R32_SINT,
VK_FORMAT_R32_SFLOAT,
VK_FORMAT_R32G32_UINT,
VK_FORMAT_R32G32_SINT,
VK_FORMAT_R32G32_SFLOAT,
VK_FORMAT_R32G32B32A32_UINT,
VK_FORMAT_R32G32B32A32_SINT,
VK_FORMAT_R32G32B32A32_SFLOAT,
VK_FORMAT_R16G16B16A16_UINT,
VK_FORMAT_R16G16B16A16_SINT,
VK_FORMAT_R16G16B16A16_SFLOAT,
VK_FORMAT_R16G16_SFLOAT,
VK_FORMAT_B10G11R11_UFLOAT_PACK32,
VK_FORMAT_R16_SFLOAT,
VK_FORMAT_R16G16B16A16_UNORM,
VK_FORMAT_A2B10G10R10_UNORM_PACK32,
VK_FORMAT_R16G16_UNORM,
VK_FORMAT_R8G8_UNORM,
VK_FORMAT_R16_UNORM,
VK_FORMAT_R8_UNORM,
VK_FORMAT_R16G16B16A16_SNORM,
VK_FORMAT_R16G16_SNORM,
VK_FORMAT_R8G8_SNORM,
VK_FORMAT_R16_SNORM,
VK_FORMAT_R8_SNORM,
VK_FORMAT_R16G16_SINT,
VK_FORMAT_R8G8_SINT,
VK_FORMAT_R16_SINT,
VK_FORMAT_R8_SINT,
VK_FORMAT_A2B10G10R10_UINT_PACK32,
VK_FORMAT_R16G16_UINT,
VK_FORMAT_R8G8_UINT,
VK_FORMAT_R16_UINT,
VK_FORMAT_R8_UINT,
};
for (auto fmt : supported_formats)
if (fmt == format)
return true;
return false;
}
}
@@ -0,0 +1,260 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "image.hpp"
#include "device.hpp"
#include "buffer.hpp"
namespace Vulkan
{
ImageView::ImageView(Device *device_, const CachedImageView &view_, const ImageViewCreateInfo &info_)
: Cookie(device_)
, device(device_)
, view(view_)
, info(info_)
{
}
const CachedImageView &ImageView::get_render_target_view(unsigned layer) const
{
// Transient images just have one layer.
if (info.image->get_create_info().domain == ImageDomain::Transient)
return view;
VK_ASSERT(layer < get_create_info().layers);
if (render_target_views.empty())
return view;
else
{
VK_ASSERT(layer < render_target_views.size());
return render_target_views[layer];
}
}
const CachedImageView &ImageView::get_mip_view(unsigned level) const
{
VK_ASSERT(level < get_create_info().levels);
if (mip_views.empty())
return view;
else
{
VK_ASSERT(level < mip_views.size());
return mip_views[level];
}
}
void ImageView::free_cached_view(CachedImageView &cached)
{
if (internal_sync)
device->destroy_image_view_nolock(cached);
else
device->destroy_image_view(cached);
}
ImageView::~ImageView()
{
free_cached_view(view);
free_cached_view(depth_view);
free_cached_view(stencil_view);
free_cached_view(unorm_view);
free_cached_view(srgb_view);
for (auto &v : render_target_views)
free_cached_view(v);
for (auto &v : mip_views)
free_cached_view(v);
}
unsigned ImageView::get_view_width() const
{
unsigned width = info.image->get_width(info.base_level);
if (info.aspect == VK_IMAGE_ASPECT_PLANE_1_BIT || info.aspect == VK_IMAGE_ASPECT_PLANE_2_BIT)
{
unsigned h = 0;
format_ycbcr_downsample_dimensions(info.image->get_format(), info.aspect, width, h);
}
return width;
}
unsigned ImageView::get_view_height() const
{
unsigned height = info.image->get_height(info.base_level);
if (info.aspect == VK_IMAGE_ASPECT_PLANE_1_BIT || info.aspect == VK_IMAGE_ASPECT_PLANE_2_BIT)
{
unsigned w = 0;
format_ycbcr_downsample_dimensions(info.image->get_format(), info.aspect, w, height);
}
return height;
}
unsigned ImageView::get_view_depth() const
{
return info.image->get_depth(info.base_level);
}
Image::Image(Device *device_, VkImage image_, const CachedImageView &default_view, const DeviceAllocation &alloc_,
const ImageCreateInfo &create_info_, VkImageViewType view_type)
: Cookie(device_)
, device(device_)
, image(image_)
, alloc(alloc_)
, create_info(create_info_)
{
if (view_type != VK_IMAGE_VIEW_TYPE_MAX_ENUM)
{
ImageViewCreateInfo info;
info.image = this;
info.view_type = view_type;
info.format = create_info.format;
info.base_level = 0;
info.levels = create_info.levels;
info.base_layer = 0;
info.layers = create_info.layers;
view = ImageViewHandle(device->handle_pool.image_views.allocate(device, default_view, info));
}
}
DeviceAllocation Image::take_allocation_ownership()
{
DeviceAllocation ret = {};
std::swap(ret, alloc);
return ret;
}
ExternalHandle Image::export_handle()
{
return alloc.export_handle(*device);
}
void Image::disown_image()
{
owns_image = false;
}
void Image::disown_memory_allocation()
{
owns_memory_allocation = false;
}
Image::~Image()
{
if (owns_image)
{
if (internal_sync)
device->destroy_image_nolock(image);
else
device->destroy_image(image);
}
if (alloc.get_memory() && owns_memory_allocation)
{
if (internal_sync)
device->free_memory_nolock(alloc);
else
device->free_memory(alloc);
}
}
const Buffer &LinearHostImage::get_host_visible_buffer() const
{
return *cpu_image;
}
bool LinearHostImage::need_staging_copy() const
{
return gpu_image->get_create_info().domain != ImageDomain::LinearHostCached &&
gpu_image->get_create_info().domain != ImageDomain::LinearHost;
}
const DeviceAllocation &LinearHostImage::get_host_visible_allocation() const
{
return need_staging_copy() ? cpu_image->get_allocation() : gpu_image->get_allocation();
}
const ImageView &LinearHostImage::get_view() const
{
return gpu_image->get_view();
}
const Image &LinearHostImage::get_image() const
{
return *gpu_image;
}
size_t LinearHostImage::get_offset() const
{
return row_offset;
}
size_t LinearHostImage::get_row_pitch_bytes() const
{
return row_pitch;
}
VkPipelineStageFlags2 LinearHostImage::get_used_pipeline_stages() const
{
return stages;
}
LinearHostImage::LinearHostImage(Device *device_, ImageHandle gpu_image_, BufferHandle cpu_image_, VkPipelineStageFlags2 stages_)
: device(device_), gpu_image(std::move(gpu_image_)), cpu_image(std::move(cpu_image_)), stages(stages_)
{
if (gpu_image->get_create_info().domain == ImageDomain::LinearHostCached ||
gpu_image->get_create_info().domain == ImageDomain::LinearHost)
{
VkImageSubresource sub = {};
sub.aspectMask = format_to_aspect_mask(gpu_image->get_format());
VkSubresourceLayout layout;
auto &table = device_->get_device_table();
table.vkGetImageSubresourceLayout(device->get_device(), gpu_image->get_image(), &sub, &layout);
row_pitch = layout.rowPitch;
row_offset = layout.offset;
}
else
{
row_pitch = gpu_image->get_width() * TextureFormatLayout::format_block_size(gpu_image->get_format(),
format_to_aspect_mask(gpu_image->get_format()));
row_offset = 0;
}
}
void ImageViewDeleter::operator()(ImageView *view)
{
view->device->handle_pool.image_views.free(view);
}
void ImageDeleter::operator()(Image *image)
{
image->device->handle_pool.images.free(image);
}
void LinearHostImageDeleter::operator()(LinearHostImage *image)
{
image->device->handle_pool.linear_images.free(image);
}
}
@@ -0,0 +1,582 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "cookie.hpp"
#include "format.hpp"
#include "vulkan_common.hpp"
#include "memory_allocator.hpp"
#include "vulkan_headers.hpp"
#include <algorithm>
namespace Vulkan
{
class Device;
static inline uint32_t image_num_miplevels(const VkExtent3D &extent)
{
uint32_t size = std::max<uint32_t>(std::max<uint32_t>(extent.width, extent.height), extent.depth);
return Util::floor_log2(size) + 1;
}
static inline VkFormatFeatureFlags image_usage_to_features(VkImageUsageFlags usage)
{
VkFormatFeatureFlags flags = 0;
if (usage & VK_IMAGE_USAGE_SAMPLED_BIT)
flags |= VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
if (usage & VK_IMAGE_USAGE_STORAGE_BIT)
flags |= VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT;
if (usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)
flags |= VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT;
if (usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)
flags |= VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT;
return flags;
}
struct ImageInitialData
{
const void *data;
unsigned row_length;
unsigned image_height;
};
enum ImageMiscFlagBits
{
IMAGE_MISC_GENERATE_MIPS_BIT = 1 << 0,
IMAGE_MISC_FORCE_ARRAY_BIT = 1 << 1,
IMAGE_MISC_MUTABLE_SRGB_BIT = 1 << 2,
IMAGE_MISC_CONCURRENT_QUEUE_GRAPHICS_BIT = 1 << 3,
IMAGE_MISC_CONCURRENT_QUEUE_ASYNC_COMPUTE_BIT = 1 << 4,
IMAGE_MISC_CONCURRENT_QUEUE_ASYNC_TRANSFER_BIT = 1 << 6,
IMAGE_MISC_CONCURRENT_QUEUE_VIDEO_DECODE_BIT = 1 << 7,
IMAGE_MISC_VERIFY_FORMAT_FEATURE_SAMPLED_LINEAR_FILTER_BIT = 1 << 8,
IMAGE_MISC_LINEAR_IMAGE_IGNORE_DEVICE_LOCAL_BIT = 1 << 9,
IMAGE_MISC_FORCE_NO_DEDICATED_BIT = 1 << 10,
IMAGE_MISC_NO_DEFAULT_VIEWS_BIT = 1 << 11,
IMAGE_MISC_EXTERNAL_MEMORY_BIT = 1 << 12,
IMAGE_MISC_CONCURRENT_QUEUE_VIDEO_ENCODE_BIT = 1 << 13,
IMAGE_MISC_CONCURRENT_QUEUE_VIDEO_DUPLEX =
IMAGE_MISC_CONCURRENT_QUEUE_VIDEO_DECODE_BIT |
IMAGE_MISC_CONCURRENT_QUEUE_VIDEO_ENCODE_BIT,
IMAGE_MISC_CREATE_PER_MIP_LEVEL_VIEWS_BIT = 1 << 14
};
using ImageMiscFlags = uint32_t;
enum ImageViewMiscFlagBits
{
IMAGE_VIEW_MISC_FORCE_ARRAY_BIT = 1 << 0
};
using ImageViewMiscFlags = uint32_t;
class Image;
class ImmutableYcbcrConversion;
struct ImageViewCreateInfo
{
const Image *image = nullptr;
VkFormat format = VK_FORMAT_UNDEFINED;
unsigned base_level = 0;
unsigned levels = VK_REMAINING_MIP_LEVELS;
unsigned base_layer = 0;
unsigned layers = VK_REMAINING_ARRAY_LAYERS;
VkImageViewType view_type = VK_IMAGE_VIEW_TYPE_MAX_ENUM;
ImageViewMiscFlags misc = 0;
VkComponentMapping swizzle = {
VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
};
VkImageAspectFlags aspect = 0;
const ImmutableYcbcrConversion *ycbcr_conversion = nullptr;
};
class ImageView;
struct ImageViewDeleter
{
void operator()(ImageView *view);
};
class ImageView : public Util::IntrusivePtrEnabled<ImageView, ImageViewDeleter, HandleCounter>,
public Cookie, public InternalSyncEnabled
{
public:
friend struct ImageViewDeleter;
ImageView(Device *device, const CachedImageView &view, const ImageViewCreateInfo &info);
~ImageView();
void set_separate_depth_stencil_views(const CachedImageView &depth, const CachedImageView &stencil)
{
VK_ASSERT(depth_view.view == VK_NULL_HANDLE);
VK_ASSERT(stencil_view.view == VK_NULL_HANDLE);
depth_view = depth;
stencil_view = stencil;
}
void set_render_target_views(std::vector<CachedImageView> views)
{
VK_ASSERT(render_target_views.empty());
render_target_views = std::move(views);
}
void set_mip_views(std::vector<CachedImageView> views)
{
VK_ASSERT(mip_views.empty());
mip_views = std::move(views);
}
void set_unorm_view(const CachedImageView &view_)
{
VK_ASSERT(unorm_view.view == VK_NULL_HANDLE);
unorm_view = view_;
}
void set_srgb_view(const CachedImageView &view_)
{
VK_ASSERT(srgb_view.view == VK_NULL_HANDLE);
srgb_view = view_;
}
// By default, gets a combined view which includes all aspects in the image.
// This would be used mostly for render targets.
const CachedImageView &get_view() const
{
return view;
}
const CachedImageView &get_render_target_view(unsigned layer) const;
const CachedImageView &get_mip_view(unsigned level) const;
// Gets an image view which only includes floating point domains.
// Takes effect when we want to sample from an image which is Depth/Stencil,
// but we only want to sample depth.
const CachedImageView &get_float_view() const
{
return depth_view.view != VK_NULL_HANDLE ? depth_view : view;
}
// Gets an image view which only includes integer domains.
// Takes effect when we want to sample from an image which is Depth/Stencil,
// but we only want to sample stencil.
const CachedImageView &get_integer_view() const
{
return stencil_view.view != VK_NULL_HANDLE ? stencil_view : view;
}
const CachedImageView &get_unorm_view() const
{
return unorm_view.view != VK_NULL_HANDLE ? unorm_view : view;
}
const CachedImageView &get_srgb_view() const
{
return srgb_view.view != VK_NULL_HANDLE ? srgb_view : view;
}
VkFormat get_format() const
{
return info.format;
}
const Image &get_image() const
{
return *info.image;
}
const ImageViewCreateInfo &get_create_info() const
{
return info;
}
unsigned get_view_width() const;
unsigned get_view_height() const;
unsigned get_view_depth() const;
private:
Device *device;
CachedImageView view = {};
std::vector<CachedImageView> render_target_views;
std::vector<CachedImageView> mip_views;
CachedImageView depth_view = {};
CachedImageView stencil_view = {};
CachedImageView unorm_view = {};
CachedImageView srgb_view = {};
ImageViewCreateInfo info;
void free_cached_view(CachedImageView &cached);
};
using ImageViewHandle = Util::IntrusivePtr<ImageView>;
enum class ImageDomain
{
Physical,
Transient,
LinearHostCached,
LinearHost,
LinearDevice,
HostCopy
};
enum class ImageLayout
{
Optimal,
General
};
struct ImageCreateInfo
{
ImageDomain domain = ImageDomain::Physical;
unsigned width = 0;
unsigned height = 0;
unsigned depth = 1;
unsigned levels = 1;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageType type = VK_IMAGE_TYPE_2D;
unsigned layers = 1;
VkImageUsageFlags usage = 0;
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
VkImageCreateFlags flags = 0;
ImageMiscFlags misc = 0;
VkImageLayout initial_layout = VK_IMAGE_LAYOUT_GENERAL;
VkComponentMapping swizzle = {
VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
};
const DeviceAllocation **memory_aliases = nullptr;
unsigned num_memory_aliases = 0;
const ImmutableYcbcrConversion *ycbcr_conversion = nullptr;
void *pnext = nullptr;
ExternalHandle external;
ImageLayout layout = ImageLayout::Optimal;
static ImageCreateInfo immutable_image(const TextureFormatLayout &layout)
{
Vulkan::ImageCreateInfo info;
info.width = layout.get_width();
info.height = layout.get_height();
info.type = layout.get_image_type();
info.depth = layout.get_depth();
info.format = layout.get_format();
info.layers = layout.get_layers();
info.levels = layout.get_levels();
info.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
info.initial_layout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL;
info.samples = VK_SAMPLE_COUNT_1_BIT;
info.domain = ImageDomain::Physical;
return info;
}
static ImageCreateInfo immutable_2d_image(unsigned width, unsigned height, VkFormat format, bool mipmapped = false)
{
ImageCreateInfo info;
info.width = width;
info.height = height;
info.depth = 1;
info.levels = mipmapped ? 0u : 1u;
info.format = format;
info.type = VK_IMAGE_TYPE_2D;
info.layers = 1;
info.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
info.samples = VK_SAMPLE_COUNT_1_BIT;
info.flags = 0;
info.misc = mipmapped ? unsigned(IMAGE_MISC_GENERATE_MIPS_BIT) : 0u;
info.initial_layout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL;
return info;
}
static ImageCreateInfo
immutable_3d_image(unsigned width, unsigned height, unsigned depth, VkFormat format, bool mipmapped = false)
{
ImageCreateInfo info = immutable_2d_image(width, height, format, mipmapped);
info.depth = depth;
info.type = VK_IMAGE_TYPE_3D;
return info;
}
static ImageCreateInfo render_target(unsigned width, unsigned height, VkFormat format)
{
ImageCreateInfo info;
info.width = width;
info.height = height;
info.depth = 1;
info.levels = 1;
info.format = format;
info.type = VK_IMAGE_TYPE_2D;
info.layers = 1;
info.usage = (format_has_depth_or_stencil_aspect(format) ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
info.samples = VK_SAMPLE_COUNT_1_BIT;
info.flags = 0;
info.misc = 0;
info.initial_layout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL;
return info;
}
static ImageCreateInfo transient_render_target(unsigned width, unsigned height, VkFormat format)
{
ImageCreateInfo info;
info.domain = ImageDomain::Transient;
info.width = width;
info.height = height;
info.depth = 1;
info.levels = 1;
info.format = format;
info.type = VK_IMAGE_TYPE_2D;
info.layers = 1;
info.usage = (format_has_depth_or_stencil_aspect(format) ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) |
VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT;
info.samples = VK_SAMPLE_COUNT_1_BIT;
info.flags = 0;
info.misc = 0;
info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
return info;
}
static uint32_t compute_view_formats(const ImageCreateInfo &info, VkFormat *formats)
{
if ((info.misc & IMAGE_MISC_MUTABLE_SRGB_BIT) == 0)
return 0;
switch (info.format)
{
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_R8G8B8A8_SRGB:
formats[0] = VK_FORMAT_R8G8B8A8_UNORM;
formats[1] = VK_FORMAT_R8G8B8A8_SRGB;
return 2;
case VK_FORMAT_B8G8R8A8_UNORM:
case VK_FORMAT_B8G8R8A8_SRGB:
formats[0] = VK_FORMAT_B8G8R8A8_UNORM;
formats[1] = VK_FORMAT_B8G8R8A8_SRGB;
return 2;
case VK_FORMAT_A8B8G8R8_UNORM_PACK32:
case VK_FORMAT_A8B8G8R8_SRGB_PACK32:
formats[0] = VK_FORMAT_A8B8G8R8_UNORM_PACK32;
formats[1] = VK_FORMAT_A8B8G8R8_SRGB_PACK32;
return 2;
default:
return 0;
}
}
};
class Image;
struct ImageDeleter
{
void operator()(Image *image);
};
class Image : public Util::IntrusivePtrEnabled<Image, ImageDeleter, HandleCounter>,
public Cookie, public InternalSyncEnabled
{
public:
friend struct ImageDeleter;
~Image();
Image(Image &&) = delete;
Image &operator=(Image &&) = delete;
const ImageView &get_view() const
{
VK_ASSERT(view);
return *view;
}
ImageView &get_view()
{
VK_ASSERT(view);
return *view;
}
VkImage get_image() const
{
return image;
}
VkFormat get_format() const
{
return create_info.format;
}
uint32_t get_width(uint32_t lod = 0) const
{
return std::max<uint32_t>(1u, create_info.width >> lod);
}
uint32_t get_height(uint32_t lod = 0) const
{
return std::max<uint32_t>(1u, create_info.height >> lod);
}
uint32_t get_depth(uint32_t lod = 0) const
{
return std::max<uint32_t>(1u, create_info.depth >> lod);
}
const ImageCreateInfo &get_create_info() const
{
return create_info;
}
VkImageLayout get_layout(VkImageLayout optimal) const
{
return create_info.layout == ImageLayout::Optimal ? optimal : VK_IMAGE_LAYOUT_GENERAL;
}
bool is_swapchain_image() const
{
return swapchain_layout != VK_IMAGE_LAYOUT_UNDEFINED;
}
VkImageLayout get_swapchain_layout() const
{
return swapchain_layout;
}
void set_swapchain_layout(VkImageLayout layout)
{
swapchain_layout = layout;
}
const DeviceAllocation &get_allocation() const
{
return alloc;
}
void disown_image();
void disown_memory_allocation();
DeviceAllocation take_allocation_ownership();
void set_surface_transform(VkSurfaceTransformFlagBitsKHR transform)
{
surface_transform = transform;
if (transform != VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)
{
const VkImageUsageFlags safe_usage_flags =
VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT |
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT |
VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT;
if ((create_info.usage & ~safe_usage_flags) != 0)
{
LOGW("Using surface transform for non-pure render target image (usage: %u). This can lead to weird results.\n",
create_info.usage);
}
}
}
VkSurfaceTransformFlagBitsKHR get_surface_transform() const
{
return surface_transform;
}
ExternalHandle export_handle();
private:
friend class Util::ObjectPool<Image>;
Image(Device *device, VkImage image, const CachedImageView &default_view, const DeviceAllocation &alloc,
const ImageCreateInfo &info, VkImageViewType view_type);
Device *device;
VkImage image;
ImageViewHandle view;
DeviceAllocation alloc;
ImageCreateInfo create_info;
VkImageLayout swapchain_layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkSurfaceTransformFlagBitsKHR surface_transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
bool owns_image = true;
bool owns_memory_allocation = true;
};
using ImageHandle = Util::IntrusivePtr<Image>;
class LinearHostImage;
struct LinearHostImageDeleter
{
void operator()(LinearHostImage *image);
};
class Buffer;
enum LinearHostImageCreateInfoFlagBits
{
LINEAR_HOST_IMAGE_HOST_CACHED_BIT = 1 << 0,
LINEAR_HOST_IMAGE_REQUIRE_LINEAR_FILTER_BIT = 1 << 1,
LINEAR_HOST_IMAGE_IGNORE_DEVICE_LOCAL_BIT = 1 << 2
};
using LinearHostImageCreateInfoFlags = uint32_t;
struct LinearHostImageCreateInfo
{
unsigned width = 0;
unsigned height = 0;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageUsageFlags usage = 0;
VkPipelineStageFlags2 stages = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
LinearHostImageCreateInfoFlags flags = 0;
};
// Special image type which supports direct CPU mapping.
// Useful optimization for UMA implementations of Vulkan where we don't necessarily need
// to perform staging copies. It gracefully falls back to staging buffer as needed.
// Only usage flag SAMPLED_BIT is currently supported.
class LinearHostImage : public Util::IntrusivePtrEnabled<LinearHostImage, LinearHostImageDeleter, HandleCounter>
{
public:
friend struct LinearHostImageDeleter;
size_t get_row_pitch_bytes() const;
size_t get_offset() const;
const ImageView &get_view() const;
const Image &get_image() const;
const DeviceAllocation &get_host_visible_allocation() const;
const Buffer &get_host_visible_buffer() const;
bool need_staging_copy() const;
VkPipelineStageFlags2 get_used_pipeline_stages() const;
private:
friend class Util::ObjectPool<LinearHostImage>;
LinearHostImage(Device *device, ImageHandle gpu_image, Util::IntrusivePtr<Buffer> cpu_image,
VkPipelineStageFlags2 stages);
Device *device;
ImageHandle gpu_image;
Util::IntrusivePtr<Buffer> cpu_image;
VkPipelineStageFlags2 stages;
size_t row_pitch;
size_t row_offset;
};
using LinearHostImageHandle = Util::IntrusivePtr<LinearHostImage>;
}
@@ -0,0 +1,146 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "indirect_layout.hpp"
#include "device.hpp"
namespace Vulkan
{
IndirectLayout::IndirectLayout(Device *device_,
const PipelineLayout *pipeline_layout, const IndirectLayoutToken *tokens,
uint32_t num_tokens, uint32_t stride)
: device(device_)
{
VkIndirectCommandsLayoutCreateInfoEXT info = { VK_STRUCTURE_TYPE_INDIRECT_COMMANDS_LAYOUT_CREATE_INFO_EXT };
info.indirectStride = stride;
info.pipelineLayout = pipeline_layout ? pipeline_layout->get_layout() : VK_NULL_HANDLE;
info.flags = VK_INDIRECT_COMMANDS_LAYOUT_USAGE_EXPLICIT_PREPROCESS_BIT_EXT |
VK_INDIRECT_COMMANDS_LAYOUT_USAGE_UNORDERED_SEQUENCES_BIT_EXT;
Util::SmallVector<VkIndirectCommandsLayoutTokenEXT, 8> ext_tokens;
Util::SmallVector<VkIndirectCommandsVertexBufferTokenEXT, 8> vbo_tokens;
Util::SmallVector<VkIndirectCommandsPushConstantTokenEXT, 8> push_tokens;
VkIndirectCommandsIndexBufferTokenEXT ibo_token;
VkIndirectCommandsExecutionSetTokenEXT exec_token;
ext_tokens.reserve(num_tokens);
vbo_tokens.reserve(num_tokens);
push_tokens.reserve(num_tokens);
bool heap = device->get_device_features().descriptor_heap_features.descriptorHeap == VK_TRUE;
for (uint32_t i = 0; i < num_tokens; i++)
{
VkIndirectCommandsLayoutTokenEXT token = { VK_STRUCTURE_TYPE_INDIRECT_COMMANDS_LAYOUT_TOKEN_EXT };
switch (tokens[i].type)
{
case IndirectLayoutToken::Type::VBO:
token.type = VK_INDIRECT_COMMANDS_TOKEN_TYPE_VERTEX_BUFFER_EXT;
vbo_tokens.emplace_back();
token.data.pVertexBuffer = &vbo_tokens.back();
vbo_tokens.back().vertexBindingUnit = tokens[i].data.vbo.binding;
break;
case IndirectLayoutToken::Type::IBO:
token.type = VK_INDIRECT_COMMANDS_TOKEN_TYPE_INDEX_BUFFER_EXT;
token.data.pIndexBuffer = &ibo_token;
ibo_token.mode = VK_INDIRECT_COMMANDS_INPUT_MODE_VULKAN_INDEX_BUFFER_EXT;
break;
case IndirectLayoutToken::Type::PushConstant:
case IndirectLayoutToken::Type::SequenceCount:
{
auto push_token_type = heap ?
VK_INDIRECT_COMMANDS_TOKEN_TYPE_PUSH_DATA_EXT :
VK_INDIRECT_COMMANDS_TOKEN_TYPE_PUSH_CONSTANT_EXT;
auto sequence_token_type = heap ?
VK_INDIRECT_COMMANDS_TOKEN_TYPE_PUSH_DATA_SEQUENCE_INDEX_EXT :
VK_INDIRECT_COMMANDS_TOKEN_TYPE_SEQUENCE_INDEX_EXT;
token.type = tokens[i].type == IndirectLayoutToken::Type::PushConstant ?
push_token_type : sequence_token_type;
push_tokens.emplace_back();
token.data.pPushConstant = &push_tokens.back();
VK_ASSERT(pipeline_layout->get_layout());
push_tokens.back().updateRange.size = tokens[i].data.push.range;
push_tokens.back().updateRange.offset = tokens[i].data.push.offset;
push_tokens.back().updateRange.stageFlags =
heap ? VkShaderStageFlags(VK_SHADER_STAGE_ALL) :
pipeline_layout->get_resource_layout().push_constant_range.stageFlags;
break;
}
case IndirectLayoutToken::Type::Draw:
info.shaderStages = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
token.type = VK_INDIRECT_COMMANDS_TOKEN_TYPE_DRAW_EXT;
break;
case IndirectLayoutToken::Type::DrawIndexed:
token.type = VK_INDIRECT_COMMANDS_TOKEN_TYPE_DRAW_INDEXED_EXT;
info.shaderStages = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
break;
case IndirectLayoutToken::Type::Shader:
token.type = VK_INDIRECT_COMMANDS_TOKEN_TYPE_EXECUTION_SET_EXT;
token.data.pExecutionSet = &exec_token;
break;
case IndirectLayoutToken::Type::MeshTasks:
token.type = VK_INDIRECT_COMMANDS_TOKEN_TYPE_DRAW_MESH_TASKS_EXT;
info.shaderStages |= VK_SHADER_STAGE_MESH_BIT_EXT | VK_SHADER_STAGE_TASK_BIT_EXT |
VK_SHADER_STAGE_FRAGMENT_BIT;
break;
case IndirectLayoutToken::Type::Dispatch:
token.type = VK_INDIRECT_COMMANDS_TOKEN_TYPE_DISPATCH_EXT;
info.shaderStages |= VK_SHADER_STAGE_COMPUTE_BIT;
break;
default:
LOGE("Invalid token type.\n");
break;
}
token.offset = tokens[i].offset;
ext_tokens.push_back(token);
}
info.pTokens = ext_tokens.data();
info.tokenCount = num_tokens;
exec_token.type = VK_INDIRECT_EXECUTION_SET_INFO_TYPE_PIPELINES_EXT;
exec_token.shaderStages = info.shaderStages;
stages = info.shaderStages;
auto &table = device->get_device_table();
if (table.vkCreateIndirectCommandsLayoutEXT(device->get_device(), &info, nullptr, &layout) != VK_SUCCESS)
{
LOGE("Failed to create indirect layout.\n");
}
}
IndirectLayout::~IndirectLayout()
{
device->get_device_table().vkDestroyIndirectCommandsLayoutEXT(device->get_device(), layout, nullptr);
}
}
@@ -0,0 +1,93 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_headers.hpp"
#include "vulkan_common.hpp"
#include "cookie.hpp"
#include "small_vector.hpp"
namespace Vulkan
{
class Device;
class PipelineLayout;
struct IndirectLayoutToken
{
enum class Type
{
Invalid = 0,
Shader,
PushConstant,
SequenceCount,
VBO,
IBO,
Draw,
DrawIndexed,
MeshTasks,
Dispatch
};
Type type = Type::Invalid;
uint32_t offset = 0;
union
{
struct
{
uint32_t offset;
uint32_t range;
} push;
struct
{
uint32_t binding;
} vbo;
} data = {};
};
class IndirectLayout : public HashedObject<IndirectLayout>
{
public:
IndirectLayout(Device *device, const PipelineLayout *layout, const IndirectLayoutToken *token,
uint32_t num_tokens, uint32_t stride);
~IndirectLayout();
VkIndirectCommandsLayoutEXT get_layout() const
{
return layout;
}
VkShaderStageFlags get_shader_stages() const
{
return stages;
}
private:
friend class Device;
Device *device;
VkIndirectCommandsLayoutEXT layout;
VkShaderStageFlags stages;
};
}
@@ -0,0 +1,41 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
namespace Vulkan
{
constexpr unsigned VULKAN_NUM_DESCRIPTOR_SETS = 4;
constexpr unsigned VULKAN_NUM_DYNAMIC_UBOS = 8; // Vulkan min-spec
constexpr unsigned VULKAN_NUM_BINDINGS = 32;
constexpr unsigned VULKAN_NUM_BINDINGS_BINDLESS_VARYING = 16 * 1024;
constexpr unsigned VULKAN_NUM_ATTACHMENTS = 8;
constexpr unsigned VULKAN_NUM_VERTEX_ATTRIBS = 16;
constexpr unsigned VULKAN_NUM_VERTEX_BUFFERS = 4;
constexpr unsigned VULKAN_PUSH_CONSTANT_SIZE = 128;
constexpr unsigned VULKAN_PUSH_DATA_SIZE = 256;
constexpr unsigned VULKAN_MAX_UBO_SIZE = 64 * 1024;
constexpr unsigned VULKAN_NUM_USER_SPEC_CONSTANTS = 8;
constexpr unsigned VULKAN_NUM_INTERNAL_SPEC_CONSTANTS = 4;
constexpr unsigned VULKAN_NUM_TOTAL_SPEC_CONSTANTS =
VULKAN_NUM_USER_SPEC_CONSTANTS + VULKAN_NUM_INTERNAL_SPEC_CONSTANTS;
}
@@ -0,0 +1,922 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#define NOMINMAX
#include "resource_manager.hpp"
#include "device.hpp"
#include "memory_mapped_texture.hpp"
#include "texture_files.hpp"
#include "texture_decoder.hpp"
#include "string_helpers.hpp"
#include "thread_group.hpp"
#include "meshlet.hpp"
#include "aabb.hpp"
#include "environment.hpp"
#include <float.h>
namespace Vulkan
{
ResourceManager::ResourceManager(Device *device_)
: device(device_)
, index_buffer_allocator(*device_, 256, 17)
, attribute_buffer_allocator(*device_, 256, 17)
, indirect_buffer_allocator(*device_, 32, 15)
, mesh_header_allocator(*device_, 32, 15)
, mesh_stream_allocator(*device_, 8, 17)
, mesh_payload_allocator(*device_, 32, 17)
{
assets.reserve(Granite::AssetID::MaxIDs);
}
ResourceManager::~ResourceManager()
{
// Also works as a teardown mechanism to make sure there are no async threads in flight.
if (manager)
manager->set_asset_instantiator_interface(nullptr);
// Ensure resource releases go through.
latch_handles();
}
void ResourceManager::set_id_bounds(uint32_t bound)
{
// We must avoid reallocation here to avoid a ton of extra silly locking.
VK_ASSERT(bound <= Granite::AssetID::MaxIDs);
assets.resize(bound);
}
void ResourceManager::set_asset_class(Granite::AssetID id, Granite::AssetClass asset_class)
{
if (id)
{
assets[id.id].asset_class = asset_class;
if (asset_class != Granite::AssetClass::Mesh)
{
std::unique_lock<std::mutex> holder{lock};
views.resize(assets.size());
if (!views[id.id])
views[id.id] = &get_fallback_image(asset_class)->get_view();
}
}
}
void ResourceManager::release_asset(Granite::AssetID id)
{
if (id)
{
std::unique_lock<std::mutex> holder{lock};
VK_ASSERT(id.id < assets.size());
auto &asset = assets[id.id];
asset.latchable = false;
updates.push_back(id);
}
}
uint64_t ResourceManager::estimate_cost_asset(Granite::AssetID id, Granite::File &file)
{
if (assets[id.id].asset_class == Granite::AssetClass::Mesh)
{
// Compression factor of 2x is reasonable to assume.
if (mesh_encoding == MeshEncoding::VBOAndIBOMDI)
return file.get_size() * 2;
else
return file.get_size();
}
else
{
// TODO: When we get compressed BC/ASTC, this will have to change.
return file.get_size();
}
}
void ResourceManager::init_mesh_assets()
{
Internal::MeshGlobalAllocator::PrimeOpaque opaque = {};
opaque.domain = BufferDomain::Device;
opaque.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
if (device->get_device_features().mesh_shader_features.meshShader)
{
mesh_encoding = MeshEncoding::MeshletEncoded;
LOGI("Opting in to meshlet path.\n");
}
else
{
mesh_encoding = MeshEncoding::VBOAndIBOMDI;
LOGI("Falling back to multi-draw-indirect path.\n");
}
std::string encoding;
if (Util::get_environment("GRANITE_MESH_ENCODING", encoding))
{
if (encoding == "encoded")
mesh_encoding = MeshEncoding::MeshletEncoded;
else if (encoding == "decoded")
mesh_encoding = MeshEncoding::MeshletDecoded;
else if (encoding == "mdi")
mesh_encoding = MeshEncoding::VBOAndIBOMDI;
else if (encoding == "classic")
mesh_encoding = MeshEncoding::Classic;
else
LOGE("Unknown encoding: %s\n", encoding.c_str());
}
if (mesh_encoding != MeshEncoding::MeshletEncoded)
{
unsigned index_size;
if (mesh_encoding == MeshEncoding::Classic)
index_size = sizeof(uint32_t);
else if (device->get_device_features().vk14_features.indexTypeUint8)
index_size = sizeof(uint8_t);
else
index_size = sizeof(uint16_t);
index_buffer_allocator.set_element_size(0, 3 * index_size); // 8-bit or 32-bit indices.
attribute_buffer_allocator.set_soa_count(3);
attribute_buffer_allocator.set_element_size(0, sizeof(float) * 3);
attribute_buffer_allocator.set_element_size(1, sizeof(float) * 2 + sizeof(uint32_t) * 2);
attribute_buffer_allocator.set_element_size(2, sizeof(uint32_t) * 2);
opaque.usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
index_buffer_allocator.prime(&opaque);
opaque.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
attribute_buffer_allocator.prime(&opaque);
if (mesh_encoding != MeshEncoding::Classic)
{
auto element_size = mesh_encoding == MeshEncoding::MeshletDecoded ?
sizeof(Meshlet::RuntimeHeaderDecoded) : sizeof(Meshlet::RuntimeHeaderDecodedMDI);
indirect_buffer_allocator.set_soa_count(2);
indirect_buffer_allocator.set_element_size(0, Meshlet::ChunkFactor * element_size);
indirect_buffer_allocator.set_element_size(1, sizeof(Meshlet::Bound));
opaque.usage = VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_DST_BIT;
indirect_buffer_allocator.prime(&opaque);
}
}
else
{
mesh_header_allocator.set_element_size(0, sizeof(Meshlet::RuntimeHeaderEncoded));
mesh_stream_allocator.set_element_size(0, sizeof(Meshlet::Stream));
mesh_payload_allocator.set_element_size(0, sizeof(Meshlet::PayloadWord));
mesh_header_allocator.set_soa_count(2);
mesh_header_allocator.set_element_size(1, sizeof(Meshlet::Bound));
opaque.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
mesh_header_allocator.prime(&opaque);
mesh_stream_allocator.prime(&opaque);
mesh_payload_allocator.prime(&opaque);
}
}
void ResourceManager::init()
{
manager = device->get_system_handles().asset_manager;
// Need to initialize these before setting the interface.
{
uint8_t buffer[4] = {0xff, 0x00, 0xff, 0xff};
auto info = ImageCreateInfo::immutable_2d_image(1, 1, VK_FORMAT_R8G8B8A8_UNORM);
info.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
info.misc = IMAGE_MISC_CONCURRENT_QUEUE_ASYNC_COMPUTE_BIT |
IMAGE_MISC_CONCURRENT_QUEUE_GRAPHICS_BIT;
ImageInitialData data = {buffer, 0, 0};
fallback_color = device->create_image(info, &data);
buffer[0] = 0x80;
buffer[1] = 0x80;
buffer[2] = 0xff;
fallback_normal = device->create_image(info, &data);
buffer[0] = 0x00;
buffer[1] = 0x00;
fallback_pbr = device->create_image(info, &data);
memset(buffer, 0, sizeof(buffer));
fallback_zero = device->create_image(info, &data);
}
if (manager)
{
manager->set_asset_instantiator_interface(this);
HeapBudget budget[VK_MAX_MEMORY_HEAPS] = {};
device->get_memory_budget(budget);
// Try to set aside 50% of budgetable VRAM for the resource manager. Seems reasonable.
VkDeviceSize size = 0;
for (uint32_t i = 0; i < device->get_memory_properties().memoryHeapCount; i++)
if ((device->get_memory_properties().memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0)
size = std::max(size, budget[i].budget_size / 2);
if (size == 0)
{
LOGW("No DEVICE_LOCAL heap was found, assuming 2 GiB budget.\n");
size = 2 * 1024 * 1024;
}
LOGI("Using texture budget of %u MiB.\n", unsigned(size / (1024 * 1024)));
manager->set_asset_budget(size);
// This is somewhat arbitrary.
manager->set_asset_budget_per_iteration(2 * 1000 * 1000);
}
// Opt-in. Normal Granite applications shouldn't allocate up a ton of space up front.
if (manager && manager->get_wants_mesh_assets())
init_mesh_assets();
}
ImageHandle ResourceManager::create_gtx(const MemoryMappedTexture &mapped_file, Granite::AssetID id)
{
if (mapped_file.empty())
return {};
auto &layout = mapped_file.get_layout();
VkComponentMapping swizzle = {};
mapped_file.remap_swizzle(swizzle);
ImageHandle image;
if (!device->image_format_is_supported(layout.get_format(), VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) &&
format_compression_type(layout.get_format()) != FormatCompressionType::Uncompressed)
{
LOGI("Compressed format #%u is not supported, falling back to compute decode of compressed image.\n",
unsigned(layout.get_format()));
GRANITE_SCOPED_TIMELINE_EVENT_FILE(device->get_system_handles().timeline_trace_file, "texture-load-submit-decompress");
auto cmd = device->request_command_buffer(CommandBuffer::Type::AsyncCompute);
image = Granite::decode_compressed_image(*cmd, layout, VK_FORMAT_UNDEFINED, swizzle);
Semaphore sem;
device->submit(cmd, nullptr, 1, &sem);
device->add_wait_semaphore(CommandBuffer::Type::Generic, sem, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, true);
}
else
{
ImageCreateInfo info = ImageCreateInfo::immutable_image(layout);
info.swizzle = swizzle;
info.flags = (mapped_file.get_flags() & MEMORY_MAPPED_TEXTURE_CUBE_MAP_COMPATIBLE_BIT) ?
VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT :
0;
info.misc = IMAGE_MISC_CONCURRENT_QUEUE_GRAPHICS_BIT |
IMAGE_MISC_CONCURRENT_QUEUE_ASYNC_COMPUTE_BIT;
if (info.levels == 1 &&
(mapped_file.get_flags() & MEMORY_MAPPED_TEXTURE_GENERATE_MIPMAP_ON_LOAD_BIT) != 0 &&
device->image_format_is_supported(info.format, VK_FORMAT_FEATURE_BLIT_SRC_BIT) &&
device->image_format_is_supported(info.format, VK_FORMAT_FEATURE_BLIT_DST_BIT))
{
info.levels = 0;
info.misc |= IMAGE_MISC_GENERATE_MIPS_BIT;
}
if (!device->image_format_is_supported(info.format, VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT))
{
LOGE("Format (%u) is not supported!\n", unsigned(info.format));
return {};
}
InitialImageBuffer staging;
{
GRANITE_SCOPED_TIMELINE_EVENT_FILE(device->get_system_handles().timeline_trace_file,
"texture-load-create-staging");
staging = device->create_image_staging_buffer(layout);
}
{
GRANITE_SCOPED_TIMELINE_EVENT_FILE(device->get_system_handles().timeline_trace_file,
"texture-load-allocate-image");
image = device->create_image_from_staging_buffer(info, &staging);
}
}
if (image)
{
auto name = Util::join("AssetID-", id.id);
device->set_name(*image, name.c_str());
}
return image;
}
ImageHandle ResourceManager::create_gtx(Granite::FileMappingHandle mapping, Granite::AssetID id)
{
MemoryMappedTexture mapped_file;
if (!mapped_file.map_read(std::move(mapping)))
{
LOGE("Failed to read texture.\n");
return {};
}
return create_gtx(mapped_file, id);
}
ImageHandle ResourceManager::create_other(const Granite::FileMapping &mapping, Granite::AssetClass asset_class,
Granite::AssetID id)
{
auto tex = load_texture_from_memory(mapping.data(),
mapping.get_size(), asset_class == Granite::AssetClass::ImageColor ?
ColorSpace::sRGB : ColorSpace::Linear);
return create_gtx(tex, id);
}
const ImageView *ResourceManager::get_image_view_blocking(Granite::AssetID id)
{
std::unique_lock<std::mutex> holder{lock};
if (id.id >= assets.size())
{
LOGE("ID %u is out of bounds.\n", id.id);
return nullptr;
}
auto &asset = assets[id.id];
if (asset.image)
return &asset.image->get_view();
if (!manager->iterate_blocking(*device->get_system_handles().thread_group, id))
{
LOGE("Failed to iterate.\n");
return nullptr;
}
cond.wait(holder, [&asset]() -> bool {
return bool(asset.latchable);
});
return &asset.image->get_view();
}
void ResourceManager::instantiate_asset(Granite::AssetManager &manager_, Granite::TaskGroup *task,
Granite::AssetID id, Granite::File &file)
{
if (task)
{
task->enqueue_task([this, &manager_, &file, id]() {
instantiate_asset(manager_, id, file);
});
}
else
{
instantiate_asset(manager_, id, file);
}
}
void ResourceManager::instantiate_asset(Granite::AssetManager &manager_,
Granite::AssetID id,
Granite::File &file)
{
auto &asset = assets[id.id];
if (asset.asset_class == Granite::AssetClass::Mesh)
instantiate_asset_mesh(manager_, id, file);
else
instantiate_asset_image(manager_, id, file);
}
bool ResourceManager::allocate_asset_mesh(Granite::AssetID id, const Meshlet::MeshView &view)
{
if (!view.format_header)
return false;
std::lock_guard<std::mutex> holder{mesh_allocator_lock};
auto &asset = assets[id.id];
bool ret = true;
if (mesh_encoding == MeshEncoding::MeshletEncoded)
{
if (ret)
ret = mesh_header_allocator.allocate(view.num_bounds_256, &asset.mesh.indirect_or_header);
if (ret)
{
ret = mesh_stream_allocator.allocate(
view.num_bounds_256 * Meshlet::ChunkFactor * view.format_header->stream_count,
&asset.mesh.attr_or_stream);
}
if (ret)
ret = mesh_payload_allocator.allocate(view.format_header->payload_size_words, &asset.mesh.index_or_payload);
}
else
{
if (ret)
ret = index_buffer_allocator.allocate(view.total_primitives, &asset.mesh.index_or_payload);
if (ret)
ret = attribute_buffer_allocator.allocate(view.total_vertices, &asset.mesh.attr_or_stream);
if (ret && mesh_encoding != MeshEncoding::Classic)
ret = indirect_buffer_allocator.allocate(view.num_bounds_256, &asset.mesh.indirect_or_header);
}
if (mesh_encoding == MeshEncoding::Classic)
{
asset.mesh.draw.indexed = {
view.total_primitives * 3, 1,
asset.mesh.index_or_payload.offset,
int32_t(asset.mesh.attr_or_stream.offset), 0,
};
}
else
{
asset.mesh.draw.meshlet = {
asset.mesh.indirect_or_header.offset,
view.num_bounds_256,
view.format_header->style,
};
}
if (!ret)
{
if (mesh_encoding == MeshEncoding::MeshletEncoded)
{
mesh_payload_allocator.free(asset.mesh.index_or_payload);
mesh_stream_allocator.free(asset.mesh.attr_or_stream);
mesh_header_allocator.free(asset.mesh.indirect_or_header);
}
else
{
index_buffer_allocator.free(asset.mesh.index_or_payload);
attribute_buffer_allocator.free(asset.mesh.attr_or_stream);
indirect_buffer_allocator.free(asset.mesh.indirect_or_header);
}
asset.mesh = {};
}
return ret;
}
void ResourceManager::instantiate_asset_mesh(Granite::AssetManager &manager_,
Granite::AssetID id,
Granite::File &file)
{
Granite::FileMappingHandle mapping;
if (file.get_size())
mapping = file.map();
Meshlet::MeshView view = {};
if (mapping)
view = Meshlet::create_mesh_view(*mapping);
bool ret = allocate_asset_mesh(id, view);
// Decode the meshlet. Later, we'll have to do a lot of device specific stuff here to select optimal
// processing:
// - Native meshlets
// - Encoded attribute
// - Decoded attributes
// - Optimize for multi-draw-indirect or not? (8-bit indices).
auto &asset = assets[id.id];
if (ret)
{
size_t total_streams = view.format_header->meshlet_count * view.format_header->stream_count;
size_t total_padded_streams = view.num_bounds_256 * Meshlet::ChunkFactor * view.format_header->stream_count;
if (mesh_encoding == MeshEncoding::MeshletEncoded)
{
auto cmd = device->request_command_buffer(CommandBuffer::Type::AsyncTransfer);
void *payload_data = cmd->update_buffer(*mesh_payload_allocator.get_buffer(0, 0),
asset.mesh.index_or_payload.offset * sizeof(Meshlet::PayloadWord),
view.format_header->payload_size_words * sizeof(Meshlet::PayloadWord));
memcpy(payload_data, view.payload, view.format_header->payload_size_words * sizeof(Meshlet::PayloadWord));
auto *headers = static_cast<Meshlet::RuntimeHeaderEncoded *>(
cmd->update_buffer(*mesh_header_allocator.get_buffer(0, 0),
asset.mesh.indirect_or_header.offset * sizeof(Meshlet::RuntimeHeaderEncoded),
view.num_bounds_256 * sizeof(Meshlet::RuntimeHeaderEncoded)));
for (uint32_t i = 0, n = view.num_bounds_256; i < n; i++)
{
headers[i].stream_offset = asset.mesh.attr_or_stream.offset +
i * Meshlet::ChunkFactor * view.format_header->stream_count;
}
auto *bounds = static_cast<Meshlet::Bound *>(
cmd->update_buffer(*mesh_header_allocator.get_buffer(0, 1),
asset.mesh.indirect_or_header.offset * sizeof(Meshlet::Bound),
view.num_bounds_256 * sizeof(Meshlet::Bound)));
memcpy(bounds, view.bounds_256, view.num_bounds_256 * sizeof(Meshlet::Bound));
auto *streams = static_cast<Meshlet::Stream *>(
cmd->update_buffer(*mesh_stream_allocator.get_buffer(0, 0),
asset.mesh.attr_or_stream.offset * sizeof(Meshlet::Stream),
total_padded_streams * sizeof(Meshlet::Stream)));
for (uint32_t i = 0; i < total_streams; i++)
{
auto in_stream = view.streams[i];
in_stream.offset_in_words += asset.mesh.index_or_payload.offset;
streams[i] = in_stream;
}
memset(streams + total_streams, 0, (total_padded_streams - total_streams) * sizeof(Meshlet::Stream));
Semaphore sem;
device->submit(cmd, nullptr, 1, &sem);
device->add_wait_semaphore(CommandBuffer::Type::Generic, std::move(sem),
VK_PIPELINE_STAGE_2_MESH_SHADER_BIT_EXT |
VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, false);
}
else
{
auto cmd = device->request_command_buffer(CommandBuffer::Type::AsyncCompute);
BufferCreateInfo buf = {};
buf.domain = BufferDomain::Host;
buf.size = view.format_header->payload_size_words * sizeof(Meshlet::PayloadWord);
buf.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
auto payload = device->create_buffer(buf, view.payload);
Meshlet::DecodeInfo info = {};
info.target_style = view.format_header->style;
if (mesh_encoding == MeshEncoding::Classic)
info.flags |= Meshlet::DECODE_MODE_UNROLLED_MESH;
else if (!device->get_device_features().vk14_features.indexTypeUint8)
info.flags |= Meshlet::DECODE_MODE_INDEX_16;
info.ibo = index_buffer_allocator.get_buffer(0, 0);
for (unsigned i = 0; i < 3; i++)
info.streams[i] = attribute_buffer_allocator.get_buffer(0, i);
info.payload = payload.get();
info.push.primitive_offset = asset.mesh.index_or_payload.offset;
info.push.vertex_offset = asset.mesh.attr_or_stream.offset;
info.runtime_style = mesh_encoding == MeshEncoding::MeshletDecoded ?
Meshlet::RuntimeStyle::Meshlet : Meshlet::RuntimeStyle::MDI;
if (mesh_encoding != MeshEncoding::Classic)
{
auto *bounds = static_cast<Meshlet::Bound *>(
cmd->update_buffer(*indirect_buffer_allocator.get_buffer(0, 1),
asset.mesh.indirect_or_header.offset * sizeof(Meshlet::Bound),
view.num_bounds_256 * sizeof(Meshlet::Bound)));
memcpy(bounds, view.bounds_256, view.num_bounds_256 * sizeof(Meshlet::Bound));
info.indirect = indirect_buffer_allocator.get_buffer(0, 0);
info.indirect_offset = asset.mesh.indirect_or_header.offset;
}
Meshlet::decode_mesh(*cmd, info, view);
Semaphore sem;
device->submit(cmd, nullptr, 1, &sem);
device->add_wait_semaphore(CommandBuffer::Type::Generic, std::move(sem),
VK_PIPELINE_STAGE_2_VERTEX_ATTRIBUTE_INPUT_BIT |
VK_PIPELINE_STAGE_2_INDEX_INPUT_BIT, false);
}
}
uint64_t cost = 0;
if (ret)
{
if (mesh_encoding == MeshEncoding::MeshletEncoded)
{
cost += view.format_header->payload_size_words * mesh_payload_allocator.get_element_size(0);
cost += view.num_bounds_256 * mesh_header_allocator.get_element_size(0);
cost += view.num_bounds_256 * mesh_header_allocator.get_element_size(1);
cost += view.format_header->meshlet_count * view.format_header->stream_count * mesh_stream_allocator.get_element_size(0);
}
else
{
cost += view.total_primitives * index_buffer_allocator.get_element_size(0);
cost += view.total_vertices * attribute_buffer_allocator.get_element_size(0);
cost += view.total_vertices * attribute_buffer_allocator.get_element_size(1);
cost += view.total_vertices * attribute_buffer_allocator.get_element_size(2);
if (mesh_encoding != MeshEncoding::Classic)
{
cost += view.format_header->meshlet_count * indirect_buffer_allocator.get_element_size(0);
cost += view.format_header->meshlet_count * indirect_buffer_allocator.get_element_size(1);
}
}
}
std::lock_guard<std::mutex> holder{lock};
updates.push_back(id);
manager_.update_cost(id, ret ? cost : 0);
asset.latchable = true;
cond.notify_all();
}
void ResourceManager::instantiate_asset_image(Granite::AssetManager &manager_,
Granite::AssetID id,
Granite::File &file)
{
auto &asset = assets[id.id];
ImageHandle image;
if (file.get_size())
{
auto mapping = file.map();
if (mapping)
{
if (MemoryMappedTexture::is_header(mapping->data(), mapping->get_size()))
image = create_gtx(std::move(mapping), id);
else
image = create_other(*mapping, asset.asset_class, id);
}
else
LOGE("Failed to map file.\n");
}
// Have to signal something.
if (!image)
image = get_fallback_image(asset.asset_class);
std::lock_guard<std::mutex> holder{lock};
updates.push_back(id);
asset.image = std::move(image);
asset.latchable = true;
manager_.update_cost(id, asset.image ? asset.image->get_allocation().get_size() : 0);
cond.notify_all();
}
const ImageHandle &ResourceManager::get_fallback_image(Granite::AssetClass asset_class)
{
switch (asset_class)
{
default:
case Granite::AssetClass::ImageZeroable:
return fallback_zero;
case Granite::AssetClass::ImageColor:
return fallback_color;
case Granite::AssetClass::ImageNormal:
return fallback_normal;
case Granite::AssetClass::ImageMetallicRoughness:
return fallback_pbr;
}
}
void ResourceManager::latch_handles()
{
std::lock_guard<std::mutex> holder{lock};
views.resize(assets.size());
draws.resize(assets.size());
for (auto &update : updates)
{
if (update.id >= views.size())
continue;
auto &asset = assets[update.id];
if (asset.asset_class == Granite::AssetClass::Mesh)
{
if (!asset.latchable)
{
{
std::lock_guard<std::mutex> holder_alloc{mesh_allocator_lock};
if (mesh_encoding == MeshEncoding::MeshletEncoded)
{
mesh_payload_allocator.free(asset.mesh.index_or_payload);
mesh_stream_allocator.free(asset.mesh.attr_or_stream);
mesh_header_allocator.free(asset.mesh.indirect_or_header);
}
else
{
index_buffer_allocator.free(asset.mesh.index_or_payload);
attribute_buffer_allocator.free(asset.mesh.attr_or_stream);
indirect_buffer_allocator.free(asset.mesh.indirect_or_header);
}
}
asset.mesh = {};
}
draws[update.id] = asset.mesh.draw;
}
else
{
const ImageView *view;
if (!asset.latchable)
asset.image.reset();
if (asset.image)
{
view = &asset.image->get_view();
}
else
{
auto &img = get_fallback_image(asset.asset_class);
view = &img->get_view();
}
views[update.id] = view;
}
}
updates.clear();
}
const Buffer *ResourceManager::get_index_buffer() const
{
return index_buffer_allocator.get_buffer(0, 0);
}
const Buffer *ResourceManager::get_position_buffer() const
{
return attribute_buffer_allocator.get_buffer(0, 0);
}
const Buffer *ResourceManager::get_attribute_buffer() const
{
return attribute_buffer_allocator.get_buffer(0, 1);
}
const Buffer *ResourceManager::get_skinning_buffer() const
{
return attribute_buffer_allocator.get_buffer(0, 2);
}
const Buffer *ResourceManager::get_indirect_buffer() const
{
return indirect_buffer_allocator.get_buffer(0, 0);
}
const Buffer *ResourceManager::get_meshlet_payload_buffer() const
{
return mesh_payload_allocator.get_buffer(0, 0);
}
const Buffer *ResourceManager::get_meshlet_header_buffer() const
{
return mesh_header_allocator.get_buffer(0, 0);
}
const Buffer *ResourceManager::get_meshlet_stream_header_buffer() const
{
return mesh_stream_allocator.get_buffer(0, 0);
}
const Buffer *ResourceManager::get_cluster_bounds_buffer() const
{
if (mesh_encoding == MeshEncoding::MeshletEncoded)
return mesh_header_allocator.get_buffer(0, 1);
else
return indirect_buffer_allocator.get_buffer(0, 1);
}
bool ResourceManager::mesh_rendering_is_hierarchical_task() const
{
return device->get_gpu_properties().vendorID == VENDOR_ID_AMD;
}
bool ResourceManager::mesh_rendering_is_local_invocation_indexed() const
{
#if 0
bool local_invocation_indexed =
device->get_device_features().mesh_shader_properties.prefersLocalInvocationPrimitiveOutput ||
device->get_device_features().mesh_shader_properties.prefersLocalInvocationVertexOutput;
return local_invocation_indexed;
#else
return false;
#endif
}
bool ResourceManager::mesh_rendering_is_wave_culled() const
{
return device->supports_subgroup_size_log2(true, 5, 5, VK_SHADER_STAGE_MESH_BIT_EXT) &&
device->get_device_features().vk13_props.minSubgroupSize == 32;
}
MeshBufferAllocator::MeshBufferAllocator(Device &device, uint32_t sub_block_size, uint32_t num_sub_blocks_in_arena_log2)
: global_allocator(device)
{
init(sub_block_size, num_sub_blocks_in_arena_log2, &global_allocator);
}
void MeshBufferAllocator::set_soa_count(unsigned soa_count)
{
VK_ASSERT(soa_count <= Internal::MeshGlobalAllocator::MaxSoACount);
global_allocator.soa_count = soa_count;
}
void MeshBufferAllocator::set_element_size(unsigned soa_index, uint32_t element_size)
{
VK_ASSERT(soa_index < global_allocator.soa_count);
global_allocator.element_size[soa_index] = element_size;
}
uint32_t MeshBufferAllocator::get_element_size(unsigned soa_index) const
{
VK_ASSERT(soa_index < global_allocator.soa_count);
return global_allocator.element_size[soa_index];
}
const Buffer *MeshBufferAllocator::get_buffer(unsigned index, unsigned soa_index) const
{
VK_ASSERT(soa_index < global_allocator.soa_count);
index = index * global_allocator.soa_count + soa_index;
// Avoid any race condition.
if (index < soa_index && global_allocator.preallocated_handles[soa_index])
return global_allocator.preallocated_handles[soa_index];
else if (index < global_allocator.global_buffers.size())
return global_allocator.global_buffers[index].get();
else
return nullptr;
}
namespace Internal
{
uint32_t MeshGlobalAllocator::allocate(uint32_t count)
{
BufferCreateInfo info = {};
uint32_t target_index = UINT32_MAX;
uint32_t search_index = 0;
for (uint32_t i = 0, n = global_buffers.size(); i < n; i += soa_count, search_index++)
{
if (!global_buffers[i])
{
target_index = search_index;
break;
}
}
if (target_index == UINT32_MAX)
{
if (!global_buffers.empty())
return UINT32_MAX;
target_index = search_index;
for (uint32_t i = 0; i < soa_count; i++)
global_buffers.emplace_back();
}
for (uint32_t soa_index = 0; soa_index < soa_count; soa_index++)
{
info.size = VkDeviceSize(count) * element_size[soa_index];
info.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
info.domain = BufferDomain::Device;
if (preallocated[soa_index] && preallocated[soa_index]->get_create_info().size >= info.size)
std::swap(preallocated[soa_index], global_buffers[target_index * soa_count + soa_index]);
else
global_buffers[target_index * soa_count + soa_index] = device.create_buffer(info);
}
return target_index;
}
void MeshGlobalAllocator::prime(uint32_t count, const void *opaque_meta)
{
auto *opaque = static_cast<const PrimeOpaque *>(opaque_meta);
BufferCreateInfo info = {};
for (uint32_t i = 0; i < soa_count; i++)
{
if (preallocated[i])
continue;
info.size = VkDeviceSize(count) * element_size[i];
info.usage = opaque->usage;
info.domain = opaque->domain;
preallocated[i] = device.create_buffer(info);
preallocated_handles[i] = preallocated[i].get();
}
}
void MeshGlobalAllocator::free(uint32_t index)
{
index *= soa_count;
VK_ASSERT(index < global_buffers.size());
for (uint32_t i = 0; i < soa_count; i++)
{
std::swap(preallocated[i], global_buffers[index + i]);
global_buffers[index + i].reset();
}
}
MeshGlobalAllocator::MeshGlobalAllocator(Device &device_)
: device(device_)
{}
}
}
@@ -0,0 +1,206 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "image.hpp"
#include "buffer.hpp"
#include "asset_manager.hpp"
#include "meshlet.hpp"
#include "arena_allocator.hpp"
#include "small_vector.hpp"
#include <mutex>
#include <condition_variable>
namespace Vulkan
{
class MemoryMappedTexture;
namespace Internal
{
struct MeshGlobalAllocator final : Util::SliceBackingAllocator
{
explicit MeshGlobalAllocator(Device &device);
uint32_t allocate(uint32_t count) override;
void free(uint32_t index) override;
struct PrimeOpaque
{
VkBufferUsageFlags usage;
BufferDomain domain;
};
void prime(uint32_t count, const void *opaque_meta) override;
enum { MaxSoACount = 3 }; // Position, attribute, skinning.
Device &device;
uint32_t element_size[MaxSoACount] = {};
uint32_t soa_count = 1;
Util::SmallVector<BufferHandle> global_buffers;
BufferHandle preallocated[MaxSoACount];
const Buffer *preallocated_handles[MaxSoACount] = {};
};
}
class MeshBufferAllocator : public Util::SliceAllocator
{
public:
MeshBufferAllocator(Device &device, uint32_t sub_block_size, uint32_t num_sub_blocks_in_arena_log2);
void set_soa_count(unsigned soa_count);
void set_element_size(unsigned soa_index, uint32_t element_size);
uint32_t get_element_size(unsigned soa_index) const;
const Buffer *get_buffer(unsigned index, unsigned soa_index) const;
private:
Internal::MeshGlobalAllocator global_allocator;
};
class ResourceManager final : private Granite::AssetInstantiatorInterface
{
public:
explicit ResourceManager(Device *device);
~ResourceManager() override;
void init();
enum class MeshEncoding
{
MeshletEncoded,
MeshletDecoded,
VBOAndIBOMDI,
Classic
};
const Vulkan::ImageView *get_image_view(Granite::AssetID id) const
{
if (id.id < views.size())
return views[id.id];
else
return nullptr;
}
const Vulkan::ImageView *get_image_view_blocking(Granite::AssetID id);
struct DrawRange
{
uint32_t offset;
uint32_t count;
Meshlet::MeshStyle style;
};
union DrawCall
{
DrawRange meshlet;
VkDrawIndexedIndirectCommand indexed;
};
DrawCall get_mesh_draw_range(Granite::AssetID id) const
{
if (id.id < draws.size())
return draws[id.id];
else
return {};
}
MeshEncoding get_mesh_encoding() const
{
return mesh_encoding;
}
const Buffer *get_index_buffer() const;
const Buffer *get_position_buffer() const;
const Buffer *get_attribute_buffer() const;
const Buffer *get_skinning_buffer() const;
const Buffer *get_indirect_buffer() const;
const Buffer *get_meshlet_payload_buffer() const;
const Buffer *get_meshlet_header_buffer() const;
const Buffer *get_meshlet_stream_header_buffer() const;
const Buffer *get_cluster_bounds_buffer() const;
// Mesh shading requires some vendor specific tuning.
bool mesh_rendering_is_hierarchical_task() const;
bool mesh_rendering_is_local_invocation_indexed() const;
bool mesh_rendering_is_wave_culled() const;
private:
Device *device;
Granite::AssetManager *manager = nullptr;
void latch_handles() override;
uint64_t estimate_cost_asset(Granite::AssetID id, Granite::File &file) override;
void instantiate_asset(Granite::AssetManager &manager, Granite::TaskGroup *task,
Granite::AssetID id, Granite::File &file) override;
void release_asset(Granite::AssetID id) override;
void set_id_bounds(uint32_t bound) override;
void set_asset_class(Granite::AssetID id, Granite::AssetClass asset_class) override;
struct Asset
{
ImageHandle image;
struct
{
Util::AllocatedSlice index_or_payload, attr_or_stream, indirect_or_header;
DrawCall draw;
} mesh;
Granite::AssetClass asset_class = Granite::AssetClass::ImageZeroable;
bool latchable = false;
};
std::mutex lock;
std::condition_variable cond;
std::vector<Asset> assets;
std::vector<const ImageView *> views;
std::vector<DrawCall> draws;
std::vector<Granite::AssetID> updates;
ImageHandle fallback_color;
ImageHandle fallback_normal;
ImageHandle fallback_zero;
ImageHandle fallback_pbr;
ImageHandle create_gtx(Granite::FileMappingHandle mapping, Granite::AssetID id);
ImageHandle create_gtx(const MemoryMappedTexture &mapping, Granite::AssetID id);
ImageHandle create_other(const Granite::FileMapping &mapping, Granite::AssetClass asset_class, Granite::AssetID id);
const ImageHandle &get_fallback_image(Granite::AssetClass asset_class);
void instantiate_asset(Granite::AssetManager &manager, Granite::AssetID id, Granite::File &file);
void instantiate_asset_image(Granite::AssetManager &manager, Granite::AssetID id, Granite::File &file);
void instantiate_asset_mesh(Granite::AssetManager &manager, Granite::AssetID id, Granite::File &file);
std::mutex mesh_allocator_lock;
MeshBufferAllocator index_buffer_allocator;
MeshBufferAllocator attribute_buffer_allocator;
MeshBufferAllocator indirect_buffer_allocator;
MeshBufferAllocator mesh_header_allocator;
MeshBufferAllocator mesh_stream_allocator;
MeshBufferAllocator mesh_payload_allocator;
MeshEncoding mesh_encoding = MeshEncoding::Classic;
bool allocate_asset_mesh(Granite::AssetID id, const Meshlet::MeshView &view);
void init_mesh_assets();
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,277 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "shader.hpp"
#include "vulkan_common.hpp"
#include "filesystem.hpp"
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <string>
#include <vector>
#include "hash.hpp"
#include "read_write_lock.hpp"
namespace Granite
{
class GLSLCompiler;
struct TaskGroup;
enum class Stage;
}
namespace Vulkan
{
struct ShaderTemplateVariant;
struct PrecomputedMeta : Util::IntrusiveHashMapEnabled<PrecomputedMeta>
{
PrecomputedMeta(Util::Hash source_hash_, Util::Hash shader_hash_)
: source_hash(source_hash_), shader_hash(shader_hash_)
{
}
Util::Hash source_hash;
Util::Hash shader_hash;
};
using PrecomputedShaderCache = VulkanCache<PrecomputedMeta>;
using ReflectionCache = VulkanCache<Util::IntrusivePODWrapper<ResourceLayout>>;
struct MetaCache
{
PrecomputedShaderCache variant_to_shader;
ReflectionCache shader_to_layout;
};
class ShaderManager;
class Device;
struct ShaderTemplateVariant : public Util::IntrusiveHashMapEnabled<ShaderTemplateVariant>
{
Util::Hash hash = 0;
Util::Hash spirv_hash = 0;
std::vector<uint32_t> spirv;
std::vector<std::pair<std::string, int>> defines;
Shader *precompiled_shader = nullptr;
unsigned instance = 0;
Vulkan::Shader *resolve(Vulkan::Device &device) const;
};
class ShaderTemplate : public Util::IntrusiveHashMapEnabled<ShaderTemplate>
{
public:
ShaderTemplate(Device *device, const std::string &shader_path,
ShaderStage force_stage, MetaCache &cache,
Util::Hash path_hash, const std::vector<std::string> &include_directories);
~ShaderTemplate();
bool init();
const ShaderTemplateVariant *register_variant(const std::vector<std::pair<std::string, int>> *defines,
Shader *precompiled_shader);
void register_dependencies(ShaderManager &manager);
Util::Hash get_path_hash() const
{
return path_hash;
}
const std::string &get_path() const
{
return path;
}
ShaderStage get_stage() const
{
return force_stage;
}
VulkanCache<ShaderTemplateVariant> &get_variants()
{
return variants;
}
#ifndef GRANITE_SHIPPING
// We'll never want to recompile shaders in runtime outside a dev environment.
void recompile();
#endif
private:
Device *device;
std::string path;
ShaderStage force_stage;
MetaCache &cache;
Util::Hash path_hash = 0;
std::vector<uint32_t> static_shader;
#ifdef GRANITE_VULKAN_SHADER_MANAGER_RUNTIME_COMPILER
std::unique_ptr<Granite::GLSLCompiler> compiler;
const std::vector<std::string> &include_directories;
void update_variant_cache(const ShaderTemplateVariant &variant);
Util::Hash source_hash = 0;
#ifndef GRANITE_SHIPPING
// We'll never want to recompile shaders in runtime outside a dev environment.
void recompile_variant(ShaderTemplateVariant &variant);
#endif
#endif
VulkanCache<ShaderTemplateVariant> variants;
};
class ShaderProgramVariant : public Util::IntrusiveHashMapEnabled<ShaderProgramVariant>
{
public:
explicit ShaderProgramVariant(Device *device);
Vulkan::Program *get_program();
private:
friend class ShaderProgram;
Device *device;
const ShaderTemplateVariant *stages[static_cast<unsigned>(Vulkan::ShaderStage::Count)] = {};
std::unique_ptr<ImmutableSamplerBank> sampler_bank;
#ifndef GRANITE_SHIPPING
// We'll never want to recompile shaders in runtime outside a dev environment.
std::atomic_uint shader_instance[static_cast<unsigned>(Vulkan::ShaderStage::Count)];
std::atomic<Vulkan::Program *> program;
Util::RWSpinLock instance_lock;
#endif
Vulkan::Program *get_program_compute();
Vulkan::Program *get_program_graphics();
};
class ShaderProgram : public Util::IntrusiveHashMapEnabled<ShaderProgram>
{
public:
ShaderProgram(Device *device_, ShaderTemplate *compute)
: device(device_)
{
set_stage(Vulkan::ShaderStage::Compute, compute);
}
ShaderProgram(Device *device_, ShaderTemplate *vert, ShaderTemplate *frag)
: device(device_)
{
set_stage(Vulkan::ShaderStage::Vertex, vert);
set_stage(Vulkan::ShaderStage::Fragment, frag);
}
ShaderProgram(Device *device_, ShaderTemplate *task, ShaderTemplate *mesh, ShaderTemplate *frag)
: device(device_)
{
if (task)
set_stage(Vulkan::ShaderStage::Task, task);
set_stage(Vulkan::ShaderStage::Mesh, mesh);
set_stage(Vulkan::ShaderStage::Fragment, frag);
}
void set_stage(Vulkan::ShaderStage stage, ShaderTemplate *shader);
ShaderProgramVariant *register_variant(const std::vector<std::pair<std::string, int>> &defines,
const ImmutableSamplerBank *sampler_bank = nullptr);
ShaderProgramVariant *register_precompiled_variant(
Shader *vert, Shader *frag,
const std::vector<std::pair<std::string, int>> &defines,
const ImmutableSamplerBank *sampler_bank = nullptr);
ShaderProgramVariant *register_precompiled_variant(
Shader *comp,
const std::vector<std::pair<std::string, int>> &defines,
const ImmutableSamplerBank *sampler_bank = nullptr);
ShaderProgramVariant *register_precompiled_variant(
Shader *task, Shader *mesh, Shader *frag,
const std::vector<std::pair<std::string, int>> &defines,
const ImmutableSamplerBank *sampler_bank = nullptr);
private:
Device *device;
ShaderTemplate *stages[static_cast<unsigned>(Vulkan::ShaderStage::Count)] = {};
VulkanCacheReadWrite<ShaderProgramVariant> variant_cache;
ShaderProgramVariant *register_variant(Shader * const *precompiled_shaders,
const std::vector<std::pair<std::string, int>> &defines,
const ImmutableSamplerBank *sampler_bank);
};
class ShaderManager
{
public:
explicit ShaderManager(Device *device_)
: device(device_)
{
}
bool load_shader_cache(const std::string &path, Granite::TaskGroup *shader_compilation_group);
bool save_shader_cache(const std::string &path);
void add_include_directory(const std::string &path);
~ShaderManager();
ShaderProgram *register_graphics(const std::string &task, const std::string &mesh, const std::string &fragment);
ShaderProgram *register_graphics(const std::string &vertex, const std::string &fragment);
ShaderProgram *register_compute(const std::string &compute);
#ifdef GRANITE_VULKAN_SHADER_MANAGER_RUNTIME_COMPILER
void register_dependency(ShaderTemplate *shader, const std::string &dependency);
void register_dependency_nolock(ShaderTemplate *shader, const std::string &dependency);
#endif
bool get_shader_hash_by_variant_hash(Util::Hash variant_hash, Util::Hash &shader_hash) const;
bool get_resource_layout_by_shader_hash(Util::Hash shader_hash, ResourceLayout &layout) const;
void register_shader_from_variant_hash(Util::Hash variant_hash, Util::Hash source_hash,
Util::Hash shader_hash, const ResourceLayout &layout);
Device *get_device()
{
return device;
}
void promote_read_write_caches_to_read_only();
private:
Device *device;
MetaCache meta_cache;
VulkanCache<ShaderTemplate> shaders;
VulkanCache<ShaderProgram> programs;
std::vector<std::string> include_directories;
ShaderTemplate *get_template(const std::string &source, ShaderStage force_stage);
#ifdef GRANITE_VULKAN_SHADER_MANAGER_RUNTIME_COMPILER
std::unordered_map<std::string, std::unordered_set<ShaderTemplate *>> dependees;
std::mutex dependency_lock;
#ifndef GRANITE_SHIPPING
// We'll never want to recompile shaders in runtime outside a dev environment.
struct Notify
{
Granite::FilesystemBackend *backend;
Granite::FileNotifyHandle handle;
};
std::unordered_map<std::string, Notify> directory_watches;
void add_directory_watch(const std::string &source);
void recompile(const Granite::FileNotifyInfo &info);
#endif
#endif
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,434 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "intrusive.hpp"
#include "object_pool.hpp"
#include "slab_allocator.hpp"
#include "intrusive_list.hpp"
#include "vulkan_headers.hpp"
#include "logging.hpp"
#include "bitops.hpp"
#include "enum_cast.hpp"
#include "vulkan_common.hpp"
#include "arena_allocator.hpp"
#include <assert.h>
#include <memory>
#include <stddef.h>
#include <stdint.h>
#include <vector>
namespace Vulkan
{
class Device;
enum class ImageLayout;
enum class MemoryClass : uint8_t
{
Small = 0,
Medium,
Large,
Huge,
Count
};
enum class AllocationMode : uint8_t
{
LinearHostMappable = 0,
LinearDevice,
LinearDeviceHighPriority,
OptimalResource,
OptimalRenderTarget,
External,
Count
};
enum MemoryAccessFlag : uint32_t
{
MEMORY_ACCESS_WRITE_BIT = 1,
MEMORY_ACCESS_READ_BIT = 2,
MEMORY_ACCESS_READ_WRITE_BIT = MEMORY_ACCESS_WRITE_BIT | MEMORY_ACCESS_READ_BIT
};
using MemoryAccessFlags = uint32_t;
struct DeviceAllocation;
class DeviceAllocator;
class ClassAllocator;
class DeviceAllocator;
class Allocator;
class Device;
using MiniHeap = Util::LegionHeap<DeviceAllocation>;
struct DeviceAllocation
{
friend class Util::ArenaAllocator<ClassAllocator, DeviceAllocation>;
friend class ClassAllocator;
friend class Allocator;
friend class DeviceAllocator;
friend class Device;
friend class ImageResourceHolder;
public:
inline VkDeviceMemory get_memory() const
{
return base;
}
inline bool allocation_is_global() const
{
return !alloc && base;
}
inline uint32_t get_offset() const
{
return offset;
}
inline uint32_t get_size() const
{
return size;
}
inline uint32_t get_mask() const
{
return mask;
}
inline bool is_host_allocation() const
{
return host_base != nullptr;
}
static DeviceAllocation make_imported_allocation(VkDeviceMemory memory, VkDeviceSize size, uint32_t memory_type);
ExternalHandle export_handle(Device &device);
private:
VkDeviceMemory base = VK_NULL_HANDLE;
uint8_t *host_base = nullptr;
ClassAllocator *alloc = nullptr;
Util::IntrusiveList<MiniHeap>::Iterator heap = {};
uint32_t offset = 0;
uint32_t mask = 0;
uint32_t size = 0;
VkExternalMemoryHandleTypeFlags exportable_types = 0;
AllocationMode mode = AllocationMode::Count;
uint8_t memory_type = 0;
void free_global(DeviceAllocator &allocator, uint32_t size, uint32_t memory_type);
void free_immediate();
void free_immediate(DeviceAllocator &allocator);
};
class DeviceAllocationOwner;
struct DeviceAllocationDeleter
{
void operator()(DeviceAllocationOwner *owner);
};
class DeviceAllocationOwner : public Util::IntrusivePtrEnabled<DeviceAllocationOwner, DeviceAllocationDeleter, HandleCounter>
{
public:
friend class Util::ObjectPool<DeviceAllocationOwner>;
friend struct DeviceAllocationDeleter;
~DeviceAllocationOwner();
const DeviceAllocation &get_allocation() const;
private:
DeviceAllocationOwner(Device *device, const DeviceAllocation &alloc);
Device *device;
DeviceAllocation alloc;
};
using DeviceAllocationOwnerHandle = Util::IntrusivePtr<DeviceAllocationOwner>;
struct MemoryAllocateInfo
{
VkMemoryRequirements requirements = {};
VkMemoryPropertyFlags required_properties = 0;
AllocationMode mode = {};
};
class ClassAllocator : public Util::ArenaAllocator<ClassAllocator, DeviceAllocation>
{
public:
friend class Util::ArenaAllocator<ClassAllocator, DeviceAllocation>;
inline void set_global_allocator(DeviceAllocator *allocator, AllocationMode mode, uint32_t memory_type_)
{
global_allocator = allocator;
global_allocator_mode = mode;
memory_type = memory_type_;
}
inline void set_parent(ClassAllocator *allocator)
{
parent = allocator;
}
private:
ClassAllocator *parent = nullptr;
uint32_t memory_type = 0;
DeviceAllocator *global_allocator = nullptr;
AllocationMode global_allocator_mode = AllocationMode::Count;
// Implements curious recurring template pattern calls.
bool allocate_backing_heap(DeviceAllocation *allocation);
void free_backing_heap(DeviceAllocation *allocation);
void prepare_allocation(DeviceAllocation *allocation, Util::IntrusiveList<MiniHeap>::Iterator heap_itr,
const Util::SuballocationResult &suballoc);
};
class Allocator
{
public:
explicit Allocator(Util::ObjectPool<MiniHeap> &object_pool);
void operator=(const Allocator &) = delete;
Allocator(const Allocator &) = delete;
bool allocate(uint32_t size, uint32_t alignment, AllocationMode mode, DeviceAllocation *alloc);
bool allocate_global(uint32_t size, AllocationMode mode, DeviceAllocation *alloc);
bool allocate_dedicated(uint32_t size, AllocationMode mode, DeviceAllocation *alloc,
VkObjectType object_type, uint64_t object, ExternalHandle *external);
inline ClassAllocator &get_class_allocator(MemoryClass clazz, AllocationMode mode)
{
return classes[unsigned(clazz)][unsigned(mode)];
}
static void free(DeviceAllocation *alloc)
{
alloc->free_immediate();
}
void set_global_allocator(DeviceAllocator *allocator, uint32_t memory_type_)
{
memory_type = memory_type_;
for (auto &sub : classes)
for (int i = 0; i < Util::ecast(AllocationMode::Count); i++)
sub[i].set_global_allocator(allocator, AllocationMode(i), memory_type);
global_allocator = allocator;
}
private:
ClassAllocator classes[Util::ecast(MemoryClass::Count)][Util::ecast(AllocationMode::Count)];
DeviceAllocator *global_allocator = nullptr;
uint32_t memory_type = 0;
};
struct HeapBudget
{
VkDeviceSize max_size;
VkDeviceSize budget_size;
VkDeviceSize tracked_usage;
VkDeviceSize device_usage;
};
class DeviceAllocator
{
public:
void init(Device *device);
~DeviceAllocator();
bool allocate_generic_memory(uint32_t size, uint32_t alignment, AllocationMode mode, uint32_t memory_type,
DeviceAllocation *alloc);
bool allocate_buffer_memory(uint32_t size, uint32_t alignment, AllocationMode mode, uint32_t memory_type,
VkBuffer buffer, DeviceAllocation *alloc, ExternalHandle *external);
bool allocate_image_memory(uint32_t size, uint32_t alignment, AllocationMode mode, uint32_t memory_type,
VkImage image, bool force_no_dedicated, DeviceAllocation *alloc, ExternalHandle *external);
void garbage_collect();
void *map_memory(const DeviceAllocation &alloc, MemoryAccessFlags flags, VkDeviceSize offset, VkDeviceSize length);
void unmap_memory(const DeviceAllocation &alloc, MemoryAccessFlags flags, VkDeviceSize offset, VkDeviceSize length);
void get_memory_budget(HeapBudget *heaps);
bool internal_allocate(uint32_t size, uint32_t memory_type, AllocationMode mode,
VkDeviceMemory *memory, uint8_t **host_memory,
VkObjectType object_type, uint64_t dedicated_object, ExternalHandle *external);
void internal_free(uint32_t size, uint32_t memory_type, AllocationMode mode, VkDeviceMemory memory, bool is_mapped);
void internal_free_no_recycle(uint32_t size, uint32_t memory_type, VkDeviceMemory memory);
private:
Util::ObjectPool<MiniHeap> object_pool;
std::vector<std::unique_ptr<Allocator>> allocators;
Device *device = nullptr;
const VolkDeviceTable *table = nullptr;
VkPhysicalDeviceMemoryProperties mem_props;
VkDeviceSize atom_alignment = 1;
struct Allocation
{
VkDeviceMemory memory;
uint32_t size;
uint32_t type;
AllocationMode mode;
};
struct Heap
{
uint64_t size = 0;
std::vector<Allocation> blocks;
void garbage_collect(Device *device);
};
std::vector<Heap> heaps;
bool memory_heap_is_budget_critical[VK_MAX_MEMORY_HEAPS] = {};
void get_memory_budget_nolock(HeapBudget *heaps);
};
// Avoid cross-dependency in header.
class Buffer;
struct DescriptorBufferAllocation
{
inline VkDeviceSize get_offset() const { return backing_slice.offset; }
inline VkDeviceSize get_size() const { return backing_slice.count; }
// Internal detail.
Util::AllocatedSlice backing_slice;
};
using DescriptorCopyFunc = void (*)(uint8_t *, const uint8_t *, size_t size);
using DescriptorCopyNFunc = void (*)(uint8_t *, const uint8_t * const *, size_t count, size_t size);
struct CachedDescriptorPayload
{
uint8_t *ptr;
VkDescriptorType type;
uint32_t heap_index;
explicit operator bool() const { return ptr != nullptr; }
};
struct CachedImageView
{
VkImageView view; // For legacy and descriptor buffer.
// For DB, this is used all the time. For heap, only occasionally as needed,
// usually for bindless.
CachedDescriptorPayload sampled; // SHADER_READ_ONLY
CachedDescriptorPayload input_attachment; // INPUT_ATTACHMENT + read only (if applicable)
CachedDescriptorPayload input_attachment_feedback; // INPUT_ATTACHMENT + GENERAL (if applicable)
CachedDescriptorPayload storage; // For storage image, always GENERAL layout.
};
struct CachedBufferView
{
VkBufferView view;
CachedDescriptorPayload uniform;
CachedDescriptorPayload storage;
};
struct BufferViewCreateInfo;
class DescriptorBufferAllocator : private Util::SliceAllocator
{
public:
bool init(Device *device);
~DescriptorBufferAllocator();
void teardown();
struct HeapInfo
{
VkDeviceAddress va;
uint8_t *mapped;
VkDeviceSize reserved_offset;
VkDeviceSize size;
};
HeapInfo get_resource_heap() const { return resource_heap; }
// Only for descriptor_heap.
HeapInfo get_sampler_heap() const { return sampler_heap; }
DescriptorBufferAllocation allocate(VkDeviceSize size);
void free(const DescriptorBufferAllocation &alloc);
void free(const DescriptorBufferAllocation *alloc, size_t count);
uint32_t get_descriptor_size_for_type(VkDescriptorType type) const;
bool create_image_view(const VkImageViewCreateInfo &info, VkImageUsageFlags usage,
ImageLayout layout, CachedImageView &view);
void free_image_view(const CachedImageView &view);
bool create_buffer_view(const BufferViewCreateInfo &info, CachedBufferView &view);
void free_buffer_view(const CachedBufferView &view);
#define IMPL_TYPE(type, desc_type) \
inline void copy_##type(uint8_t *dst, const uint8_t *src) const { type##_copy.func(dst, src, type##_copy.size); } \
inline void copy_##type##_n(uint8_t *dst, const uint8_t * const *src, size_t count) const { type##_copy.func_n(dst, src, count, type##_copy.size); } \
inline CachedDescriptorPayload alloc_##type() { return { type##_copy.slab.allocate(), desc_type }; } \
inline void free_##type(uint8_t *ptr) { type##_copy.slab.free(ptr); }
IMPL_TYPE(combined_image, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER)
IMPL_TYPE(sampled_image, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE)
IMPL_TYPE(storage_image, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE)
IMPL_TYPE(sampler, VK_DESCRIPTOR_TYPE_SAMPLER)
IMPL_TYPE(input_attachment, VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT)
IMPL_TYPE(ubo, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
IMPL_TYPE(ssbo, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER)
IMPL_TYPE(uniform_texel, VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER)
IMPL_TYPE(storage_texel, VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER)
void free_cached_descriptors(const CachedDescriptorPayload *payloads, size_t count);
// On heap, this is a dummy handle.
VkSampler create_sampler(const VkSamplerCreateInfo *info);
void destroy_sampler(VkSampler sampler);
private:
Device *device = nullptr;
Buffer *resource_buffer = nullptr;
Buffer *sampler_buffer = nullptr;
Util::SliceBackingAllocatorVA backing_va;
VkDeviceSize alignment = 0;
VkDeviceSize sub_block_size = 0;
std::mutex lock;
HeapInfo resource_heap = {}, sampler_heap = {};
struct DescriptorTypeInfo
{
DescriptorCopyFunc func;
DescriptorCopyNFunc func_n;
size_t size;
Util::ThreadSafeSlabAllocator slab;
};
DescriptorTypeInfo sampled_image_copy, storage_image_copy, combined_image_copy, sampler_copy, input_attachment_copy;
DescriptorTypeInfo ubo_copy, ssbo_copy, uniform_texel_copy, storage_texel_copy;
void init_copy_func(DescriptorTypeInfo &info, VkDescriptorType type) const;
VkDeviceSize total_size = 0;
VkDeviceSize high_water_mark = 0;
std::vector<uint32_t> heap_resource_indices;
std::vector<uint32_t> heap_sampler_indices;
// For descriptor heap.
uint32_t allocate_single_resource_heap_entry();
void free_single_resource_heap_entry(uint32_t index);
};
}
@@ -0,0 +1,281 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "meshlet.hpp"
#include "command_buffer.hpp"
#include "buffer.hpp"
#include "device.hpp"
#include "filesystem.hpp"
namespace Vulkan
{
namespace Meshlet
{
MeshView create_mesh_view(const Granite::FileMapping &mapping)
{
MeshView view = {};
if (mapping.get_size() < sizeof(magic) + sizeof(FormatHeader))
{
LOGE("MESHLET2 file too small.\n");
return view;
}
auto *ptr = mapping.data<unsigned char>();
auto *end_ptr = ptr + mapping.get_size();
if (memcmp(ptr, magic, sizeof(magic)) != 0)
{
LOGE("Invalid MESHLET2 magic.\n");
return {};
}
ptr += sizeof(magic);
view.format_header = reinterpret_cast<const FormatHeader *>(ptr);
ptr += sizeof(*view.format_header);
if (end_ptr - ptr < ptrdiff_t(view.format_header->meshlet_count * sizeof(Bound)))
return {};
view.bounds = reinterpret_cast<const Bound *>(ptr);
ptr += view.format_header->meshlet_count * sizeof(Bound);
size_t num_bounds_256 = (view.format_header->meshlet_count + ChunkFactor - 1) / ChunkFactor;
if (end_ptr - ptr < ptrdiff_t(num_bounds_256 * sizeof(Bound)))
return {};
view.bounds_256 = reinterpret_cast<const Bound *>(ptr);
ptr += num_bounds_256 * sizeof(Bound);
view.num_bounds = view.format_header->meshlet_count;
view.num_bounds_256 = num_bounds_256;
if (end_ptr - ptr < ptrdiff_t(view.format_header->meshlet_count * view.format_header->stream_count * sizeof(Stream)))
return {};
view.streams = reinterpret_cast<const Stream *>(ptr);
ptr += view.format_header->meshlet_count * view.format_header->stream_count * sizeof(Stream);
if (!view.format_header->payload_size_words)
return {};
if (end_ptr - ptr < ptrdiff_t(view.format_header->payload_size_words * sizeof(PayloadWord)))
return {};
view.payload = reinterpret_cast<const PayloadWord *>(ptr);
for (uint32_t i = 0, n = view.format_header->meshlet_count; i < n; i++)
{
auto counts = view.streams[i * view.format_header->stream_count].u.counts;
view.total_primitives += counts.prim_count;
view.total_vertices += counts.vert_count;
}
return view;
}
static void upload_indirect_buffer(CommandBuffer &cmd, const Vulkan::Buffer &indirect_buffer, uint32_t alloc_offset,
const MeshView &view, RuntimeStyle runtime_style,
uint32_t global_prim_offset, uint32_t global_vert_offset)
{
size_t total_padded_meshlets = view.num_bounds_256 * ChunkFactor;
size_t total_meshlets = view.format_header->meshlet_count;
uint32_t prim_offset = global_prim_offset;
uint32_t vert_offset = global_vert_offset;
if (runtime_style == RuntimeStyle::Meshlet)
{
constexpr size_t Stride = sizeof(RuntimeHeaderDecoded) * ChunkFactor;
auto *indirect = static_cast<RuntimeHeaderDecoded *>(
cmd.update_buffer(indirect_buffer, alloc_offset * Stride,
view.num_bounds_256 * Stride));
for (uint32_t i = 0; i < total_meshlets; i++)
{
auto &counts = view.streams[i * view.format_header->stream_count].u.counts;
uint32_t prim_count = counts.prim_count;
uint32_t vert_count = counts.vert_count;
indirect[i].primitive_offset = prim_offset;
indirect[i].vertex_offset = vert_offset;
indirect[i].primitive_count = prim_count;
indirect[i].vertex_count = vert_count;
prim_offset += prim_count;
vert_offset += vert_count;
}
memset(indirect + total_meshlets, 0,
(total_padded_meshlets - total_meshlets) * sizeof(RuntimeHeaderDecoded));
}
else
{
constexpr size_t Stride = sizeof(RuntimeHeaderDecodedMDI);
auto *indirect = static_cast<RuntimeHeaderDecodedMDI *>(
cmd.update_buffer(indirect_buffer, alloc_offset * Stride,
view.num_bounds_256 * Stride));
for (uint32_t i = 0; i < view.num_bounds_256; i++)
{
uint32_t chunks = std::min<uint32_t>(total_meshlets - i * ChunkFactor, ChunkFactor);
RuntimeHeaderDecodedMDI draw = {};
draw.firstIndex = 3 * prim_offset;
draw.vertexOffset = int32_t(vert_offset);
for (uint32_t chunk = 0; chunk < chunks; chunk++)
{
auto &counts = view.streams[(i * ChunkFactor + chunk) *
view.format_header->stream_count].u.counts;
draw.indexCount += counts.prim_count;
vert_offset += counts.vert_count;
prim_offset += counts.prim_count;
}
draw.indexCount *= 3;
indirect[i] = draw;
}
}
}
bool decode_mesh(CommandBuffer &cmd, const DecodeInfo &info, const MeshView &view)
{
if (!cmd.get_device().supports_subgroup_size_log2(true, 5, 7))
{
LOGE("Device does not support subgroup paths.\n");
return false;
}
if (!info.streams[0])
{
LOGE("Decode stream 0 must be set.\n");
return false;
}
if (!info.ibo)
{
LOGE("Output IBO must be set.\n");
return false;
}
BufferCreateInfo buf_info = {};
buf_info.domain = BufferDomain::LinkedDeviceHost;
buf_info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
buf_info.size = view.format_header->meshlet_count * view.format_header->stream_count * sizeof(*view.streams);
auto meshlet_stream_buffer = cmd.get_device().create_buffer(buf_info, view.streams);
bool meshlet_runtime = info.runtime_style == RuntimeStyle::Meshlet;
cmd.set_program("builtin://shaders/decode/meshlet_decode.comp");
cmd.enable_subgroup_size_control(true);
if (cmd.get_device().supports_subgroup_size_log2(true, 5, 5))
cmd.set_subgroup_size_log2(true, 5, 5);
else
cmd.set_subgroup_size_log2(true, 5, 7);
cmd.set_storage_buffer(0, 0, *meshlet_stream_buffer);
cmd.set_storage_buffer(0, 1, *info.payload);
cmd.set_storage_buffer(0, 2, *info.ibo);
cmd.set_specialization_constant_mask(0x1f);
cmd.set_specialization_constant(0, view.format_header->stream_count);
cmd.set_specialization_constant(1, (info.flags & DECODE_MODE_UNROLLED_MESH) != 0);
cmd.set_specialization_constant(2, uint32_t(info.target_style));
cmd.set_specialization_constant(3, uint32_t(meshlet_runtime));
cmd.set_specialization_constant(4, (info.flags & DECODE_MODE_INDEX_16) != 0);
for (unsigned i = 0; i < 3; i++)
cmd.set_storage_buffer(0, 3 + i, info.streams[i] ? *info.streams[i] : *info.streams[0]);
struct Offsets
{
uint32_t primitive_output_offset;
uint32_t vertex_output_offset;
uint32_t index_offset;
};
std::vector<Offsets> decode_offsets;
Offsets offsets = {};
decode_offsets.reserve(view.format_header->meshlet_count);
for (uint32_t i = 0; i < view.format_header->meshlet_count; i++)
{
if (info.runtime_style == RuntimeStyle::MDI && (info.flags & DECODE_MODE_UNROLLED_MESH) == 0)
{
uint32_t mdi_start_index = i & ~(Meshlet::ChunkFactor - 1);
if (mdi_start_index == i)
offsets.index_offset = 0;
}
decode_offsets.push_back(offsets);
auto &counts = view.streams[i * view.format_header->stream_count].u.counts;
offsets.primitive_output_offset += counts.prim_count;
offsets.vertex_output_offset += counts.vert_count;
if (!meshlet_runtime)
offsets.index_offset += counts.vert_count;
}
buf_info.domain = BufferDomain::LinkedDeviceHost;
buf_info.size = decode_offsets.size() * sizeof(decode_offsets.front());
auto output_offsets_buffer = cmd.get_device().create_buffer(buf_info, decode_offsets.data());
cmd.set_storage_buffer(0, 6, *output_offsets_buffer);
uint32_t wg_x = (view.format_header->meshlet_count + 7) / 8;
struct Push
{
uint32_t primitive_offset;
uint32_t vertex_offset;
uint32_t meshlet_count;
uint32_t wg_offset;
} push = {};
push.primitive_offset = info.push.primitive_offset;
push.vertex_offset = info.push.vertex_offset;
push.meshlet_count = view.format_header->meshlet_count;
push.wg_offset = 0;
const uint32_t max_wgx = cmd.get_device().get_gpu_properties().limits.maxComputeWorkGroupCount[0];
for (uint32_t i = 0; i < wg_x; i += max_wgx)
{
uint32_t to_dispatch = std::min<uint32_t>(wg_x - i, max_wgx);
push.wg_offset = i;
cmd.push_constants(&push, 0, sizeof(push));
cmd.dispatch(to_dispatch, 1, 1);
}
cmd.set_specialization_constant_mask(0);
cmd.enable_subgroup_size_control(false);
if (info.indirect)
{
upload_indirect_buffer(cmd, *info.indirect, info.indirect_offset, view, info.runtime_style,
info.push.primitive_offset, info.push.vertex_offset);
}
return true;
}
}
}
@@ -0,0 +1,160 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <stdint.h>
namespace Granite
{
class FileMapping;
}
namespace Vulkan
{
class CommandBuffer;
class Buffer;
}
namespace Vulkan
{
// MESHLET1 format.
namespace Meshlet
{
static constexpr unsigned MaxStreams = 8;
static constexpr unsigned MaxElements = 32;
static constexpr unsigned ChunkFactor = 256 / MaxElements;
struct Stream
{
union
{
uint32_t base_value[2];
struct { uint32_t prim_count; uint32_t vert_count; } counts;
} u;
uint32_t bits;
uint32_t offset_in_words;
};
static_assert(sizeof(Stream) == 16, "Unexpected Stream size.");
struct RuntimeHeaderEncoded
{
uint32_t stream_offset;
};
struct RuntimeHeaderDecoded
{
uint32_t primitive_offset;
uint32_t vertex_offset;
uint32_t primitive_count;
uint32_t vertex_count;
};
struct RuntimeHeaderDecodedMDI
{
uint32_t indexCount;
uint32_t firstIndex;
int32_t vertexOffset;
};
struct Bound
{
float center[3];
float radius;
float cone_axis_cutoff[4];
};
enum class StreamType
{
Primitive = 0, // RGB8_UINT (fixed 5-bit encoding, fixed base value of 0)
Position, // RGB16_SINT * 2^aux
NormalTangentOct8, // Octahedron encoding in RG8, BA8 for tangent. Following uvec4 encodes 1-bit sign.
UV, // (0.5 * (R16G16_SINT * 2^aux) + 0.5
BoneIndices, // RGBA8_UINT
BoneWeights, // RGBA8_UNORM
};
enum class MeshStyle : uint32_t
{
Wireframe = 0, // Primitive + Position
Textured, // Untextured + TangentOct8 + UV
Skinned // Textured + Bone*
};
struct FormatHeader
{
MeshStyle style;
uint32_t stream_count;
uint32_t meshlet_count;
uint32_t payload_size_words;
};
using PayloadWord = uint32_t;
struct MeshView
{
const FormatHeader *format_header;
const Bound *bounds;
const Bound *bounds_256;
const Stream *streams;
const PayloadWord *payload;
uint32_t total_primitives;
uint32_t total_vertices;
uint32_t num_bounds;
uint32_t num_bounds_256;
};
static const char magic[8] = { 'M', 'E', 'S', 'H', 'L', 'E', 'T', '4' };
MeshView create_mesh_view(const Granite::FileMapping &mapping);
enum DecodeModeFlagBits : uint32_t
{
DECODE_MODE_UNROLLED_MESH = 1 << 0,
DECODE_MODE_INDEX_16 = 1 << 1,
};
using DecodeModeFlags = uint32_t;
enum class RuntimeStyle
{
MDI,
Meshlet
};
struct DecodeInfo
{
const Vulkan::Buffer *ibo, *streams[3], *indirect, *payload;
DecodeModeFlags flags;
MeshStyle target_style;
RuntimeStyle runtime_style;
struct
{
uint32_t primitive_offset;
uint32_t vertex_offset;
} push;
uint32_t indirect_offset;
};
bool decode_mesh(Vulkan::CommandBuffer &cmd, const DecodeInfo &decode_info, const MeshView &view);
}
}
@@ -0,0 +1,680 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "pipeline_cache.hpp"
#include "device.hpp"
namespace Vulkan
{
PipelineCache::Binary::Binary(const VkPipelineBinaryKeyKHR &key_, const void *payload_, size_t payload_size_)
: device(nullptr), key(key_), payload(payload_), payload_size(payload_size_)
{
}
PipelineCache::Binary::Binary(Vulkan::Device &device_, const VkPipelineBinaryKeyKHR &key_, VkPipelineBinaryKHR binary_)
: device(&device_), key(key_), binary(binary_)
{
VkPipelineBinaryDataInfoKHR data_info = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_DATA_INFO_KHR };
VkPipelineBinaryKeyKHR dummy_key = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR };
data_info.pipelineBinary = binary;
device->get_device_table().vkGetPipelineBinaryDataKHR(device->get_device(), &data_info, &dummy_key, &payload_size, nullptr);
}
PipelineCache::Binary::~Binary()
{
if (device)
device->get_device_table().vkDestroyPipelineBinaryKHR(device->get_device(), binary, nullptr);
}
PipelineCache::PipelineCache(Device *device_)
: device(*device_), new_entries(false)
{
}
PipelineCache::~PipelineCache()
{
}
Util::Hash PipelineCache::get_create_info_key(const void *create_info) const
{
VkPipelineCreateInfoKHR key_create_info = { VK_STRUCTURE_TYPE_PIPELINE_CREATE_INFO_KHR };
VkPipelineBinaryKeyKHR global_key = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR };
key_create_info.pNext = const_cast<void *>(create_info);
if (device.get_device_table().vkGetPipelineKeyKHR(device.get_device(), &key_create_info, &global_key) != VK_SUCCESS)
return false;
Util::Hasher h;
h.data(global_key.key, global_key.keySize);
return h.get();
}
bool PipelineCache::place_binary(VkPipelineBinaryKHR binary, Util::Hash *hash)
{
VkPipelineBinaryDataInfoKHR data_info = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_DATA_INFO_KHR };
VkPipelineBinaryKeyKHR key = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR };
data_info.pipelineBinary = binary;
size_t data_size = 0;
if (device.get_device_table().vkGetPipelineBinaryDataKHR(
device.get_device(), &data_info, &key, &data_size, nullptr) != VK_SUCCESS)
{
LOGE("Failed to get pipeline binary key.\n");
return false;
}
VK_ASSERT(key.keySize);
Util::Hasher h;
h.data(key.key, key.keySize);
*hash = h.get();
static constexpr uint32_t AllZero[VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR] = {};
if (memcmp(AllZero, key.key, VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR) == 0)
{
LOGW("Driver seems broken? Key is all zeros ...\n");
return false;
}
if (binaries.find(h.get()))
device.get_device_table().vkDestroyPipelineBinaryKHR(device.get_device(), binary, nullptr);
else
binaries.emplace_yield(h.get(), device, key, binary);
return true;
}
void PipelineCache::place_pipeline(Util::Hash hash, VkPipeline pipeline)
{
const auto release_binaries = [&]()
{
VkReleaseCapturedPipelineDataInfoKHR release_info = { VK_STRUCTURE_TYPE_RELEASE_CAPTURED_PIPELINE_DATA_INFO_KHR };
release_info.pipeline = pipeline;
device.get_device_table().vkReleaseCapturedPipelineDataKHR(device.get_device(), &release_info, nullptr);
};
if (binary_mapping.find(hash) != nullptr)
{
release_binaries();
return;
}
VkPipelineBinaryCreateInfoKHR create_info = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_CREATE_INFO_KHR };
create_info.pipeline = pipeline;
VkPipelineBinaryHandlesInfoKHR handles_info = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_HANDLES_INFO_KHR };
if (device.get_device_table().vkCreatePipelineBinariesKHR(
device.get_device(), &create_info, nullptr, &handles_info) != VK_SUCCESS ||
handles_info.pipelineBinaryCount == 0)
{
LOGE("Failed to query pipeline binaries from pipeline.\n");
release_binaries();
return;
}
Util::SmallVector<VkPipelineBinaryKHR> out_binaries(handles_info.pipelineBinaryCount);
handles_info.pPipelineBinaries = out_binaries.data();
if (device.get_device_table().vkCreatePipelineBinariesKHR(
device.get_device(), &create_info, nullptr, &handles_info) != VK_SUCCESS)
{
LOGE("Failed to query pipeline binaries from pipeline.\n");
release_binaries();
return;
}
release_binaries();
Util::SmallVector<Util::Hash> keys;
keys.resize(out_binaries.size());
auto *pkeys = keys.data();
for (auto &binary : out_binaries)
if (!place_binary(binary, pkeys++))
return;
binary_mapping.emplace_yield(hash, std::move(keys));
new_entries.store(true, std::memory_order_release);
}
bool PipelineCache::find_pipeline_binaries_from_internal_cache(const void *pso_create_info,
Util::SmallVector<VkPipelineBinaryKHR> &out_binaries,
Util::SmallVector<bool> &out_binaries_owned)
{
out_binaries.clear();
out_binaries_owned.clear();
VkPipelineBinaryCreateInfoKHR create_info = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_CREATE_INFO_KHR };
VkPipelineCreateInfoKHR pipeline_create_info = { VK_STRUCTURE_TYPE_PIPELINE_CREATE_INFO_KHR, const_cast<void *>(pso_create_info) };
create_info.pPipelineCreateInfo = &pipeline_create_info;
out_binaries.resize(32);
// Ideally we don't query twice, just assume we're not going to receive more than 32 binaries in one go.
// For graphics and compute, this is surely fine ... :')
VkPipelineBinaryHandlesInfoKHR handles_info = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_HANDLES_INFO_KHR };
handles_info.pPipelineBinaries = out_binaries.data();
handles_info.pipelineBinaryCount = 32;
auto result = device.get_device_table().vkCreatePipelineBinariesKHR(
device.get_device(), &create_info, nullptr, &handles_info);
out_binaries.resize(handles_info.pipelineBinaryCount);
if (result != VK_SUCCESS)
{
for (auto &b : out_binaries)
device.get_device_table().vkDestroyPipelineBinaryKHR(device.get_device(), b, nullptr);
out_binaries.clear();
return false;
}
for (uint32_t i = 0; i < handles_info.pipelineBinaryCount; i++)
out_binaries_owned.push_back(true);
return true;
}
bool PipelineCache::find_pipeline_binaries(Util::Hash pso_hash,
Util::SmallVector<VkPipelineBinaryKHR> &out_binaries,
Util::SmallVector<bool> &out_binaries_owned)
{
auto *mapped = binary_mapping.find(pso_hash);
if (!mapped)
return false;
out_binaries.clear();
out_binaries_owned.clear();
for (auto &hash : mapped->hashes)
{
auto *existing_binary = binaries.find(hash);
if (!existing_binary)
{
for (auto &binary: out_binaries)
device.get_device_table().vkDestroyPipelineBinaryKHR(device.get_device(), binary, nullptr);
out_binaries.clear();
return false;
}
VkPipelineBinaryKHR binary = VK_NULL_HANDLE;
if (existing_binary->binary)
{
binary = existing_binary->binary;
out_binaries_owned.push_back(false);
}
else
{
VkPipelineBinaryCreateInfoKHR create_info = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_CREATE_INFO_KHR };
VkPipelineBinaryKeysAndDataKHR keys_and_data_info = {};
VkPipelineBinaryDataKHR binary_data = {};
keys_and_data_info.binaryCount = 1;
keys_and_data_info.pPipelineBinaryKeys = &existing_binary->key;
VK_ASSERT(existing_binary->key.keySize);
keys_and_data_info.pPipelineBinaryData = &binary_data;
create_info.pKeysAndDataInfo = &keys_and_data_info;
binary_data.pData = const_cast<void *>(existing_binary->payload);
binary_data.dataSize = existing_binary->payload_size;
VkPipelineBinaryHandlesInfoKHR handles_info = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_HANDLES_INFO_KHR };
handles_info.pPipelineBinaries = &binary;
handles_info.pipelineBinaryCount = 1;
if (device.get_device_table().vkCreatePipelineBinariesKHR(
device.get_device(), &create_info, nullptr, &handles_info) != VK_SUCCESS ||
handles_info.pipelineBinaryCount != 1 ||
handles_info.pPipelineBinaries[0] == VK_NULL_HANDLE)
{
for (auto &b : out_binaries)
device.get_device_table().vkDestroyPipelineBinaryKHR(device.get_device(), b, nullptr);
out_binaries.clear();
return false;
}
}
out_binaries_owned.push_back(existing_binary->binary == VK_NULL_HANDLE);
out_binaries.push_back(binary);
}
return true;
}
bool PipelineCache::init_from_payload(const void *payload, size_t size, bool persistent_mapping)
{
if (!size)
return true;
if (!persistent_mapping)
{
payload_holder.reset(new uint8_t[size]);
memcpy(payload_holder.get(), payload, size);
payload = payload_holder.get();
}
if (!parse(payload, size))
return false;
return true;
}
static constexpr char CacheUUID[VK_UUID_SIZE] = "GraniteBinary1";
bool PipelineCache::parse(const void *payload_, size_t size)
{
if (!device.get_device_features().pipeline_binary_features.pipelineBinaries)
return false;
constexpr size_t minimum_size = VK_UUID_SIZE + sizeof(uint32_t) +
VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR + sizeof(uint32_t);
if (size < minimum_size)
return false;
auto *payload = static_cast<const uint8_t *>(payload_);
if (memcmp(payload, CacheUUID, sizeof(CacheUUID)) != 0)
return false;
payload += VK_UUID_SIZE;
VkPipelineBinaryKeyKHR key = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR };
device.get_device_table().vkGetPipelineKeyKHR(device.get_device(), nullptr, &key);
if (memcmp(payload, &key.keySize, sizeof(uint32_t)) != 0)
{
LOGW("Pipeline binary global key changed, resetting the cache ...\n");
return true;
}
payload += sizeof(uint32_t);
if (memcmp(payload, key.key, key.keySize) != 0)
{
LOGW("Pipeline binary global key changed, resetting the cache ...\n");
return true;
}
payload += VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR;
uint32_t num_pipelines = *reinterpret_cast<const uint32_t *>(payload);
payload += sizeof(uint32_t);
auto *payload64 = reinterpret_cast<const uint64_t *>(payload);
size -= minimum_size;
const auto read_u64 = [&]() -> uint64_t {
if (size >= sizeof(uint64_t))
{
auto data = *payload64++;
size -= sizeof(uint64_t);
return data;
}
else
return 0;
};
for (uint32_t i = 0; i < num_pipelines; i++)
{
Util::SmallVector<Util::Hash> hashes;
auto hash = read_u64();
auto num_hashes = uint32_t(read_u64());
for (uint32_t j = 0; j < num_hashes; j++)
hashes.push_back(read_u64());
binary_mapping.emplace_yield(hash, std::move(hashes));
}
auto num_binaries = uint32_t(read_u64());
for (uint32_t i = 0; i < num_binaries; i++)
{
auto hash = read_u64();
union
{
struct
{
uint32_t size;
uint32_t key_size;
};
uint64_t word;
} u;
u.word = read_u64();
if (size < VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR)
return false;
key.keySize = u.key_size;
memcpy(key.key, payload64, sizeof(key.key));
payload64 += VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR / sizeof(uint64_t);
size -= VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR;
auto padded_size = (u.size + sizeof(uint64_t) - 1) & ~(sizeof(uint64_t) - 1);
if (size < padded_size)
return false;
binaries.emplace_yield(hash, key, payload64, u.size);
payload64 += padded_size / sizeof(uint64_t);
size -= padded_size;
}
if (size == 0)
LOGI("Successfully parsed %u pipelines and %u binary blobs.\n", num_pipelines, num_binaries);
return size == 0;
}
bool PipelineCache::has_new_binary_entries() const
{
return new_entries.load(std::memory_order_acquire);
}
size_t PipelineCache::get_serialized_size() const
{
// Granite's magic UUID.
size_t size = VK_UUID_SIZE;
// Driver's global key.
size += sizeof(uint32_t);
size += VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR;
// Pipeline number count.
size += sizeof(uint32_t);
for (auto &mapping : binary_mapping.get_thread_unsafe())
{
// Count + Keys per pipeline.
size += sizeof(Util::Hash) + sizeof(uint64_t) + mapping.hashes.size() * sizeof(Util::Hash);
}
// Binary count.
size += sizeof(uint64_t);
for (auto &binary : binaries.get_thread_unsafe())
{
size += sizeof(Util::Hash); // Hash
size += sizeof(uint32_t); // Size
size += sizeof(uint32_t); // Key size
size += VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR;
size += (binary.payload_size + 7) & ~size_t(7); // Padded payload
}
return size;
}
bool PipelineCache::serialize(void *data_, size_t size) const
{
if (size < get_serialized_size())
return false;
auto *data = static_cast<uint8_t *>(data_);
memcpy(data, CacheUUID, sizeof(CacheUUID));
data += VK_UUID_SIZE;
VkPipelineBinaryKeyKHR key = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR };
device.get_device_table().vkGetPipelineKeyKHR(device.get_device(), nullptr, &key);
memcpy(data, &key.keySize, sizeof(key.keySize));
data += sizeof(uint32_t);
memcpy(data, key.key, sizeof(key.key));
data += sizeof(key.key);
uint32_t pipeline_count = 0;
for (auto &mapping : binary_mapping.get_thread_unsafe())
{
(void)mapping;
pipeline_count++;
}
memcpy(data, &pipeline_count, sizeof(pipeline_count));
data += sizeof(uint32_t);
auto *data64 = reinterpret_cast<uint64_t *>(data);
for (auto &mapping : binary_mapping.get_thread_unsafe())
{
*data64++ = mapping.get_hash();
*data64++ = mapping.hashes.size();
for (auto &hash : mapping.hashes)
*data64++ = hash;
}
uint32_t binary_count = 0;
for (auto &mapping : binaries.get_thread_unsafe())
{
(void)mapping;
binary_count++;
}
*data64++ = binary_count;
for (auto &mapping : binaries.get_thread_unsafe())
{
*data64++ = mapping.get_hash();
const uint32_t words[] = { uint32_t(mapping.payload_size), mapping.key.keySize };
memcpy(data64, words, sizeof(words));
data64++;
memcpy(data64, mapping.key.key, sizeof(mapping.key.key));
data64 += VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR / sizeof(uint64_t);
VK_ASSERT(mapping.binary || mapping.payload);
if (mapping.binary)
{
// TODO: Ignore compressed property for now.
VkPipelineBinaryDataInfoKHR data_info = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_DATA_INFO_KHR };
VkPipelineBinaryKeyKHR dummy_key = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR };
data_info.pipelineBinary = mapping.binary;
size_t payload_size = mapping.payload_size;
device.get_device_table().vkGetPipelineBinaryDataKHR(device.get_device(), &data_info, &dummy_key,
&payload_size, data64);
}
else
{
memcpy(data64, mapping.payload, mapping.payload_size);
}
data64 += (mapping.payload_size + sizeof(uint64_t) - 1) / sizeof(uint64_t);
}
LOGI("Serialized %u pipelines and %u binary blobs.\n", pipeline_count, binary_count);
return true;
}
template <typename T>
static inline const T *find_pnext(VkStructureType type, const void *pNext)
{
while (pNext != nullptr)
{
auto *sin = static_cast<const VkBaseInStructure *>(pNext);
if (sin->sType == type)
return static_cast<const T*>(pNext);
pNext = sin->pNext;
}
return nullptr;
}
VkPipeline PipelineCache::create_pipeline_and_place(Util::Hash pso_key, void *plain_info)
{
auto *graphics_info = static_cast<VkGraphicsPipelineCreateInfo *>(plain_info);
auto *compute_info = static_cast<VkComputePipelineCreateInfo *>(plain_info);
if (graphics_info && graphics_info->sType != VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO)
graphics_info = nullptr;
if (compute_info && compute_info->sType != VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO)
compute_info = nullptr;
VkPipelineCreateFlags2CreateInfoKHR flags2 = { VK_STRUCTURE_TYPE_PIPELINE_CREATE_FLAGS_2_CREATE_INFO_KHR };
VkPipeline pipe = VK_NULL_HANDLE;
if (!device.get_device_features().pipeline_binary_properties.pipelineBinaryPrefersInternalCache)
{
auto *existing_flags2 = find_pnext<VkPipelineCreateFlags2CreateInfoKHR>(
VK_STRUCTURE_TYPE_PIPELINE_CREATE_FLAGS_2_CREATE_INFO_KHR, plain_info);
if (existing_flags2)
{
const_cast<VkPipelineCreateFlags2CreateInfoKHR *>(existing_flags2)->flags |=
VK_PIPELINE_CREATE_2_CAPTURE_DATA_BIT_KHR;
}
else
{
flags2.flags = VK_PIPELINE_CREATE_2_CAPTURE_DATA_BIT_KHR;
if (graphics_info)
{
flags2.flags |= graphics_info->flags;
flags2.pNext = graphics_info->pNext;
graphics_info->pNext = &flags2;
}
else if (compute_info)
{
flags2.flags |= compute_info->flags;
flags2.pNext = compute_info->pNext;
compute_info->pNext = &flags2;
}
}
}
if ((compute_info && device.get_device_table().vkCreateComputePipelines(
device.get_device(), VK_NULL_HANDLE, 1, compute_info, nullptr, &pipe) != VK_SUCCESS) ||
(graphics_info && device.get_device_table().vkCreateGraphicsPipelines(
device.get_device(), VK_NULL_HANDLE, 1, graphics_info, nullptr, &pipe) != VK_SUCCESS))
{
LOGE("Failed to create pipeline from binaries.\n");
pipe = VK_NULL_HANDLE;
}
if (!device.get_device_features().pipeline_binary_properties.pipelineBinaryPrefersInternalCache &&
pipe != VK_NULL_HANDLE)
{
place_pipeline(pso_key, pipe);
}
return pipe;
}
VkPipeline
PipelineCache::create_pipeline_from_binaries(
void *plain_info, const VkPipelineBinaryKHR *found_binaries,
const bool *binaries_owned, size_t binary_count)
{
auto *graphics_info = static_cast<VkGraphicsPipelineCreateInfo *>(plain_info);
auto *compute_info = static_cast<VkComputePipelineCreateInfo *>(plain_info);
if (graphics_info && graphics_info->sType != VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO)
graphics_info = nullptr;
if (compute_info && compute_info->sType != VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO)
compute_info = nullptr;
// Cache hit :3
VkPipelineBinaryInfoKHR binary_info = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_INFO_KHR };
binary_info.pPipelineBinaries = found_binaries;
binary_info.binaryCount = binary_count;
constexpr VkPipelineCreateFlags invalid_flags =
VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT |
VK_PIPELINE_CREATE_EARLY_RETURN_ON_FAILURE_BIT;
if (compute_info)
{
compute_info->stage.module = VK_NULL_HANDLE;
binary_info.pNext = compute_info->pNext;
compute_info->pNext = &binary_info;
compute_info->flags &= ~invalid_flags;
}
else if (graphics_info)
{
for (uint32_t i = 0; i < graphics_info->stageCount; i++)
const_cast<VkPipelineShaderStageCreateInfo &>(graphics_info->pStages[i]).module = VK_NULL_HANDLE;
binary_info.pNext = graphics_info->pNext;
graphics_info->pNext = &binary_info;
graphics_info->flags &= ~invalid_flags;
}
VkPipeline pipe = VK_NULL_HANDLE;
if ((compute_info && device.get_device_table().vkCreateComputePipelines(
device.get_device(), VK_NULL_HANDLE, 1, compute_info, nullptr, &pipe) != VK_SUCCESS) ||
(graphics_info && device.get_device_table().vkCreateGraphicsPipelines(
device.get_device(), VK_NULL_HANDLE, 1, graphics_info, nullptr, &pipe) != VK_SUCCESS))
{
LOGE("Failed to create pipeline from binaries.\n");
pipe = VK_NULL_HANDLE;
}
for (size_t i = 0; i < binary_count; i++)
if (binaries_owned[i])
device.get_device_table().vkDestroyPipelineBinaryKHR(device.get_device(), found_binaries[i], nullptr);
return pipe;
}
VkResult PipelineCache::create_pipeline(void *plain_info, VkPipelineCache cache, VkPipeline *pipe)
{
*pipe = VK_NULL_HANDLE;
auto *graphics_info = static_cast<VkGraphicsPipelineCreateInfo *>(plain_info);
auto *compute_info = static_cast<VkComputePipelineCreateInfo *>(plain_info);
if (graphics_info && graphics_info->sType != VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO)
graphics_info = nullptr;
if (compute_info && compute_info->sType != VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO)
compute_info = nullptr;
if (!device.get_device_features().pipeline_binary_features.pipelineBinaries)
{
if (compute_info)
{
return device.get_device_table().vkCreateComputePipelines(
device.get_device(), cache, 1, compute_info, nullptr, pipe);
}
else if (graphics_info)
{
return device.get_device_table().vkCreateGraphicsPipelines(
device.get_device(), cache, 1, graphics_info, nullptr, pipe);
}
else
return VK_ERROR_INITIALIZATION_FAILED;
}
auto pso_key = get_create_info_key(plain_info);
Util::SmallVector<VkPipelineBinaryKHR> pipeline_binaries;
Util::SmallVector<bool> pipeline_binaries_owned;
if (find_pipeline_binaries(pso_key, pipeline_binaries, pipeline_binaries_owned))
{
*pipe = create_pipeline_from_binaries(plain_info, pipeline_binaries.data(), pipeline_binaries_owned.data(),
pipeline_binaries.size());
return *pipe ? VK_SUCCESS : VK_ERROR_OUT_OF_HOST_MEMORY;
}
if (device.get_device_features().pipeline_binary_properties.pipelineBinaryInternalCache &&
!device.get_device_features().pipeline_binary_internal_cache_control.disableInternalCache &&
find_pipeline_binaries_from_internal_cache(plain_info, pipeline_binaries, pipeline_binaries_owned))
{
*pipe = create_pipeline_from_binaries(plain_info, pipeline_binaries.data(), pipeline_binaries_owned.data(),
pipeline_binaries.size());
return *pipe ? VK_SUCCESS : VK_ERROR_OUT_OF_HOST_MEMORY;
}
if (graphics_info && (graphics_info->flags & VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT) != 0)
return VK_PIPELINE_COMPILE_REQUIRED;
if (compute_info && (compute_info->flags & VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT) != 0)
return VK_PIPELINE_COMPILE_REQUIRED;
*pipe = create_pipeline_and_place(pso_key, plain_info);
return *pipe ? VK_SUCCESS : VK_ERROR_OUT_OF_HOST_MEMORY;
}
}
@@ -0,0 +1,92 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <stddef.h>
#include "vulkan_headers.hpp"
#include "small_vector.hpp"
#include "intrusive_hash_map.hpp"
#include <memory>
#include <utility>
namespace Vulkan
{
class Device;
class PipelineCache
{
public:
explicit PipelineCache(Device *device);
~PipelineCache();
bool init_from_payload(const void *payload, size_t size, bool persistent_mapping);
bool has_new_binary_entries() const;
size_t get_serialized_size() const;
bool serialize(void *data, size_t size) const;
VkResult create_pipeline(void *info, VkPipelineCache cache, VkPipeline *pipe);
private:
Device &device;
std::unique_ptr<uint8_t []> payload_holder;
struct PipelineBinaryMapping : Util::IntrusiveHashMapEnabled<PipelineBinaryMapping>
{
explicit PipelineBinaryMapping(Util::SmallVector<Util::Hash> hashes_)
: hashes(std::move(hashes_)) {}
Util::SmallVector<Util::Hash> hashes;
};
Util::ThreadSafeIntrusiveHashMap<PipelineBinaryMapping> binary_mapping;
struct Binary : Util::IntrusiveHashMapEnabled<Binary>
{
Binary(Device &device, const VkPipelineBinaryKeyKHR &key, VkPipelineBinaryKHR binary);
Binary(const VkPipelineBinaryKeyKHR &key, const void *payload, size_t payload_size);
~Binary();
Device *device;
VkPipelineBinaryKeyKHR key = { VK_STRUCTURE_TYPE_PIPELINE_BINARY_KEY_KHR };
VkPipelineBinaryKHR binary = VK_NULL_HANDLE;
const void *payload = nullptr;
size_t payload_size = 0;
};
Util::ThreadSafeIntrusiveHashMap<Binary> binaries;
bool place_binary(VkPipelineBinaryKHR binary, Util::Hash *hash);
bool parse(const void *payload, size_t size);
std::atomic_bool new_entries;
Util::Hash get_create_info_key(const void *create_info) const;
bool find_pipeline_binaries(Util::Hash hash,
Util::SmallVector<VkPipelineBinaryKHR> &binaries,
Util::SmallVector<bool> &binaries_owned);
bool find_pipeline_binaries_from_internal_cache(const void *create_info,
Util::SmallVector<VkPipelineBinaryKHR> &binaries,
Util::SmallVector<bool> &binaries_owned);
void place_pipeline(Util::Hash hash, VkPipeline pipeline);
VkPipeline create_pipeline_from_binaries(
void *info, const VkPipelineBinaryKHR *binaries, const bool *binaries_owned, size_t binary_count);
VkPipeline create_pipeline_and_place(Util::Hash pso_key, void *info);
};
}
@@ -0,0 +1,43 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "pipeline_event.hpp"
#include "device.hpp"
namespace Vulkan
{
EventHolder::~EventHolder()
{
if (event)
{
if (internal_sync)
device->destroy_event_nolock(event);
else
device->destroy_event(event);
}
}
void EventHolderDeleter::operator()(Vulkan::EventHolder *event)
{
event->device->handle_pool.events.free(event);
}
}
@@ -0,0 +1,67 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_headers.hpp"
#include "vulkan_common.hpp"
#include "cookie.hpp"
#include "object_pool.hpp"
namespace Vulkan
{
class Device;
class EventHolder;
struct EventHolderDeleter
{
void operator()(EventHolder *event);
};
class EventHolder : public Util::IntrusivePtrEnabled<EventHolder, EventHolderDeleter, HandleCounter>,
public InternalSyncEnabled
{
public:
friend struct EventHolderDeleter;
~EventHolder();
const VkEvent &get_event() const
{
return event;
}
private:
friend class Util::ObjectPool<EventHolder>;
EventHolder(Device *device_, VkEvent event_)
: device(device_)
, event(event_)
{
}
Device *device;
VkEvent event;
};
using PipelineEvent = Util::IntrusivePtr<EventHolder>;
}
@@ -0,0 +1,25 @@
add_granite_internal_lib(granite-vulkan-post-mortem
post_mortem.cpp post_mortem.hpp)
target_include_directories(granite-vulkan-post-mortem PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(granite-vulkan-post-mortem PRIVATE granite-util)
if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)|(amd64)|(AMD64)")
set(AFTERMATH_ARCH x64)
else()
set(AFTERMATH_ARCH x86)
endif()
find_library(GFSDK_LIBRARY GFSDK_Aftermath_Lib.x64 HINTS ${AFTERMATH_SDK_PATH}/lib/${AFTERMATH_ARCH})
if (GFSDK_LIBRARY)
target_sources(granite-vulkan-post-mortem PRIVATE
NsightAftermathGpuCrashTracker.cpp NsightAftermathGpuCrashTracker.h
NsightAftermathHelpers.h)
target_compile_definitions(granite-vulkan-post-mortem PRIVATE HAVE_AFTERMATH_SDK)
target_link_libraries(granite-vulkan-post-mortem PRIVATE ${GFSDK_LIBRARY} granite-volk-headers)
target_include_directories(granite-vulkan-post-mortem PRIVATE ${AFTERMATH_SDK_PATH}/include)
message("Found Aftermath SDK.")
else()
message("Did not find Aftermath SDK in AFTERMATH_SDK_PATH=${AFTERMATH_SDK_PATH}.")
endif()
@@ -0,0 +1,422 @@
//*********************************************************
//
// Copyright (c) 2019-2022, NVIDIA CORPORATION. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//
//*********************************************************
#include <fstream>
#include <iomanip>
#include <string>
#include <array>
#include "logging.hpp"
#include "NsightAftermathGpuCrashTracker.h"
#ifdef _WIN32
#include <windows.h>
#endif
// Modified from the official sample to fit Granite better.
//*********************************************************
// GpuCrashTracker implementation
//*********************************************************
GpuCrashTracker::GpuCrashTracker(const MarkerMap& markerMap)
: m_initialized(false)
, m_mutex()
, m_shaderDebugInfo()
, m_markerMap(markerMap)
{
}
GpuCrashTracker::~GpuCrashTracker()
{
// If initialized, disable GPU crash dumps
if (m_initialized)
{
GFSDK_Aftermath_DisableGpuCrashDumps();
}
}
// Initialize the GPU Crash Dump Tracker
void GpuCrashTracker::Initialize()
{
// Enable GPU crash dumps and set up the callbacks for crash dump notifications,
// shader debug information notifications, and providing additional crash
// dump description data.Only the crash dump callback is mandatory. The other two
// callbacks are optional and can be omitted, by passing nullptr, if the corresponding
// functionality is not used.
// The DeferDebugInfoCallbacks flag enables caching of shader debug information data
// in memory. If the flag is set, ShaderDebugInfoCallback will be called only
// in the event of a crash, right before GpuCrashDumpCallback. If the flag is not set,
// ShaderDebugInfoCallback will be called for every shader that is compiled.
AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_EnableGpuCrashDumps(
GFSDK_Aftermath_Version_API,
GFSDK_Aftermath_GpuCrashDumpWatchedApiFlags_Vulkan,
GFSDK_Aftermath_GpuCrashDumpFeatureFlags_DeferDebugInfoCallbacks, // Let the Nsight Aftermath library cache shader debug information.
GpuCrashDumpCallback, // Register callback for GPU crash dumps.
ShaderDebugInfoCallback, // Register callback for shader debug information.
CrashDumpDescriptionCallback, // Register callback for GPU crash dump description.
ResolveMarkerCallback, // Register callback for resolving application-managed markers.
this)); // Set the GpuCrashTracker object as user data for the above callbacks.
m_initialized = true;
}
// Handler for GPU crash dump callbacks from Nsight Aftermath
void GpuCrashTracker::OnCrashDump(const void* pGpuCrashDump, const uint32_t gpuCrashDumpSize)
{
// Make sure only one thread at a time...
std::lock_guard<std::mutex> lock(m_mutex);
// Write to file for later in-depth analysis with Nsight Graphics.
WriteGpuCrashDumpToFile(pGpuCrashDump, gpuCrashDumpSize);
}
// Handler for shader debug information callbacks
void GpuCrashTracker::OnShaderDebugInfo(const void* pShaderDebugInfo, const uint32_t shaderDebugInfoSize)
{
// Make sure only one thread at a time...
std::lock_guard<std::mutex> lock(m_mutex);
// Get shader debug information identifier
GFSDK_Aftermath_ShaderDebugInfoIdentifier identifier = {};
AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GetShaderDebugInfoIdentifier(
GFSDK_Aftermath_Version_API,
pShaderDebugInfo,
shaderDebugInfoSize,
&identifier));
// Store information for decoding of GPU crash dumps with shader address mapping
// from within the application.
std::vector<uint8_t> data((uint8_t*)pShaderDebugInfo, (uint8_t*)pShaderDebugInfo + shaderDebugInfoSize);
m_shaderDebugInfo[identifier].swap(data);
// Write to file for later in-depth analysis of crash dumps with Nsight Graphics
WriteShaderDebugInformationToFile(identifier, pShaderDebugInfo, shaderDebugInfoSize);
}
// Handler for GPU crash dump description callbacks
void GpuCrashTracker::OnDescription(PFN_GFSDK_Aftermath_AddGpuCrashDumpDescription addDescription)
{
// Add some basic description about the crash. This is called after the GPU crash happens, but before
// the actual GPU crash dump callback. The provided data is included in the crash dump and can be
// retrieved using GFSDK_Aftermath_GpuCrashDump_GetDescription().
addDescription(GFSDK_Aftermath_GpuCrashDumpDescriptionKey_ApplicationName, "Granite");
addDescription(GFSDK_Aftermath_GpuCrashDumpDescriptionKey_ApplicationVersion, "v1.0");
}
// Handler for app-managed marker resolve callback
void GpuCrashTracker::OnResolveMarker(const void* pMarkerData, const uint32_t, void** ppResolvedMarkerData, uint32_t* pResolvedMarkerDataSize)
{
// Important: the pointer passed back via ppResolvedMarkerData must remain valid after this function returns
// using references for all of the m_markerMap accesses ensures that the pointers refer to the persistent data
for (auto& map : m_markerMap)
{
const auto& foundMarker = map.find((uint64_t)pMarkerData);
if (foundMarker != map.end())
{
const std::string& foundMarkerData = foundMarker->second;
// std::string::data() will return a valid pointer until the string is next modified
// we don't modify the string after calling data() here, so the pointer should remain valid
*ppResolvedMarkerData = (void*)foundMarkerData.data();
*pResolvedMarkerDataSize = (uint32_t)foundMarkerData.length();
return;
}
}
}
// Helper for writing a GPU crash dump to a file
void GpuCrashTracker::WriteGpuCrashDumpToFile(const void* pGpuCrashDump, const uint32_t gpuCrashDumpSize)
{
// Create a GPU crash dump decoder object for the GPU crash dump.
GFSDK_Aftermath_GpuCrashDump_Decoder decoder = {};
AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_CreateDecoder(
GFSDK_Aftermath_Version_API,
pGpuCrashDump,
gpuCrashDumpSize,
&decoder));
// Use the decoder object to read basic information, like application
// name, PID, etc. from the GPU crash dump.
GFSDK_Aftermath_GpuCrashDump_BaseInfo baseInfo = {};
AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_GetBaseInfo(decoder, &baseInfo));
// Use the decoder object to query the application name that was set
// in the GPU crash dump description.
uint32_t applicationNameLength = 0;
AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_GetDescriptionSize(
decoder,
GFSDK_Aftermath_GpuCrashDumpDescriptionKey_ApplicationName,
&applicationNameLength));
std::vector<char> applicationName(applicationNameLength, '\0');
AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_GetDescription(
decoder,
GFSDK_Aftermath_GpuCrashDumpDescriptionKey_ApplicationName,
uint32_t(applicationName.size()),
applicationName.data()));
// Create a unique file name for writing the crash dump data to a file.
// Note: due to an Nsight Aftermath bug (will be fixed in an upcoming
// driver release) we may see redundant crash dumps. As a workaround,
// attach a unique count to each generated file name.
static int count = 0;
const std::string baseFileName =
std::string(applicationName.data())
+ "-"
+ std::to_string(baseInfo.pid)
+ "-"
+ std::to_string(++count);
// Write the crash dump data to a file using the .nv-gpudmp extension
// registered with Nsight Graphics.
const std::string crashDumpFileName = baseFileName + ".nv-gpudmp";
std::ofstream dumpFile(crashDumpFileName, std::ios::out | std::ios::binary);
if (dumpFile)
{
dumpFile.write((const char*)pGpuCrashDump, gpuCrashDumpSize);
dumpFile.close();
LOGI("Wrote crash dump file to: %s.\n", crashDumpFileName.c_str());
}
// Decode the crash dump to a JSON string.
// Step 1: Generate the JSON and get the size.
uint32_t jsonSize = 0;
AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_GenerateJSON(
decoder,
GFSDK_Aftermath_GpuCrashDumpDecoderFlags_ALL_INFO,
GFSDK_Aftermath_GpuCrashDumpFormatterFlags_NONE,
ShaderDebugInfoLookupCallback,
ShaderLookupCallback,
ShaderSourceDebugInfoLookupCallback,
this,
&jsonSize));
// Step 2: Allocate a buffer and fetch the generated JSON.
std::vector<char> json(jsonSize);
AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_GetJSON(
decoder,
uint32_t(json.size()),
json.data()));
// Write the crash dump data as JSON to a file.
const std::string jsonFileName = crashDumpFileName + ".json";
std::ofstream jsonFile(jsonFileName, std::ios::out | std::ios::binary);
if (jsonFile)
{
// Write the JSON to the file (excluding string termination)
jsonFile.write(json.data(), json.size() - 1);
jsonFile.close();
LOGI("Wrote crash dump JSON file to: %s.\n", jsonFileName.c_str());
}
// Dump active SPIR-V files.
uint32_t shader_count = 0;
AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_GetActiveShadersInfoCount(decoder, &shader_count));
std::vector<GFSDK_Aftermath_GpuCrashDump_ShaderInfo> shader_infos(shader_count);
AFTERMATH_CHECK_ERROR(
GFSDK_Aftermath_GpuCrashDump_GetActiveShadersInfo(decoder, shader_count, shader_infos.data()));
for (auto &shader : shader_infos)
{
GFSDK_Aftermath_ShaderBinaryHash hash;
AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GetShaderHashForShaderInfo(decoder, &shader, &hash));
std::lock_guard<std::mutex> holder{ shader_lock };
auto itr = shader_db.find(hash);
if (itr != shader_db.end())
{
const std::string spirvFilePath = "shader_" + std::to_string(hash) + ".spv";
std::ofstream spirvFile(spirvFilePath, std::ios::out | std::ios::binary);
if (spirvFile)
{
spirvFile.write((const char *)itr->second.data(), itr->second.size() * sizeof(uint32_t));
LOGI("Wrote SPIR-V shader file to: %s.\n", spirvFilePath.c_str());
}
}
}
// Destroy the GPU crash dump decoder object.
AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_DestroyDecoder(decoder));
#ifdef _WIN32
char print_buffer[1024];
char current_dir[1024];
GetCurrentDirectoryA(sizeof(current_dir), current_dir);
snprintf(print_buffer, sizeof(print_buffer),
"GPU hang detected with NV Aftermath. Dump files have been written to %s\\%s. Terminating process ...",
current_dir, crashDumpFileName.c_str());
MessageBoxA(nullptr, print_buffer, "VK_ERROR_DEVICE_LOST", MB_OK);
TerminateProcess(GetCurrentProcess(), 1);
#else
std::terminate();
#endif
}
void GpuCrashTracker::RegisterShader(const void *code, size_t size)
{
std::vector<uint32_t> data(static_cast<const uint32_t *>(code),
static_cast<const uint32_t *>(code) + size / sizeof(uint32_t));
// Create shader hash for the shader
const GFSDK_Aftermath_SpirvCode shader{{data.data()}, uint32_t(size)};
GFSDK_Aftermath_ShaderBinaryHash shaderHash;
AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GetShaderHashSpirv(
GFSDK_Aftermath_Version_API,
&shader,
&shaderHash));
// Store the data for shader mapping when decoding GPU crash dumps.
// cf. FindShaderBinary()
std::lock_guard<std::mutex> holder{shader_lock};
shader_db[shaderHash].swap(data);
}
// Helper for writing shader debug information to a file
void GpuCrashTracker::WriteShaderDebugInformationToFile(
GFSDK_Aftermath_ShaderDebugInfoIdentifier identifier,
const void* pShaderDebugInfo,
const uint32_t shaderDebugInfoSize)
{
// Create a unique file name.
const std::string filePath = "shader-" + std::to_string(identifier) + ".nvdbg";
std::ofstream f(filePath, std::ios::out | std::ios::binary);
if (f)
{
f.write((const char*)pShaderDebugInfo, shaderDebugInfoSize);
LOGI("Wrote shader file to: %s.\n", filePath.c_str());
}
}
// Handler for shader debug information lookup callbacks.
// This is used by the JSON decoder for mapping shader instruction
// addresses to SPIR-V IL lines or GLSL source lines.
void GpuCrashTracker::OnShaderDebugInfoLookup(
const GFSDK_Aftermath_ShaderDebugInfoIdentifier& identifier,
PFN_GFSDK_Aftermath_SetData setShaderDebugInfo) const
{
auto itr = m_shaderDebugInfo.find(identifier);
if (itr != m_shaderDebugInfo.end())
setShaderDebugInfo(itr->second.data(), uint32_t(itr->second.size()));
}
// Handler for shader lookup callbacks.
// This is used by the JSON decoder for mapping shader instruction
// addresses to SPIR-V IL lines or GLSL source lines.
// NOTE: If the application loads stripped shader binaries (ie; --strip-all in spirv-remap),
// Aftermath will require access to both the stripped and the not stripped
// shader binaries.
void GpuCrashTracker::OnShaderLookup(
const GFSDK_Aftermath_ShaderBinaryHash& shaderHash,
PFN_GFSDK_Aftermath_SetData setShaderBinary) const
{
std::lock_guard<std::mutex> holder{shader_lock};
auto itr = shader_db.find(shaderHash);
if (itr != shader_db.end())
setShaderBinary(itr->second.data(), itr->second.size() * sizeof(uint32_t));
}
// Handler for shader source debug info lookup callbacks.
// This is used by the JSON decoder for mapping shader instruction addresses to
// GLSL source lines, if the shaders used by the application were compiled with
// separate debug info data files.
void GpuCrashTracker::OnShaderSourceDebugInfoLookup(
const GFSDK_Aftermath_ShaderDebugName&,
PFN_GFSDK_Aftermath_SetData) const
{
// Granite doesn't do this.
}
// Static callback wrapper for OnCrashDump
void GpuCrashTracker::GpuCrashDumpCallback(
const void* pGpuCrashDump,
const uint32_t gpuCrashDumpSize,
void* pUserData)
{
auto* pGpuCrashTracker = static_cast<GpuCrashTracker*>(pUserData);
pGpuCrashTracker->OnCrashDump(pGpuCrashDump, gpuCrashDumpSize);
}
// Static callback wrapper for OnShaderDebugInfo
void GpuCrashTracker::ShaderDebugInfoCallback(
const void* pShaderDebugInfo,
const uint32_t shaderDebugInfoSize,
void* pUserData)
{
auto* pGpuCrashTracker = static_cast<GpuCrashTracker*>(pUserData);
pGpuCrashTracker->OnShaderDebugInfo(pShaderDebugInfo, shaderDebugInfoSize);
}
// Static callback wrapper for OnDescription
void GpuCrashTracker::CrashDumpDescriptionCallback(
PFN_GFSDK_Aftermath_AddGpuCrashDumpDescription addDescription,
void* pUserData)
{
auto* pGpuCrashTracker = static_cast<GpuCrashTracker*>(pUserData);
pGpuCrashTracker->OnDescription(addDescription);
}
// Static callback wrapper for OnResolveMarker
void GpuCrashTracker::ResolveMarkerCallback(
const void* pMarkerData,
const uint32_t markerDataSize,
void* pUserData,
void** ppResolvedMarkerData,
uint32_t* pResolvedMarkerDataSize)
{
auto* pGpuCrashTracker = static_cast<GpuCrashTracker*>(pUserData);
pGpuCrashTracker->OnResolveMarker(pMarkerData, markerDataSize, ppResolvedMarkerData, pResolvedMarkerDataSize);
}
// Static callback wrapper for OnShaderDebugInfoLookup
void GpuCrashTracker::ShaderDebugInfoLookupCallback(
const GFSDK_Aftermath_ShaderDebugInfoIdentifier* pIdentifier,
PFN_GFSDK_Aftermath_SetData setShaderDebugInfo,
void* pUserData)
{
auto* pGpuCrashTracker = static_cast<GpuCrashTracker*>(pUserData);
pGpuCrashTracker->OnShaderDebugInfoLookup(*pIdentifier, setShaderDebugInfo);
}
// Static callback wrapper for OnShaderLookup
void GpuCrashTracker::ShaderLookupCallback(
const GFSDK_Aftermath_ShaderBinaryHash* pShaderHash,
PFN_GFSDK_Aftermath_SetData setShaderBinary,
void* pUserData)
{
auto* pGpuCrashTracker = static_cast<GpuCrashTracker*>(pUserData);
pGpuCrashTracker->OnShaderLookup(*pShaderHash, setShaderBinary);
}
// Static callback wrapper for OnShaderSourceDebugInfoLookup
void GpuCrashTracker::ShaderSourceDebugInfoLookupCallback(
const GFSDK_Aftermath_ShaderDebugName* pShaderDebugName,
PFN_GFSDK_Aftermath_SetData setShaderBinary,
void* pUserData)
{
auto* pGpuCrashTracker = static_cast<GpuCrashTracker*>(pUserData);
pGpuCrashTracker->OnShaderSourceDebugInfoLookup(*pShaderDebugName, setShaderBinary);
}
@@ -0,0 +1,170 @@
//*********************************************************
//
// Copyright (c) 2019-2022, NVIDIA CORPORATION. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//
//*********************************************************
#pragma once
#include <vulkan/vulkan.h>
#include <map>
#include <mutex>
#include <vector>
#include <array>
#include "NsightAftermathHelpers.h"
//*********************************************************
// Implements GPU crash dump tracking using the Nsight
// Aftermath API.
//
class GpuCrashTracker
{
public:
// keep four frames worth of marker history
const static unsigned int c_markerFrameHistory = 4;
typedef std::array<std::map<uint64_t, std::string>, c_markerFrameHistory> MarkerMap;
GpuCrashTracker(const MarkerMap& markerMap);
~GpuCrashTracker();
// Initialize the GPU crash dump tracker.
void Initialize();
void RegisterShader(const void *code, size_t size);
private:
//*********************************************************
// Callback handlers for GPU crash dumps and related data.
//
// Handler for GPU crash dump callbacks.
void OnCrashDump(const void* pGpuCrashDump, const uint32_t gpuCrashDumpSize);
// Handler for shader debug information callbacks.
void OnShaderDebugInfo(const void* pShaderDebugInfo, const uint32_t shaderDebugInfoSize);
// Handler for GPU crash dump description callbacks.
void OnDescription(PFN_GFSDK_Aftermath_AddGpuCrashDumpDescription addDescription);
// Handler for app-managed marker resolve callback
void OnResolveMarker(const void* pMarkerData, const uint32_t markerDataSize, void** ppResolvedMarkerData, uint32_t* pResolvedMarkerDataSize);
//*********************************************************
// Helpers for writing a GPU crash dump and debug information
// data to files.
//
// Helper for writing a GPU crash dump to a file.
void WriteGpuCrashDumpToFile(const void* pGpuCrashDump, const uint32_t gpuCrashDumpSize);
// Helper for writing shader debug information to a file
void WriteShaderDebugInformationToFile(
GFSDK_Aftermath_ShaderDebugInfoIdentifier identifier,
const void* pShaderDebugInfo,
const uint32_t shaderDebugInfoSize);
//*********************************************************
// Helpers for decoding GPU crash dump to JSON.
//
// Handler for shader debug info lookup callbacks.
void OnShaderDebugInfoLookup(
const GFSDK_Aftermath_ShaderDebugInfoIdentifier& identifier,
PFN_GFSDK_Aftermath_SetData setShaderDebugInfo) const;
// Handler for shader lookup callbacks.
void OnShaderLookup(
const GFSDK_Aftermath_ShaderBinaryHash& shaderHash,
PFN_GFSDK_Aftermath_SetData setShaderBinary) const;
// Handler for shader source debug info lookup callbacks.
void OnShaderSourceDebugInfoLookup(
const GFSDK_Aftermath_ShaderDebugName& shaderDebugName,
PFN_GFSDK_Aftermath_SetData setShaderBinary) const;
//*********************************************************
// Static callback wrappers.
//
// GPU crash dump callback.
static void GpuCrashDumpCallback(
const void* pGpuCrashDump,
const uint32_t gpuCrashDumpSize,
void* pUserData);
// Shader debug information callback.
static void ShaderDebugInfoCallback(
const void* pShaderDebugInfo,
const uint32_t shaderDebugInfoSize,
void* pUserData);
// GPU crash dump description callback.
static void CrashDumpDescriptionCallback(
PFN_GFSDK_Aftermath_AddGpuCrashDumpDescription addDescription,
void* pUserData);
// App-managed marker resolve callback
static void ResolveMarkerCallback(
const void* pMarkerData,
const uint32_t markerDataSize,
void* pUserData,
void** ppResolvedMarkerData,
uint32_t* pResolvedMarkerDataSize);
// Shader debug information lookup callback.
static void ShaderDebugInfoLookupCallback(
const GFSDK_Aftermath_ShaderDebugInfoIdentifier* pIdentifier,
PFN_GFSDK_Aftermath_SetData setShaderDebugInfo,
void* pUserData);
// Shader lookup callback.
static void ShaderLookupCallback(
const GFSDK_Aftermath_ShaderBinaryHash* pShaderHash,
PFN_GFSDK_Aftermath_SetData setShaderBinary,
void* pUserData);
// Shader source debug info lookup callback.
static void ShaderSourceDebugInfoLookupCallback(
const GFSDK_Aftermath_ShaderDebugName* pShaderDebugName,
PFN_GFSDK_Aftermath_SetData setShaderBinary,
void* pUserData);
//*********************************************************
// GPU crash tracker state.
//
// Is the GPU crash dump tracker initialized?
bool m_initialized;
// For thread-safe access of GPU crash tracker state.
mutable std::mutex m_mutex;
// List of Shader Debug Information by ShaderDebugInfoIdentifier.
std::map<GFSDK_Aftermath_ShaderDebugInfoIdentifier, std::vector<uint8_t>> m_shaderDebugInfo;
// App-managed marker tracking
const MarkerMap& m_markerMap;
mutable std::mutex shader_lock;
std::map<GFSDK_Aftermath_ShaderBinaryHash, std::vector<uint32_t>> shader_db;
};
@@ -0,0 +1,131 @@
//*********************************************************
//
// Copyright (c) 2019-2022, NVIDIA CORPORATION. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//
//*********************************************************
#pragma once
#include <iomanip>
#include <string>
#include <sstream>
#include <string.h>
#include "GFSDK_Aftermath.h"
#include "GFSDK_Aftermath_GpuCrashDump.h"
#include "GFSDK_Aftermath_GpuCrashDumpDecoding.h"
//*********************************************************
// Some std::to_string overloads for some Nsight Aftermath
// API types.
//
namespace std
{
template<typename T>
inline std::string to_hex_string(T n)
{
std::stringstream stream;
stream << std::setfill('0') << std::setw(2 * sizeof(T)) << std::hex << n;
return stream.str();
}
inline std::string to_string(GFSDK_Aftermath_Result result)
{
return std::string("0x") + to_hex_string(static_cast<uint32_t>(result));
}
inline std::string to_string(const GFSDK_Aftermath_ShaderDebugInfoIdentifier& identifier)
{
return to_hex_string(identifier.id[0]) + "-" + to_hex_string(identifier.id[1]);
}
inline std::string to_string(const GFSDK_Aftermath_ShaderBinaryHash& hash)
{
return to_hex_string(hash.hash);
}
} // namespace std
//*********************************************************
// Helper for comparing shader hashes and debug info identifier.
//
// Helper for comparing GFSDK_Aftermath_ShaderDebugInfoIdentifier.
inline bool operator<(const GFSDK_Aftermath_ShaderDebugInfoIdentifier& lhs, const GFSDK_Aftermath_ShaderDebugInfoIdentifier& rhs)
{
if (lhs.id[0] == rhs.id[0])
{
return lhs.id[1] < rhs.id[1];
}
return lhs.id[0] < rhs.id[0];
}
// Helper for comparing GFSDK_Aftermath_ShaderBinaryHash.
inline bool operator<(const GFSDK_Aftermath_ShaderBinaryHash& lhs, const GFSDK_Aftermath_ShaderBinaryHash& rhs)
{
return lhs.hash < rhs.hash;
}
// Helper for comparing GFSDK_Aftermath_ShaderDebugName.
inline bool operator<(const GFSDK_Aftermath_ShaderDebugName& lhs, const GFSDK_Aftermath_ShaderDebugName& rhs)
{
return strncmp(lhs.name, rhs.name, sizeof(lhs.name)) < 0;
}
//*********************************************************
// Helper for checking Nsight Aftermath failures.
//
inline std::string AftermathErrorMessage(GFSDK_Aftermath_Result result)
{
switch (result)
{
case GFSDK_Aftermath_Result_FAIL_DriverVersionNotSupported:
return "Unsupported driver version - requires an NVIDIA R495 display driver or newer.";
default:
return "Aftermath Error 0x" + std::to_hex_string(result);
}
}
// Helper macro for checking Nsight Aftermath results and throwing exception
// in case of a failure.
#ifdef _WIN32
#define AFTERMATH_CHECK_ERROR(FC) \
[&]() { \
GFSDK_Aftermath_Result _result = FC; \
if (!GFSDK_Aftermath_SUCCEED(_result)) \
{ \
MessageBoxA(0, AftermathErrorMessage(_result).c_str(), "Aftermath Error", MB_OK); \
exit(1); \
} \
}()
#else
#define AFTERMATH_CHECK_ERROR(FC) \
[&]() { \
GFSDK_Aftermath_Result _result = FC; \
if (!GFSDK_Aftermath_SUCCEED(_result)) \
{ \
printf("%s\n", AftermathErrorMessage(_result).c_str()); \
fflush(stdout); \
exit(1); \
} \
}()
#endif
@@ -0,0 +1,75 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "post_mortem.hpp"
#ifdef HAVE_AFTERMATH_SDK
#include "NsightAftermathGpuCrashTracker.h"
#endif
namespace Vulkan
{
namespace PostMortem
{
class CrashTracker
{
public:
virtual ~CrashTracker() = default;
virtual void register_shader(const void *data, size_t size) = 0;
};
static std::unique_ptr<CrashTracker> global_tracker;
#ifdef HAVE_AFTERMATH_SDK
struct NsightCrashTracker : CrashTracker
{
GpuCrashTracker::MarkerMap marker;
GpuCrashTracker tracker;
NsightCrashTracker() : tracker(marker) {}
void register_shader(const void *data, size_t size) override { tracker.RegisterShader(data, size); }
};
#endif
void init_nv_aftermath()
{
if (global_tracker)
return;
#ifdef HAVE_AFTERMATH_SDK
auto tracker = std::make_unique<NsightCrashTracker>();
tracker->tracker.Initialize();
global_tracker = std::move(tracker);
#endif
}
void register_shader(const void *data, size_t size)
{
if (global_tracker)
global_tracker->register_shader(data, size);
}
void deinit()
{
global_tracker.reset();
}
}
}
@@ -0,0 +1,35 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <memory>
namespace Vulkan
{
namespace PostMortem
{
void init_nv_aftermath();
void deinit();
void register_shader(const void *code, size_t size);
}
}
@@ -0,0 +1,550 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "query_pool.hpp"
#include "device.hpp"
#include <utility>
namespace Vulkan
{
static const char *storage_to_str(VkPerformanceCounterStorageKHR storage)
{
switch (storage)
{
case VK_PERFORMANCE_COUNTER_STORAGE_FLOAT32_KHR:
return "float32";
case VK_PERFORMANCE_COUNTER_STORAGE_FLOAT64_KHR:
return "float64";
case VK_PERFORMANCE_COUNTER_STORAGE_INT32_KHR:
return "int32";
case VK_PERFORMANCE_COUNTER_STORAGE_INT64_KHR:
return "int64";
case VK_PERFORMANCE_COUNTER_STORAGE_UINT32_KHR:
return "uint32";
case VK_PERFORMANCE_COUNTER_STORAGE_UINT64_KHR:
return "uint64";
default:
return "???";
}
}
static const char *scope_to_str(VkPerformanceCounterScopeKHR scope)
{
switch (scope)
{
case VK_QUERY_SCOPE_COMMAND_BUFFER_KHR:
return "command buffer";
case VK_QUERY_SCOPE_RENDER_PASS_KHR:
return "render pass";
case VK_QUERY_SCOPE_COMMAND_KHR:
return "command";
default:
return "???";
}
}
static const char *unit_to_str(VkPerformanceCounterUnitKHR unit)
{
switch (unit)
{
case VK_PERFORMANCE_COUNTER_UNIT_AMPS_KHR:
return "A";
case VK_PERFORMANCE_COUNTER_UNIT_BYTES_KHR:
return "bytes";
case VK_PERFORMANCE_COUNTER_UNIT_BYTES_PER_SECOND_KHR:
return "bytes / second";
case VK_PERFORMANCE_COUNTER_UNIT_CYCLES_KHR:
return "cycles";
case VK_PERFORMANCE_COUNTER_UNIT_GENERIC_KHR:
return "units";
case VK_PERFORMANCE_COUNTER_UNIT_HERTZ_KHR:
return "Hz";
case VK_PERFORMANCE_COUNTER_UNIT_KELVIN_KHR:
return "K";
case VK_PERFORMANCE_COUNTER_UNIT_NANOSECONDS_KHR:
return "ns";
case VK_PERFORMANCE_COUNTER_UNIT_PERCENTAGE_KHR:
return "%";
case VK_PERFORMANCE_COUNTER_UNIT_VOLTS_KHR:
return "V";
case VK_PERFORMANCE_COUNTER_UNIT_WATTS_KHR:
return "W";
default:
return "???";
}
}
void PerformanceQueryPool::log_available_counters(const VkPerformanceCounterKHR *counters,
const VkPerformanceCounterDescriptionKHR *descs,
uint32_t count)
{
for (uint32_t i = 0; i < count; i++)
{
LOGI(" %s: %s\n", descs[i].name, descs[i].description);
LOGI(" Storage: %s\n", storage_to_str(counters[i].storage));
LOGI(" Scope: %s\n", scope_to_str(counters[i].scope));
LOGI(" Unit: %s\n", unit_to_str(counters[i].unit));
}
}
void PerformanceQueryPool::init_device(Device *device_, uint32_t queue_family_index_)
{
device = device_;
queue_family_index = queue_family_index_;
if (!device->get_device_features().performance_query_features.performanceCounterQueryPools)
return;
uint32_t num_counters = 0;
if (vkEnumeratePhysicalDeviceQueueFamilyPerformanceQueryCountersKHR(
device->get_physical_device(),
queue_family_index,
&num_counters,
nullptr, nullptr) != VK_SUCCESS)
{
LOGE("Failed to enumerate performance counters.\n");
return;
}
counters.resize(num_counters, { VK_STRUCTURE_TYPE_PERFORMANCE_COUNTER_KHR });
counter_descriptions.resize(num_counters, { VK_STRUCTURE_TYPE_PERFORMANCE_COUNTER_DESCRIPTION_KHR });
if (vkEnumeratePhysicalDeviceQueueFamilyPerformanceQueryCountersKHR(
device->get_physical_device(),
queue_family_index,
&num_counters,
counters.data(), counter_descriptions.data()) != VK_SUCCESS)
{
LOGE("Failed to enumerate performance counters.\n");
return;
}
}
PerformanceQueryPool::~PerformanceQueryPool()
{
if (pool)
device->get_device_table().vkDestroyQueryPool(device->get_device(), pool, nullptr);
}
void PerformanceQueryPool::begin_command_buffer(VkCommandBuffer cmd)
{
if (!pool)
return;
auto &table = device->get_device_table();
if (device->get_device_features().vk12_features.hostQueryReset)
table.vkResetQueryPool(device->get_device(), pool, 0, 1);
else
table.vkCmdResetQueryPool(cmd, pool, 0, 1);
table.vkCmdBeginQuery(cmd, pool, 0, 0);
VkMemoryBarrier barrier = { VK_STRUCTURE_TYPE_MEMORY_BARRIER };
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT;
table.vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
0, 1, &barrier, 0, nullptr, 0, nullptr);
}
void PerformanceQueryPool::end_command_buffer(VkCommandBuffer cmd)
{
if (!pool)
return;
auto &table = device->get_device_table();
VkMemoryBarrier barrier = { VK_STRUCTURE_TYPE_MEMORY_BARRIER };
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT;
table.vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
0, 1, &barrier, 0, nullptr, 0, nullptr);
table.vkCmdEndQuery(cmd, pool, 0);
}
void PerformanceQueryPool::report()
{
if (pool == VK_NULL_HANDLE)
{
LOGE("No query pool is set up.\n");
return;
}
auto &table = device->get_device_table();
if (table.vkGetQueryPoolResults(device->get_device(), pool,
0, 1,
results.size() * sizeof(VkPerformanceCounterResultKHR),
results.data(),
sizeof(VkPerformanceCounterResultKHR),
VK_QUERY_RESULT_WAIT_BIT) != VK_SUCCESS)
{
LOGE("Getting performance counters did not succeed.\n");
}
size_t num_counters = results.size();
LOGI("\n=== Profiling result ===\n");
for (size_t i = 0; i < num_counters; i++)
{
auto &counter = counters[active_indices[i]];
auto &desc = counter_descriptions[active_indices[i]];
switch (counter.storage)
{
case VK_PERFORMANCE_COUNTER_STORAGE_INT32_KHR:
LOGI(" %s (%s): %d %s\n", desc.name, desc.description, results[i].int32, unit_to_str(counter.unit));
break;
case VK_PERFORMANCE_COUNTER_STORAGE_INT64_KHR:
LOGI(" %s (%s): %lld %s\n", desc.name, desc.description, static_cast<long long>(results[i].int64), unit_to_str(counter.unit));
break;
case VK_PERFORMANCE_COUNTER_STORAGE_UINT32_KHR:
LOGI(" %s (%s): %u %s\n", desc.name, desc.description, results[i].uint32, unit_to_str(counter.unit));
break;
case VK_PERFORMANCE_COUNTER_STORAGE_UINT64_KHR:
LOGI(" %s (%s): %llu %s\n", desc.name, desc.description, static_cast<long long>(results[i].uint64), unit_to_str(counter.unit));
break;
case VK_PERFORMANCE_COUNTER_STORAGE_FLOAT32_KHR:
LOGI(" %s (%s): %g %s\n", desc.name, desc.description, results[i].float32, unit_to_str(counter.unit));
break;
case VK_PERFORMANCE_COUNTER_STORAGE_FLOAT64_KHR:
LOGI(" %s (%s): %g %s\n", desc.name, desc.description, results[i].float64, unit_to_str(counter.unit));
break;
default:
break;
}
}
LOGI("================================\n\n");
}
uint32_t PerformanceQueryPool::get_num_counters() const
{
return uint32_t(counters.size());
}
const VkPerformanceCounterKHR *PerformanceQueryPool::get_available_counters() const
{
return counters.data();
}
const VkPerformanceCounterDescriptionKHR *PerformanceQueryPool::get_available_counter_descs() const
{
return counter_descriptions.data();
}
bool PerformanceQueryPool::init_counters(const std::vector<std::string> &counter_names)
{
if (!device->get_device_features().performance_query_features.performanceCounterQueryPools)
{
LOGE("Device does not support VK_KHR_performance_query.\n");
return false;
}
if (!device->get_device_features().vk12_features.hostQueryReset)
{
LOGE("Device does not support host query reset.\n");
return false;
}
auto &table = device->get_device_table();
if (pool)
table.vkDestroyQueryPool(device->get_device(), pool, nullptr);
pool = VK_NULL_HANDLE;
VkQueryPoolPerformanceCreateInfoKHR performance_info = { VK_STRUCTURE_TYPE_QUERY_POOL_PERFORMANCE_CREATE_INFO_KHR };
VkQueryPoolCreateInfo info = { VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO };
info.pNext = &performance_info;
info.queryType = VK_QUERY_TYPE_PERFORMANCE_QUERY_KHR;
info.queryCount = 1;
active_indices.clear();
for (auto &name : counter_names)
{
auto itr = find_if(begin(counter_descriptions), end(counter_descriptions), [&](const VkPerformanceCounterDescriptionKHR &desc) {
return name == desc.name;
});
if (itr != end(counter_descriptions))
{
LOGI("Found counter %s: %s\n", itr->name, itr->description);
active_indices.push_back(itr - begin(counter_descriptions));
}
}
if (active_indices.empty())
{
LOGW("No performance counters were enabled.\n");
return false;
}
performance_info.queueFamilyIndex = queue_family_index;
performance_info.counterIndexCount = active_indices.size();
performance_info.pCounterIndices = active_indices.data();
results.resize(active_indices.size());
uint32_t num_passes = 0;
vkGetPhysicalDeviceQueueFamilyPerformanceQueryPassesKHR(device->get_physical_device(),
&performance_info, &num_passes);
if (num_passes != 1)
{
LOGE("Implementation requires %u passes to query performance counters. Cannot create query pool.\n",
num_passes);
return false;
}
if (table.vkCreateQueryPool(device->get_device(), &info, nullptr, &pool) != VK_SUCCESS)
{
LOGE("Failed to create performance query pool.\n");
return false;
}
return true;
}
QueryPool::QueryPool(Device *device_, VkQueryType type_)
: device(device_)
, table(device_->get_device_table())
, type(type_)
{
supports_type = false;
if (type == VK_QUERY_TYPE_TIMESTAMP)
{
// Ignore timestampValidBits and friends for now.
supports_type = device->get_gpu_properties().limits.timestampComputeAndGraphics;
}
else if (type == VK_QUERY_TYPE_ACCELERATION_STRUCTURE_COMPACTED_SIZE_KHR)
{
supports_type = device->get_device_features().rtas_features.accelerationStructure == VK_TRUE;
}
if (supports_type)
add_pool();
}
QueryPool::~QueryPool()
{
for (auto &pool : pools)
table.vkDestroyQueryPool(device->get_device(), pool.pool, nullptr);
}
void QueryPool::begin()
{
for (unsigned i = 0; i <= pool_index; i++)
{
if (i >= pools.size())
continue;
auto &pool = pools[i];
if (pool.index == 0)
continue;
table.vkGetQueryPoolResults(device->get_device(), pool.pool,
0, pool.index,
pool.index * sizeof(uint64_t),
pool.query_results.data(),
sizeof(uint64_t),
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
for (unsigned j = 0; j < pool.index; j++)
pool.cookies[j]->signal_value(pool.query_results[j]);
if (device->get_device_features().vk12_features.hostQueryReset)
table.vkResetQueryPool(device->get_device(), pool.pool, 0, pool.index);
}
pool_index = 0;
for (auto &pool : pools)
pool.index = 0;
}
void QueryPool::add_pool()
{
VkQueryPoolCreateInfo pool_info = { VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO };
pool_info.queryType = type;
pool_info.queryCount = 64;
Pool pool;
table.vkCreateQueryPool(device->get_device(), &pool_info, nullptr, &pool.pool);
pool.size = pool_info.queryCount;
pool.index = 0;
pool.query_results.resize(pool.size);
pool.cookies.resize(pool.size);
if (device->get_device_features().vk12_features.hostQueryReset)
table.vkResetQueryPool(device->get_device(), pool.pool, 0, pool.size);
pools.push_back(std::move(pool));
}
QueryPoolHandle QueryPool::write_timestamp(VkCommandBuffer cmd, VkPipelineStageFlags2 stage)
{
if (!supports_type)
{
LOGI("Timestamps are not supported on this implementation.\n");
return {};
}
VK_ASSERT((stage & (stage - 1)) == 0);
auto handle = allocate_query(cmd);
table.vkCmdWriteTimestamp2(cmd, stage, handle->get_query_pool(), handle->get_query_pool_index());
return handle;
}
QueryPoolHandle QueryPool::allocate_query(VkCommandBuffer cmd)
{
if (!supports_type)
{
LOGI("Query type %u not supported on this implementation.\n", type);
return {};
}
if (pools[pool_index].index >= pools[pool_index].size)
pool_index++;
if (pool_index >= pools.size())
add_pool();
auto &pool = pools[pool_index];
auto cookie = QueryPoolHandle(device->handle_pool.query.allocate(device, type == VK_QUERY_TYPE_TIMESTAMP, type, pool.pool, pool.index));
pool.cookies[pool.index] = cookie;
if (!device->get_device_features().vk12_features.hostQueryReset)
table.vkCmdResetQueryPool(cmd, pool.pool, pool.index, 1);
pool.index++;
return cookie;
}
void QueryPoolResultDeleter::operator()(QueryPoolResult *query)
{
query->device->handle_pool.query.free(query);
}
void TimestampInterval::mark_end_of_frame_context()
{
if (total_time > 0.0)
total_frame_iterations++;
}
uint64_t TimestampInterval::get_total_accumulations() const
{
return total_accumulations;
}
uint64_t TimestampInterval::get_total_frame_iterations() const
{
return total_frame_iterations;
}
double TimestampInterval::get_total_time() const
{
return total_time;
}
void TimestampInterval::accumulate_time(double t)
{
total_time += t;
total_accumulations++;
}
double TimestampInterval::get_time_per_iteration() const
{
if (total_frame_iterations)
return total_time / double(total_frame_iterations);
else
return 0.0;
}
double TimestampInterval::get_time_per_accumulation() const
{
if (total_accumulations)
return total_time / double(total_accumulations);
else
return 0.0;
}
const std::string &TimestampInterval::get_tag() const
{
return tag;
}
void TimestampInterval::reset()
{
total_time = 0.0;
total_accumulations = 0;
total_frame_iterations = 0;
}
TimestampInterval::TimestampInterval(std::string tag_)
: tag(std::move(tag_))
{
}
TimestampInterval *TimestampIntervalManager::get_timestamp_tag(const char *tag)
{
Util::Hasher h;
h.string(tag);
return timestamps.emplace_yield(h.get(), tag);
}
void TimestampIntervalManager::mark_end_of_frame_context()
{
for (auto &timestamp : timestamps)
timestamp.mark_end_of_frame_context();
}
void TimestampIntervalManager::reset()
{
for (auto &timestamp : timestamps)
timestamp.reset();
}
void TimestampIntervalManager::log_simple(const TimestampIntervalReportCallback &func) const
{
for (auto &timestamp : timestamps)
{
if (timestamp.get_total_frame_iterations())
{
TimestampIntervalReport report = {};
report.time_per_accumulation = timestamp.get_time_per_accumulation();
report.time_per_frame_context = timestamp.get_time_per_iteration();
report.accumulations_per_frame_context =
double(timestamp.get_total_accumulations()) / double(timestamp.get_total_frame_iterations());
if (func)
{
func(timestamp.get_tag(), report);
}
else
{
LOGI("Timestamp tag report: %s\n", timestamp.get_tag().c_str());
LOGI(" %.3f ms / iteration\n", 1000.0 * report.time_per_accumulation);
LOGI(" %.3f ms / frame context\n", 1000.0 * report.time_per_frame_context);
LOGI(" %.3f iterations / frame context\n", report.accumulations_per_frame_context);
}
}
}
}
}
@@ -0,0 +1,208 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_headers.hpp"
#include "vulkan_common.hpp"
#include "object_pool.hpp"
#include <functional>
namespace Vulkan
{
class Device;
class PerformanceQueryPool
{
public:
void init_device(Device *device, uint32_t queue_family_index);
~PerformanceQueryPool();
bool init_counters(const std::vector<std::string> &enable_counter_names);
void begin_command_buffer(VkCommandBuffer cmd);
void end_command_buffer(VkCommandBuffer cmd);
void report();
uint32_t get_num_counters() const;
const VkPerformanceCounterKHR *get_available_counters() const;
const VkPerformanceCounterDescriptionKHR *get_available_counter_descs() const;
static void log_available_counters(const VkPerformanceCounterKHR *counters,
const VkPerformanceCounterDescriptionKHR *descs,
uint32_t count);
private:
Device *device = nullptr;
uint32_t queue_family_index = 0;
VkQueryPool pool = VK_NULL_HANDLE;
std::vector<VkPerformanceCounterResultKHR> results;
std::vector<VkPerformanceCounterKHR> counters;
std::vector<VkPerformanceCounterDescriptionKHR> counter_descriptions;
std::vector<uint32_t> active_indices;
};
class QueryPoolResult;
struct QueryPoolResultDeleter
{
void operator()(QueryPoolResult *query);
};
class QueryPoolResult : public Util::IntrusivePtrEnabled<QueryPoolResult, QueryPoolResultDeleter, HandleCounter>
{
public:
friend struct QueryPoolResultDeleter;
inline void signal_value(uint64_t ticks)
{
value = ticks;
has_value = true;
}
// Compatibility alias.
inline uint64_t get_timestamp_ticks() const
{
VK_ASSERT(type == VK_QUERY_TYPE_TIMESTAMP);
return value;
}
inline uint64_t get_value() const
{
return value;
}
inline bool is_signalled() const
{
return has_value;
}
inline bool is_device_timebase() const
{
return device_timebase;
}
inline VkQueryPool get_query_pool() const
{
return pool;
}
inline uint32_t get_query_pool_index() const
{
return index;
}
private:
friend class Util::ObjectPool<QueryPoolResult>;
explicit QueryPoolResult(Device *device_, bool device_timebase_, VkQueryType type_,
VkQueryPool pool_, uint32_t index_)
: device(device_), device_timebase(device_timebase_), type(type_), pool(pool_), index(index_)
{}
Device *device;
uint64_t value = 0;
bool has_value = false;
bool device_timebase = false;
VkQueryType type;
VkQueryPool pool;
uint32_t index;
};
using QueryPoolHandle = Util::IntrusivePtr<QueryPoolResult>;
class QueryPool
{
public:
QueryPool(Device *device, VkQueryType type);
~QueryPool();
void begin();
QueryPoolHandle write_timestamp(VkCommandBuffer cmd, VkPipelineStageFlags2 stage);
QueryPoolHandle allocate_query(VkCommandBuffer cmd);
private:
Device *device;
const VolkDeviceTable &table;
VkQueryType type;
struct Pool
{
VkQueryPool pool = VK_NULL_HANDLE;
std::vector<uint64_t> query_results;
std::vector<QueryPoolHandle> cookies;
unsigned index = 0;
unsigned size = 0;
};
std::vector<Pool> pools;
unsigned pool_index = 0;
void add_pool();
bool supports_type = false;
};
class TimestampInterval : public Util::IntrusiveHashMapEnabled<TimestampInterval>
{
public:
explicit TimestampInterval(std::string tag);
void accumulate_time(double t);
double get_time_per_iteration() const;
double get_time_per_accumulation() const;
const std::string &get_tag() const;
void mark_end_of_frame_context();
double get_total_time() const;
uint64_t get_total_frame_iterations() const;
uint64_t get_total_accumulations() const;
void reset();
private:
std::string tag;
double total_time = 0.0;
uint64_t total_frame_iterations = 0;
uint64_t total_accumulations = 0;
};
struct TimestampIntervalReport
{
double time_per_accumulation;
double time_per_frame_context;
double accumulations_per_frame_context;
};
using TimestampIntervalReportCallback = std::function<void (const std::string &, const TimestampIntervalReport &)>;
class TimestampIntervalManager
{
public:
TimestampInterval *get_timestamp_tag(const char *tag);
void mark_end_of_frame_context();
void reset();
void log_simple(const TimestampIntervalReportCallback &func = {}) const;
private:
Util::IntrusiveHashMap<TimestampInterval> timestamps;
};
}
@@ -0,0 +1,50 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
namespace Vulkan
{
struct ImplementationQuirks
{
bool instance_deferred_lights = true;
bool merge_subpasses = true;
bool use_transient_color = true;
bool use_transient_depth_stencil = true;
bool queue_wait_on_submission = false;
bool use_async_compute_post = true;
bool render_graph_force_single_queue = false;
bool force_no_subgroups = false;
bool force_no_subgroup_shuffle = false;
bool force_no_subgroup_size_control = false;
static ImplementationQuirks &get();
};
struct ImplementationWorkarounds
{
bool emulate_event_as_pipeline_barrier = false;
bool broken_pipeline_cache_control = false;
bool force_host_cached = false;
bool broken_present_fence = false;
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,270 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "cookie.hpp"
#include "hash.hpp"
#include "image.hpp"
#include "intrusive.hpp"
#include "limits.hpp"
#include "object_pool.hpp"
#include "temporary_hashmap.hpp"
#include "vulkan_headers.hpp"
namespace Vulkan
{
enum RenderPassOp
{
RENDER_PASS_OP_CLEAR_DEPTH_STENCIL_BIT = 1 << 0,
RENDER_PASS_OP_LOAD_DEPTH_STENCIL_BIT = 1 << 1,
RENDER_PASS_OP_STORE_DEPTH_STENCIL_BIT = 1 << 2,
RENDER_PASS_OP_DEPTH_STENCIL_READ_ONLY_BIT = 1 << 3,
RENDER_PASS_OP_ENABLE_TRANSIENT_STORE_BIT = 1 << 4,
RENDER_PASS_OP_ENABLE_TRANSIENT_LOAD_BIT = 1 << 5,
RENDER_PASS_OP_PRESERVE_DEPTH_STENCIL_BIT = 1 << 6
};
using RenderPassOpFlags = uint32_t;
class ImageView;
struct RenderPassInfo
{
const ImageView *color_attachments[VULKAN_NUM_ATTACHMENTS];
const ImageView *depth_stencil = nullptr;
unsigned num_color_attachments = 0;
RenderPassOpFlags op_flags = 0;
uint32_t clear_attachments = 0;
uint32_t load_attachments = 0;
uint32_t store_attachments = 0;
uint32_t base_layer = 0;
uint32_t num_layers = 1;
// Render area will be clipped to the actual framebuffer.
VkRect2D render_area = { { 0, 0 }, { UINT32_MAX, UINT32_MAX } };
VkClearColorValue clear_color[VULKAN_NUM_ATTACHMENTS] = {};
VkClearDepthStencilValue clear_depth_stencil = {};
enum class DepthStencil
{
None,
ReadOnly,
ReadWrite
};
struct Subpass
{
uint32_t color_attachments[VULKAN_NUM_ATTACHMENTS];
uint32_t input_attachments[VULKAN_NUM_ATTACHMENTS];
uint32_t resolve_attachments[VULKAN_NUM_ATTACHMENTS];
unsigned num_color_attachments = 0;
unsigned num_input_attachments = 0;
unsigned num_resolve_attachments = 0;
DepthStencil depth_stencil_mode = DepthStencil::ReadWrite;
};
// If 0/nullptr, assume a default subpass.
const Subpass *subpasses = nullptr;
unsigned num_subpasses = 0;
};
class RenderPass : public HashedObject<RenderPass>, public NoCopyNoMove
{
public:
struct SubpassInfo
{
VkAttachmentReference2 color_attachments[VULKAN_NUM_ATTACHMENTS];
unsigned num_color_attachments;
VkAttachmentReference2 input_attachments[VULKAN_NUM_ATTACHMENTS];
unsigned num_input_attachments;
VkAttachmentReference2 depth_stencil_attachment;
unsigned samples;
};
RenderPass(Util::Hash hash, Device *device, const RenderPassInfo &info);
RenderPass(Util::Hash hash, Device *device, const VkRenderPassCreateInfo2 &create_info);
~RenderPass();
unsigned get_num_subpasses() const
{
return unsigned(subpasses_info.size());
}
VkRenderPass get_render_pass() const
{
return render_pass;
}
uint32_t get_sample_count(unsigned subpass) const
{
VK_ASSERT(subpass < subpasses_info.size());
return subpasses_info[subpass].samples;
}
unsigned get_num_color_attachments(unsigned subpass) const
{
VK_ASSERT(subpass < subpasses_info.size());
return subpasses_info[subpass].num_color_attachments;
}
unsigned get_num_input_attachments(unsigned subpass) const
{
VK_ASSERT(subpass < subpasses_info.size());
return subpasses_info[subpass].num_input_attachments;
}
const VkAttachmentReference2 &get_color_attachment(unsigned subpass, unsigned index) const
{
VK_ASSERT(subpass < subpasses_info.size());
VK_ASSERT(index < subpasses_info[subpass].num_color_attachments);
return subpasses_info[subpass].color_attachments[index];
}
const VkAttachmentReference2 &get_input_attachment(unsigned subpass, unsigned index) const
{
VK_ASSERT(subpass < subpasses_info.size());
VK_ASSERT(index < subpasses_info[subpass].num_input_attachments);
return subpasses_info[subpass].input_attachments[index];
}
bool has_depth(unsigned subpass) const
{
VK_ASSERT(subpass < subpasses_info.size());
return subpasses_info[subpass].depth_stencil_attachment.attachment != VK_ATTACHMENT_UNUSED &&
format_has_depth_aspect(depth_stencil);
}
bool has_stencil(unsigned subpass) const
{
VK_ASSERT(subpass < subpasses_info.size());
return subpasses_info[subpass].depth_stencil_attachment.attachment != VK_ATTACHMENT_UNUSED &&
format_has_stencil_aspect(depth_stencil);
}
private:
Device *device;
VkRenderPass render_pass = VK_NULL_HANDLE;
VkFormat color_attachments[VULKAN_NUM_ATTACHMENTS] = {};
VkFormat depth_stencil = VK_FORMAT_UNDEFINED;
std::vector<SubpassInfo> subpasses_info;
void setup_subpasses(const VkRenderPassCreateInfo2 &create_info);
};
class Framebuffer : public Cookie, public NoCopyNoMove, public InternalSyncEnabled
{
public:
Framebuffer(Device *device, const RenderPass &rp, const RenderPassInfo &info);
~Framebuffer();
VkFramebuffer get_framebuffer() const
{
return framebuffer;
}
static unsigned setup_raw_views(VkImageView *views, const RenderPassInfo &info);
static void compute_dimensions(const RenderPassInfo &info, uint32_t &width, uint32_t &height);
static void compute_attachment_dimensions(const RenderPassInfo &info, unsigned index, uint32_t &width, uint32_t &height);
uint32_t get_width() const
{
return width;
}
uint32_t get_height() const
{
return height;
}
const RenderPass &get_compatible_render_pass() const
{
return render_pass;
}
private:
Device *device;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
const RenderPass &render_pass;
RenderPassInfo info;
uint32_t width = 0;
uint32_t height = 0;
};
static const unsigned VULKAN_FRAMEBUFFER_RING_SIZE = 16;
class FramebufferAllocator
{
public:
explicit FramebufferAllocator(Device *device);
Framebuffer &request_framebuffer(const RenderPassInfo &info);
void begin_frame();
void clear();
private:
struct FramebufferNode : Util::TemporaryHashmapEnabled<FramebufferNode>,
Util::IntrusiveListEnabled<FramebufferNode>,
Framebuffer
{
FramebufferNode(Device *device_, const RenderPass &rp, const RenderPassInfo &info_)
: Framebuffer(device_, rp, info_)
{
set_internal_sync_object();
}
};
Device *device;
Util::TemporaryHashmap<FramebufferNode, VULKAN_FRAMEBUFFER_RING_SIZE, false> framebuffers;
std::mutex lock;
};
class TransientAttachmentAllocator
{
public:
TransientAttachmentAllocator(Device *device_)
: device(device_)
{
}
ImageHandle request_attachment(unsigned width, unsigned height, VkFormat format,
unsigned index = 0, unsigned samples = 1, unsigned layers = 1);
void begin_frame();
void clear();
private:
struct TransientNode : Util::TemporaryHashmapEnabled<TransientNode>, Util::IntrusiveListEnabled<TransientNode>
{
explicit TransientNode(ImageHandle handle_)
: handle(std::move(handle_))
{
}
ImageHandle handle;
};
Device *device;
Util::TemporaryHashmap<TransientNode, VULKAN_FRAMEBUFFER_RING_SIZE, false> attachments;
std::mutex lock;
};
}
@@ -0,0 +1,127 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "device.hpp"
#include "renderdoc_app.h"
#include <mutex>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <dlfcn.h>
#endif
namespace Vulkan
{
static std::mutex module_lock;
#ifdef _WIN32
static HMODULE renderdoc_module;
#else
static void *renderdoc_module;
#endif
static RENDERDOC_API_1_0_0 *renderdoc_api;
bool Device::init_renderdoc_capture()
{
std::lock_guard<std::mutex> holder{module_lock};
if (renderdoc_module)
return true;
#ifdef _WIN32
renderdoc_module = GetModuleHandleA("renderdoc.dll");
#elif defined(ANDROID)
renderdoc_module = dlopen("libVkLayer_GLES_RenderDoc.so", RTLD_NOW | RTLD_NOLOAD);
#else
renderdoc_module = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD);
#endif
if (!renderdoc_module)
{
LOGE("Failed to load RenderDoc, make sure RenderDoc started the application in capture mode.\n");
return false;
}
#ifdef _WIN32
// Workaround GCC warning about FARPROC mismatch.
auto *gpa = GetProcAddress(renderdoc_module, "RENDERDOC_GetAPI");
pRENDERDOC_GetAPI func;
memcpy(&func, &gpa, sizeof(func));
if (!func)
{
LOGE("Failed to load RENDERDOC_GetAPI function.\n");
return false;
}
#else
auto *func = reinterpret_cast<pRENDERDOC_GetAPI>(dlsym(renderdoc_module, "RENDERDOC_GetAPI"));
if (!func)
{
LOGE("Failed to load RENDERDOC_GetAPI function.\n");
return false;
}
#endif
if (!func(eRENDERDOC_API_Version_1_0_0, reinterpret_cast<void **>(&renderdoc_api)))
{
LOGE("Failed to obtain RenderDoc 1.0.0 API.\n");
return false;
}
else
{
int major, minor, patch;
renderdoc_api->GetAPIVersion(&major, &minor, &patch);
LOGI("Initialized RenderDoc API %d.%d.%d.\n", major, minor, patch);
}
return true;
}
void Device::begin_renderdoc_capture()
{
std::lock_guard<std::mutex> holder{module_lock};
if (!renderdoc_api)
{
LOGE("RenderDoc API is not loaded, cannot trigger capture.\n");
return;
}
next_frame_context();
LOGI("Starting RenderDoc frame capture.\n");
renderdoc_api->StartFrameCapture(nullptr, nullptr);
}
void Device::end_renderdoc_capture()
{
std::lock_guard<std::mutex> holder{module_lock};
if (!renderdoc_api)
{
LOGE("RenderDoc API is not loaded, cannot trigger capture.\n");
return;
}
next_frame_context();
renderdoc_api->EndFrameCapture(nullptr, nullptr);
LOGI("Ended RenderDoc frame capture.\n");
}
}
@@ -0,0 +1,61 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "rtas.hpp"
#include "device.hpp"
namespace Vulkan
{
RTAS::RTAS(Device *device_, VkAccelerationStructureKHR rtas_,
VkAccelerationStructureTypeKHR type_, BufferHandle backing_)
: Cookie(device_)
, device(device_)
, rtas(rtas_)
, type(type_)
, backing(std::move(backing_))
{
VkAccelerationStructureDeviceAddressInfoKHR info =
{ VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR };
info.accelerationStructure = rtas;
bda = device->get_device_table().vkGetAccelerationStructureDeviceAddressKHR(device->get_device(), &info);
}
VkDeviceSize RTAS::get_scratch_size(BuildMode mode) const
{
return mode == BuildMode::Build ? build_size : update_size;
}
RTAS::~RTAS()
{
device->destroy_rtas(rtas);
}
void RTASDeleter::operator()(Vulkan::RTAS *rtas)
{
// Avoid hitting destruction callback inside a callback.
rtas->backing.reset();
rtas->device->handle_pool.rtas.free(rtas);
}
}
@@ -0,0 +1,131 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "cookie.hpp"
#include "vulkan_common.hpp"
#include "memory_allocator.hpp"
#include "buffer.hpp"
namespace Vulkan
{
class RTAS;
class Device;
struct RTASDeleter
{
void operator()(RTAS *rtas);
};
enum class BLASMode
{
Static, // fast trace, compactable, not updateable
Skinned // fast update, updateable
};
enum class BuildMode
{
Build,
Update
};
struct BottomRTASGeometry
{
VkFormat format;
VkDeviceAddress vbo;
uint32_t num_vertices;
uint32_t stride;
VkDeviceAddress ibo;
VkIndexType index_type;
uint32_t num_primitives;
VkDeviceAddress transform;
};
struct BottomRTASCreateInfo
{
BLASMode mode;
const BottomRTASGeometry *geometries;
size_t count;
};
struct RTASInstance
{
// One of the two.
const VkAccelerationStructureInstanceKHR *instance;
VkDeviceAddress bda;
};
struct TopRTASCreateInfo
{
const RTASInstance *instances;
size_t count;
};
class RTAS : public Util::IntrusivePtrEnabled<RTAS, RTASDeleter, HandleCounter>,
public Cookie
{
public:
friend struct RTASDeleter;
~RTAS();
inline VkAccelerationStructureKHR get_rtas() const
{
return rtas;
}
inline VkAccelerationStructureTypeKHR get_type() const
{
return type;
}
inline VkDeviceAddress get_device_address() const
{
return bda;
}
VkDeviceSize get_scratch_size(BuildMode mode) const;
inline void set_scratch_size(VkDeviceSize build_size_, VkDeviceSize update_size_)
{
build_size = build_size_;
update_size = update_size_;
}
private:
friend class Util::ObjectPool<RTAS>;
RTAS(Device *device, VkAccelerationStructureKHR rtas,
VkAccelerationStructureTypeKHR type, BufferHandle backing);
Device *device;
VkAccelerationStructureKHR rtas;
VkAccelerationStructureTypeKHR type;
BufferHandle backing;
VkDeviceSize build_size;
VkDeviceSize update_size;
VkDeviceAddress bda = 0;
};
using RTASHandle = Util::IntrusivePtr<RTAS>;
}
@@ -0,0 +1,173 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "sampler.hpp"
#include "device.hpp"
namespace Vulkan
{
Sampler::Sampler(Device *device_, VkSampler sampler_, const SamplerCreateInfo &info, bool immutable_)
: Cookie(device_)
, device(device_)
, sampler(sampler_)
, create_info(info)
, immutable(immutable_)
{
// In heap, the VkSampler is a dummy object which is literally just the index into heap.
if (device->get_device_features().supports_descriptor_buffer &&
!device->get_device_features().descriptor_heap_features.descriptorHeap)
{
payload = device->managers.descriptor_buffer.alloc_sampler();
VkDescriptorGetInfoEXT get_info = { VK_STRUCTURE_TYPE_DESCRIPTOR_GET_INFO_EXT };
get_info.type = VK_DESCRIPTOR_TYPE_SAMPLER;
get_info.data.pSampler = &sampler;
device->get_device_table().vkGetDescriptorEXT(
device->get_device(),
&get_info,
device->get_device_features().descriptor_buffer_properties.samplerDescriptorSize,
payload.ptr);
}
}
Sampler::~Sampler()
{
if (sampler)
{
if (immutable)
device->managers.descriptor_buffer.destroy_sampler(sampler);
else if (internal_sync)
device->destroy_sampler_nolock(sampler);
else
device->destroy_sampler(sampler);
}
if (payload)
{
if (internal_sync)
device->free_cached_descriptor_payload_nolock(payload);
else
device->free_cached_descriptor_payload(payload);
}
}
void SamplerDeleter::operator()(Sampler *sampler)
{
sampler->device->handle_pool.samplers.free(sampler);
}
SamplerCreateInfo Sampler::fill_sampler_info(const VkSamplerCreateInfo &info)
{
SamplerCreateInfo sampler_info = {};
sampler_info.mag_filter = info.magFilter;
sampler_info.min_filter = info.minFilter;
sampler_info.mipmap_mode = info.mipmapMode;
sampler_info.address_mode_u = info.addressModeU;
sampler_info.address_mode_v = info.addressModeV;
sampler_info.address_mode_w = info.addressModeW;
sampler_info.mip_lod_bias = info.mipLodBias;
sampler_info.anisotropy_enable = info.anisotropyEnable;
sampler_info.max_anisotropy = info.maxAnisotropy;
sampler_info.compare_enable = info.compareEnable;
sampler_info.compare_op = info.compareOp;
sampler_info.min_lod = info.minLod;
sampler_info.max_lod = info.maxLod;
sampler_info.border_color = info.borderColor;
sampler_info.unnormalized_coordinates = info.unnormalizedCoordinates;
return sampler_info;
}
VkSamplerCreateInfo Sampler::fill_vk_sampler_info(const SamplerCreateInfo &sampler_info)
{
VkSamplerCreateInfo info = { VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO };
info.magFilter = sampler_info.mag_filter;
info.minFilter = sampler_info.min_filter;
info.mipmapMode = sampler_info.mipmap_mode;
info.addressModeU = sampler_info.address_mode_u;
info.addressModeV = sampler_info.address_mode_v;
info.addressModeW = sampler_info.address_mode_w;
info.mipLodBias = sampler_info.mip_lod_bias;
info.anisotropyEnable = sampler_info.anisotropy_enable;
info.maxAnisotropy = sampler_info.max_anisotropy;
info.compareEnable = sampler_info.compare_enable;
info.compareOp = sampler_info.compare_op;
info.minLod = sampler_info.min_lod;
info.maxLod = sampler_info.max_lod;
info.borderColor = sampler_info.border_color;
info.unnormalizedCoordinates = sampler_info.unnormalized_coordinates;
return info;
}
ImmutableSampler::ImmutableSampler(Util::Hash hash, Device *device_, const SamplerCreateInfo &sampler_info,
const ImmutableYcbcrConversion *ycbcr_)
: HashedObject<ImmutableSampler>(hash), device(device_), ycbcr(ycbcr_)
{
VkSamplerYcbcrConversionInfo conv_info = { VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO };
auto info = Sampler::fill_vk_sampler_info(sampler_info);
if (ycbcr)
{
conv_info.conversion = ycbcr->get_conversion();
info.pNext = &conv_info;
}
VkSampler vk_sampler = device->managers.descriptor_buffer.create_sampler(&info);
#ifdef GRANITE_VULKAN_FOSSILIZE
// Immutable samplers are on the chopping block ...
if (!device->get_device_features().supports_descriptor_buffer_or_heap)
device->register_sampler(vk_sampler, hash, info);
#endif
sampler = SamplerHandle(device->handle_pool.samplers.allocate(device, vk_sampler, sampler_info, true));
}
ImmutableYcbcrConversion::ImmutableYcbcrConversion(Util::Hash hash, Device *device_,
const VkSamplerYcbcrConversionCreateInfo &info)
: HashedObject<ImmutableYcbcrConversion>(hash), device(device_)
{
if (device->get_device_features().vk11_features.samplerYcbcrConversion)
{
if (device->get_device_table().vkCreateSamplerYcbcrConversion(device->get_device(), &info, nullptr,
&conversion) != VK_SUCCESS)
{
LOGE("Failed to create YCbCr conversion.\n");
}
else
{
#ifdef GRANITE_VULKAN_FOSSILIZE
device->register_sampler_ycbcr_conversion(conversion, info);
#endif
}
}
else
LOGE("Ycbcr conversion is not supported on this device.\n");
}
ImmutableYcbcrConversion::~ImmutableYcbcrConversion()
{
if (conversion)
device->get_device_table().vkDestroySamplerYcbcrConversion(device->get_device(), conversion, nullptr);
}
}
@@ -0,0 +1,154 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "cookie.hpp"
#include "vulkan_common.hpp"
#include "vulkan_headers.hpp"
#include "object_pool.hpp"
#include "memory_allocator.hpp"
namespace Vulkan
{
enum class StockSampler
{
NearestClamp,
LinearClamp,
TrilinearClamp,
NearestWrap,
LinearWrap,
TrilinearWrap,
NearestShadow,
LinearShadow,
DefaultGeometryFilterClamp,
DefaultGeometryFilterWrap,
Count
};
struct SamplerCreateInfo
{
VkFilter mag_filter;
VkFilter min_filter;
VkSamplerMipmapMode mipmap_mode;
VkSamplerAddressMode address_mode_u;
VkSamplerAddressMode address_mode_v;
VkSamplerAddressMode address_mode_w;
float mip_lod_bias;
VkBool32 anisotropy_enable;
float max_anisotropy;
VkBool32 compare_enable;
VkCompareOp compare_op;
float min_lod;
float max_lod;
VkBorderColor border_color;
VkBool32 unnormalized_coordinates;
};
class Sampler;
struct SamplerDeleter
{
void operator()(Sampler *sampler);
};
class Sampler : public Util::IntrusivePtrEnabled<Sampler, SamplerDeleter, HandleCounter>,
public Cookie, public InternalSyncEnabled
{
public:
friend struct SamplerDeleter;
~Sampler();
VkSampler get_sampler() const
{
return sampler;
}
const CachedDescriptorPayload &get_descriptor_payload() const
{
VK_ASSERT(payload && payload.type == VK_DESCRIPTOR_TYPE_SAMPLER);
return payload;
}
const SamplerCreateInfo &get_create_info() const
{
return create_info;
}
static VkSamplerCreateInfo fill_vk_sampler_info(const SamplerCreateInfo &sampler_info);
static SamplerCreateInfo fill_sampler_info(const VkSamplerCreateInfo &sampler_info);
private:
friend class Util::ObjectPool<Sampler>;
Sampler(Device *device, VkSampler sampler, const SamplerCreateInfo &info, bool immutable);
Device *device;
VkSampler sampler;
CachedDescriptorPayload payload;
SamplerCreateInfo create_info;
bool immutable;
};
using SamplerHandle = Util::IntrusivePtr<Sampler>;
class ImmutableYcbcrConversion : public HashedObject<ImmutableYcbcrConversion>
{
public:
ImmutableYcbcrConversion(Util::Hash hash, Device *device,
const VkSamplerYcbcrConversionCreateInfo &info);
~ImmutableYcbcrConversion();
void operator=(const ImmutableYcbcrConversion &) = delete;
ImmutableYcbcrConversion(const ImmutableYcbcrConversion &) = delete;
VkSamplerYcbcrConversion get_conversion() const
{
return conversion;
}
private:
Device *device;
VkSamplerYcbcrConversion conversion = VK_NULL_HANDLE;
};
class ImmutableSampler : public HashedObject<ImmutableSampler>
{
public:
ImmutableSampler(Util::Hash hash, Device *device,
const SamplerCreateInfo &info,
const ImmutableYcbcrConversion *ycbcr);
void operator=(const ImmutableSampler &) = delete;
ImmutableSampler(const ImmutableSampler &) = delete;
const Sampler &get_sampler() const
{
return *sampler;
}
VkSamplerYcbcrConversion get_ycbcr_conversion() const
{
return ycbcr ? ycbcr->get_conversion() : VK_NULL_HANDLE;
}
private:
Device *device;
const ImmutableYcbcrConversion *ycbcr;
SamplerHandle sampler;
};
}
@@ -0,0 +1,244 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "semaphore.hpp"
#include "device.hpp"
#ifndef _WIN32
#include <unistd.h>
#else
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
namespace Vulkan
{
SemaphoreHolder::~SemaphoreHolder()
{
recycle_semaphore();
}
void SemaphoreHolder::recycle_semaphore()
{
if (!owned)
return;
VK_ASSERT(semaphore);
if (internal_sync)
{
if (semaphore_type == VK_SEMAPHORE_TYPE_TIMELINE || external_compatible_features)
{
device->destroy_semaphore_nolock(semaphore);
}
else if (is_signalled())
{
// We can't just destroy a semaphore if we don't know who signals it (e.g. WSI).
// Have to consume it by waiting then recycle.
if (signal_is_foreign_queue)
device->consume_semaphore_nolock(semaphore);
else
device->destroy_semaphore_nolock(semaphore);
}
else
device->recycle_semaphore_nolock(semaphore);
}
else
{
if (semaphore_type == VK_SEMAPHORE_TYPE_TIMELINE || external_compatible_features)
{
device->destroy_semaphore(semaphore);
}
else if (is_signalled())
{
// We can't just destroy a semaphore if we don't know who signals it (e.g. WSI).
// Have to consume it by waiting then recycle.
if (signal_is_foreign_queue)
device->consume_semaphore(semaphore);
else
device->destroy_semaphore(semaphore);
}
else
device->recycle_semaphore(semaphore);
}
}
bool SemaphoreHolder::wait_timeline_timeout(uint64_t value, uint64_t timeout)
{
VK_ASSERT(semaphore_type == VK_SEMAPHORE_TYPE_TIMELINE);
VK_ASSERT(is_proxy_timeline());
VkSemaphoreWaitInfo wait_info = { VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO };
wait_info.pSemaphores = &semaphore;
wait_info.semaphoreCount = 1;
wait_info.pValues = &value;
return device->get_device_table().vkWaitSemaphores(device->get_device(), &wait_info, timeout) == VK_SUCCESS;
}
void SemaphoreHolder::wait_timeline(uint64_t value)
{
wait_timeline_timeout(value, UINT64_MAX);
}
SemaphoreHolder &SemaphoreHolder::operator=(SemaphoreHolder &&other) noexcept
{
if (this == &other)
return *this;
assert(device == other.device);
recycle_semaphore();
semaphore = other.semaphore;
timeline = other.timeline;
signalled = other.signalled;
pending_wait = other.pending_wait;
semaphore_type = other.semaphore_type;
owned = other.owned;
other.semaphore = VK_NULL_HANDLE;
other.timeline = 0;
other.signalled = false;
other.pending_wait = false;
other.owned = false;
return *this;
}
ExternalHandle SemaphoreHolder::export_to_handle()
{
ExternalHandle h;
if ((external_compatible_features & VK_EXTERNAL_SEMAPHORE_FEATURE_EXPORTABLE_BIT) == 0)
{
LOGE("Semaphore is not export compatible.\n");
return h;
}
if (!semaphore)
{
LOGE("Semaphore has already been consumed.\n");
return h;
}
// Technically we can export early with reference transference, but it's a bit dubious.
// We want to remain compatible with copy transference for later, e.g. SYNC_FD.
if (!signalled && semaphore_type == VK_SEMAPHORE_TYPE_BINARY)
{
LOGE("Cannot export payload from a semaphore that is not queued up for signal.\n");
return h;
}
#ifdef _WIN32
VkSemaphoreGetWin32HandleInfoKHR handle_info = { VK_STRUCTURE_TYPE_SEMAPHORE_GET_WIN32_HANDLE_INFO_KHR };
handle_info.semaphore = semaphore;
handle_info.handleType = external_compatible_handle_type;
if (device->get_device_table().vkGetSemaphoreWin32HandleKHR(device->get_device(), &handle_info, &h.handle) != VK_SUCCESS)
{
LOGE("Failed to export to opaque handle.\n");
h.handle = nullptr;
}
#else
VkSemaphoreGetFdInfoKHR fd_info = { VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR };
fd_info.semaphore = semaphore;
fd_info.handleType = external_compatible_handle_type;
if (device->get_device_table().vkGetSemaphoreFdKHR(device->get_device(), &fd_info, &h.handle) != VK_SUCCESS)
{
LOGE("Failed to export to opaque FD.\n");
h.handle = -1;
}
#endif
h.semaphore_handle_type = external_compatible_handle_type;
return h;
}
bool SemaphoreHolder::import_from_handle(ExternalHandle handle)
{
if ((external_compatible_features & VK_EXTERNAL_SEMAPHORE_FEATURE_IMPORTABLE_BIT) == 0)
{
LOGE("Semaphore is not import compatible.\n");
return false;
}
if (!semaphore)
{
LOGE("Semaphore has already been consumed.\n");
return false;
}
if (signalled)
{
LOGE("Cannot import payload to semaphore that is already signalled.\n");
return false;
}
if (handle.semaphore_handle_type != external_compatible_handle_type)
{
LOGE("Mismatch in semaphore handle type.\n");
return false;
}
#ifdef _WIN32
VkImportSemaphoreWin32HandleInfoKHR import = { VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_WIN32_HANDLE_INFO_KHR };
import.handle = handle.handle;
import.semaphore = semaphore;
import.handleType = handle.semaphore_handle_type;
import.flags = semaphore_type == VK_SEMAPHORE_TYPE_BINARY_KHR ? VK_SEMAPHORE_IMPORT_TEMPORARY_BIT : 0;
if (device->get_device_table().vkImportSemaphoreWin32HandleKHR(device->get_device(), &import) != VK_SUCCESS)
{
LOGE("Failed to import semaphore handle %p!\n", handle.handle);
return false;
}
#else
VkImportSemaphoreFdInfoKHR import = { VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR };
import.fd = handle.handle;
import.semaphore = semaphore;
import.handleType = handle.semaphore_handle_type;
import.flags = semaphore_type == VK_SEMAPHORE_TYPE_BINARY_KHR ? VK_SEMAPHORE_IMPORT_TEMPORARY_BIT : 0;
if (device->get_device_table().vkImportSemaphoreFdKHR(device->get_device(), &import) != VK_SUCCESS)
{
LOGE("Failed to import semaphore FD %d!\n", handle.handle);
return false;
}
#endif
if (ExternalHandle::semaphore_handle_type_imports_by_reference(import.handleType))
{
#ifdef _WIN32
// Consume the handle, since the VkSemaphore holds a reference on Win32.
::CloseHandle(handle.handle);
#else
::close(handle.handle);
#endif
}
signal_external();
return true;
}
void SemaphoreHolderDeleter::operator()(Vulkan::SemaphoreHolder *semaphore)
{
semaphore->device->handle_pool.semaphores.free(semaphore);
}
}
@@ -0,0 +1,211 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_common.hpp"
#include "vulkan_headers.hpp"
#include "cookie.hpp"
#include "object_pool.hpp"
namespace Vulkan
{
class Device;
class SemaphoreHolder;
struct SemaphoreHolderDeleter
{
void operator()(SemaphoreHolder *semaphore);
};
class SemaphoreHolder : public Util::IntrusivePtrEnabled<SemaphoreHolder, SemaphoreHolderDeleter, HandleCounter>,
public InternalSyncEnabled
{
public:
friend struct SemaphoreHolderDeleter;
~SemaphoreHolder();
const VkSemaphore &get_semaphore() const
{
return semaphore;
}
bool is_signalled() const
{
return signalled;
}
uint64_t get_timeline_value() const
{
VK_ASSERT(!owned && semaphore_type == VK_SEMAPHORE_TYPE_TIMELINE_KHR);
return timeline;
}
bool is_owned() const
{
return owned;
}
VkSemaphore consume()
{
auto ret = semaphore;
VK_ASSERT(semaphore);
VK_ASSERT(signalled);
semaphore = VK_NULL_HANDLE;
signalled = false;
owned = false;
return ret;
}
VkSemaphore release_semaphore()
{
auto ret = semaphore;
semaphore = VK_NULL_HANDLE;
signalled = false;
owned = false;
return ret;
}
void wait_external()
{
VK_ASSERT(semaphore);
VK_ASSERT(signalled);
signalled = false;
}
void signal_external()
{
VK_ASSERT(!signalled);
VK_ASSERT(semaphore);
signalled = true;
}
void set_signal_is_foreign_queue()
{
VK_ASSERT(signalled);
signal_is_foreign_queue = true;
}
void set_pending_wait()
{
pending_wait = true;
}
bool is_pending_wait() const
{
return pending_wait;
}
void set_external_object_compatible(VkExternalSemaphoreHandleTypeFlagBits handle_type,
VkExternalSemaphoreFeatureFlags features)
{
external_compatible_handle_type = handle_type;
external_compatible_features = features;
}
bool is_external_object_compatible() const
{
return external_compatible_features != 0;
}
VkSemaphoreTypeKHR get_semaphore_type() const
{
return semaphore_type;
}
bool is_proxy_timeline() const
{
return proxy_timeline;
}
void set_proxy_timeline()
{
proxy_timeline = true;
signalled = false;
}
// If successful, importing takes ownership of the handle/fd.
// Application can use dup() / DuplicateHandle() to keep a reference.
// Imported semaphores are assumed to be signalled, or pending to be signalled.
// All imports are performed with TEMPORARY permanence.
ExternalHandle export_to_handle();
bool import_from_handle(ExternalHandle handle);
VkExternalSemaphoreFeatureFlags get_external_features() const
{
return external_compatible_features;
}
VkExternalSemaphoreHandleTypeFlagBits get_external_handle_type() const
{
return external_compatible_handle_type;
}
SemaphoreHolder &operator=(SemaphoreHolder &&other) noexcept;
void wait_timeline(uint64_t value);
bool wait_timeline_timeout(uint64_t value, uint64_t timeout);
private:
friend class Util::ObjectPool<SemaphoreHolder>;
SemaphoreHolder(Device *device_, VkSemaphore semaphore_, bool signalled_, bool owned_)
: device(device_)
, semaphore(semaphore_)
, timeline(0)
, semaphore_type(VK_SEMAPHORE_TYPE_BINARY_KHR)
, signalled(signalled_)
, owned(owned_)
{
}
SemaphoreHolder(Device *device_, uint64_t timeline_, VkSemaphore semaphore_, bool owned_)
: device(device_)
, semaphore(semaphore_)
, timeline(timeline_)
, semaphore_type(VK_SEMAPHORE_TYPE_TIMELINE_KHR)
, owned(owned_)
{
}
explicit SemaphoreHolder(Device *device_)
: device(device_)
{
}
void recycle_semaphore();
Device *device;
VkSemaphore semaphore = VK_NULL_HANDLE;
uint64_t timeline = 0;
VkSemaphoreTypeKHR semaphore_type = VK_SEMAPHORE_TYPE_BINARY_KHR;
bool signalled = false;
bool pending_wait = false;
bool owned = false;
bool proxy_timeline = false;
bool signal_is_foreign_queue = false;
VkExternalSemaphoreHandleTypeFlagBits external_compatible_handle_type = {};
VkExternalSemaphoreFeatureFlags external_compatible_features = 0;
};
using Semaphore = Util::IntrusivePtr<SemaphoreHolder>;
}
@@ -0,0 +1,68 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "semaphore_manager.hpp"
#include "device.hpp"
namespace Vulkan
{
void SemaphoreManager::init(Device *device_)
{
device = device_;
table = &device->get_device_table();
}
SemaphoreManager::~SemaphoreManager()
{
for (auto &sem : semaphores)
table->vkDestroySemaphore(device->get_device(), sem, nullptr);
}
void SemaphoreManager::recycle(VkSemaphore sem)
{
if (sem != VK_NULL_HANDLE)
semaphores.push_back(sem);
}
VkSemaphore SemaphoreManager::request_cleared_semaphore()
{
if (semaphores.empty())
{
VkSemaphoreCreateInfo info = { VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
VkSemaphore semaphore;
if (table->vkCreateSemaphore(device->get_device(), &info, nullptr, &semaphore) != VK_SUCCESS)
{
LOGE("Failed to create semaphore.\n");
semaphore = VK_NULL_HANDLE;
}
return semaphore;
}
else
{
auto sem = semaphores.back();
semaphores.pop_back();
return sem;
}
}
}
@@ -0,0 +1,45 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_headers.hpp"
#include <vector>
namespace Vulkan
{
class Device;
class SemaphoreManager
{
public:
void init(Device *device);
~SemaphoreManager();
VkSemaphore request_cleared_semaphore();
void recycle(VkSemaphore semaphore);
private:
Device *device = nullptr;
const VolkDeviceTable *table = nullptr;
std::vector<VkSemaphore> semaphores;
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,357 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "cookie.hpp"
#include "descriptor_set.hpp"
#include "hash.hpp"
#include "intrusive.hpp"
#include "limits.hpp"
#include "vulkan_headers.hpp"
#include "enum_cast.hpp"
namespace spirv_cross
{
struct SPIRType;
}
namespace Vulkan
{
class Device;
enum class ShaderStage
{
Vertex = 0,
Fragment = 4, // Skip over tess/geom to match Vulkan ordering.
Compute,
Task,
Mesh,
Count
};
struct ResourceLayout
{
DescriptorSetLayout sets[VULKAN_NUM_DESCRIPTOR_SETS];
uint32_t input_mask = 0;
uint32_t output_mask = 0;
uint32_t push_constant_size = 0;
uint32_t spec_constant_mask = 0;
uint32_t bindless_set_mask = 0;
enum { Version = 7 };
bool unserialize(const uint8_t *data, size_t size);
bool serialize(uint8_t *data, size_t size) const;
static size_t serialization_size();
};
static_assert(sizeof(DescriptorSetLayout) % 8 == 0, "Size of DescriptorSetLayout does not align to 64 bits.");
struct CombinedResourceLayout
{
uint32_t attribute_mask = 0;
uint32_t render_target_mask = 0;
DescriptorSetLayout sets[VULKAN_NUM_DESCRIPTOR_SETS] = {};
uint32_t stages_for_bindings[VULKAN_NUM_DESCRIPTOR_SETS][VULKAN_NUM_BINDINGS] = {};
uint32_t stages_for_sets[VULKAN_NUM_DESCRIPTOR_SETS] = {};
VkPushConstantRange push_constant_range = {};
uint32_t descriptor_set_mask = 0;
uint32_t bindless_descriptor_set_mask = 0;
uint32_t spec_constant_mask[Util::ecast(ShaderStage::Count)] = {};
uint32_t combined_spec_constant_mask = 0;
Util::Hash push_constant_layout_hash = 0;
};
union CombinedImageSamplerIndex
{
struct
{
uint32_t image_heap_index : 20;
uint32_t sampler_heap_index : 12;
};
uint32_t word;
};
static_assert(sizeof(CombinedImageSamplerIndex) == sizeof(uint32_t), "Unexpected size of CombinedImageSamplerIndex.");
union ResourceBinding
{
VkDescriptorBufferInfo buffer;
struct
{
VkDescriptorImageInfo fp;
VkDescriptorImageInfo integer;
const uint8_t *fp_ptr;
const uint8_t *integer_ptr;
const uint8_t *sampler_ptr;
CombinedImageSamplerIndex fp_heap_index;
CombinedImageSamplerIndex integer_heap_index;
} image;
VkDescriptorAddressInfoEXT buffer_addr_buffer;
VkDeviceAddressRangeEXT buffer_addr_heap;
VkAccelerationStructureKHR rtas;
union
{
VkBufferView handle;
struct
{
const uint8_t *ptr;
uint32_t heap_index;
} buffer;
} buffer_view;
};
struct ResourceBindings
{
ResourceBinding bindings[VULKAN_NUM_DESCRIPTOR_SETS][VULKAN_NUM_BINDINGS];
uint64_t cookies[VULKAN_NUM_DESCRIPTOR_SETS][VULKAN_NUM_BINDINGS];
uint64_t secondary_cookies[VULKAN_NUM_DESCRIPTOR_SETS][VULKAN_NUM_BINDINGS];
uint8_t push_constant_data[VULKAN_PUSH_CONSTANT_SIZE];
union
{
uint32_t push_data_words[(VULKAN_PUSH_DATA_SIZE - VULKAN_PUSH_CONSTANT_SIZE) / (VULKAN_NUM_DESCRIPTOR_SETS * sizeof(uint32_t))];
VkDeviceAddress push_data_addr[(VULKAN_PUSH_DATA_SIZE - VULKAN_PUSH_CONSTANT_SIZE) / (VULKAN_NUM_DESCRIPTOR_SETS * sizeof(VkDeviceAddress))];
} inline_descriptors[VULKAN_NUM_DESCRIPTOR_SETS];
};
struct ImmutableSamplerBank
{
const ImmutableSampler *samplers[VULKAN_NUM_DESCRIPTOR_SETS][VULKAN_NUM_BINDINGS];
static void hash(Util::Hasher &h, const ImmutableSamplerBank *bank);
};
class PipelineLayout : public HashedObject<PipelineLayout>
{
public:
PipelineLayout(Util::Hash hash, Device *device, const CombinedResourceLayout &layout,
const ImmutableSamplerBank *sampler_bank);
~PipelineLayout();
const CombinedResourceLayout &get_resource_layout() const
{
return layout;
}
// Legacy
VkPipelineLayout get_layout() const
{
return pipe_layout;
}
DescriptorSetAllocator *get_allocator(unsigned set) const
{
return set_allocators[set];
}
VkDescriptorUpdateTemplate get_update_template(unsigned set) const
{
return update_template[set];
}
uint32_t get_push_set_index() const
{
return push_set_index;
}
// Heap
enum class DescriptorStrategy
{
// For images: a u32 index. For buffers: PUSH_ADDRESS.
Inline,
// Not compatible with array of samplers or combined image samplers.
// Not compatible with SSBO that need ArrayLength.
HeapSlice,
// Indirect version of inline, for larger sets.
IndirectTable,
};
// Allocation size from indirection table UBO.
uint32_t get_heap_table_size(uint32_t desc_set) const
{
return heap.heap_table_size[desc_set];
}
// Allocation size from descriptor heap.
// Used when we want to copy descriptors straight into the heap.
uint32_t get_heap_slice_size(uint32_t desc_set) const
{
return heap.heap_slice_size[desc_set];
}
uint32_t get_descriptor_set_push_buffer_offset(uint32_t desc_set) const
{
VK_ASSERT(desc_set < VULKAN_NUM_DESCRIPTOR_SETS);
return heap.push_buffer_offsets[desc_set];
}
uint32_t get_descriptor_set_push_image_offset(uint32_t desc_set) const
{
VK_ASSERT(desc_set < VULKAN_NUM_DESCRIPTOR_SETS);
return heap.push_image_offsets[desc_set];
}
uint32_t get_descriptor_set_inline_offsets(uint32_t desc_set) const
{
VK_ASSERT(desc_set < VULKAN_NUM_DESCRIPTOR_SETS);
return heap.push_inline_offsets[desc_set];
}
uint32_t get_descriptor_set_inline_size(uint32_t desc_set) const
{
VK_ASSERT(desc_set < VULKAN_NUM_DESCRIPTOR_SETS);
return heap.push_inline_size[desc_set];
}
DescriptorStrategy get_heap_buffer_descriptor_strategy(uint32_t desc_set) const
{
VK_ASSERT(desc_set < VULKAN_NUM_DESCRIPTOR_SETS);
return heap.buffer_strategies[desc_set];
}
DescriptorStrategy get_heap_image_descriptor_strategy(uint32_t desc_set) const
{
VK_ASSERT(desc_set < VULKAN_NUM_DESCRIPTOR_SETS);
return heap.image_strategies[desc_set];
}
// Inline: local offset into inline push data
// HeapSlice: offset into allocated heap slice
// IndirectTable: offset into indirect table
uint32_t get_descriptor_offset(uint32_t desc_set, uint32_t binding) const
{
VK_ASSERT(desc_set < VULKAN_NUM_DESCRIPTOR_SETS);
VK_ASSERT(binding < VULKAN_NUM_BINDINGS);
return heap.desc_offsets[desc_set][binding];
}
// Passed directly to CreatePipeline.
const std::vector<VkDescriptorSetAndBindingMappingEXT> &get_heap_mappings() const
{
return heap.mappings;
}
private:
Device *device;
VkPipelineLayout pipe_layout = VK_NULL_HANDLE;
CombinedResourceLayout layout;
DescriptorSetAllocator *set_allocators[VULKAN_NUM_DESCRIPTOR_SETS] = {};
VkDescriptorUpdateTemplate update_template[VULKAN_NUM_DESCRIPTOR_SETS] = {};
uint32_t push_set_index = UINT32_MAX;
void create_update_templates();
void init_heap();
void init_heap(uint32_t set_index);
void init_heap_buffers(uint32_t set_index);
void init_heap_image(uint32_t set_index);
void init_heap_offsets(uint32_t set_index);
void init_legacy(const ImmutableSamplerBank *immutable_samplers);
struct
{
std::vector<VkDescriptorSetAndBindingMappingEXT> mappings;
// Inline descriptors are packed together.
uint32_t push_inline_offsets[VULKAN_NUM_DESCRIPTOR_SETS];
uint32_t push_inline_size[VULKAN_NUM_DESCRIPTOR_SETS];
// For tables and slices.
uint32_t push_buffer_offsets[VULKAN_NUM_DESCRIPTOR_SETS];
uint32_t push_image_offsets[VULKAN_NUM_DESCRIPTOR_SETS];
uint32_t heap_table_size[VULKAN_NUM_DESCRIPTOR_SETS];
uint32_t heap_slice_size[VULKAN_NUM_DESCRIPTOR_SETS];
DescriptorStrategy buffer_strategies[VULKAN_NUM_DESCRIPTOR_SETS];
DescriptorStrategy image_strategies[VULKAN_NUM_DESCRIPTOR_SETS];
uint32_t desc_offsets[VULKAN_NUM_DESCRIPTOR_SETS][VULKAN_NUM_BINDINGS];
uint32_t push_data_size;
} heap = {};
};
class Shader : public HashedObject<Shader>
{
public:
Shader(Util::Hash binding, Device *device, const uint32_t *data, size_t size,
const ResourceLayout *layout = nullptr);
~Shader();
const ResourceLayout &get_layout() const
{
return layout;
}
VkShaderModule get_module() const
{
return module;
}
static bool reflect_resource_layout(ResourceLayout &layout, const uint32_t *spirv_data, size_t spirv_size);
static const char *stage_to_name(ShaderStage stage);
static Util::Hash hash(const uint32_t *data, size_t size);
private:
Device *device;
VkShaderModule module = VK_NULL_HANDLE;
ResourceLayout layout;
};
struct Pipeline
{
VkPipeline pipeline;
uint32_t dynamic_mask;
};
class Program : public HashedObject<Program>
{
public:
Program(Device *device, Shader *vertex, Shader *fragment, const ImmutableSamplerBank *sampler_bank);
Program(Device *device, Shader *task, Shader *mesh, Shader *fragment, const ImmutableSamplerBank *sampler_bank);
Program(Device *device, Shader *compute, const ImmutableSamplerBank *sampler_bank);
~Program();
inline const Shader *get_shader(ShaderStage stage) const
{
return shaders[Util::ecast(stage)];
}
void set_pipeline_layout(const PipelineLayout *new_layout)
{
layout = new_layout;
}
const PipelineLayout *get_pipeline_layout() const
{
return layout;
}
Pipeline get_pipeline(Util::Hash hash) const;
Pipeline add_pipeline(Util::Hash hash, const Pipeline &pipeline);
void promote_read_write_to_read_only();
private:
void set_shader(ShaderStage stage, Shader *handle);
Device *device;
Shader *shaders[Util::ecast(ShaderStage::Count)] = {};
const PipelineLayout *layout = nullptr;
VulkanCache<Util::IntrusivePODWrapper<Pipeline>> pipelines;
void destroy_pipeline(const Pipeline &pipeline);
};
}
@@ -0,0 +1,346 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "memory_mapped_texture.hpp"
#include <string.h>
#include <stdlib.h>
namespace Vulkan
{
struct MemoryMappedHeader
{
char magic[16];
VkImageType type;
VkFormat format;
uint32_t width;
uint32_t height;
uint32_t depth;
uint32_t layers;
uint32_t levels;
uint32_t flags;
uint64_t payload_size;
uint64_t reserved1;
};
static const size_t header_size = 16 + 8 * 4 + 2 * 8;
static_assert(sizeof(MemoryMappedHeader) == header_size, "Header size is not properly packed.");
static const char MAGIC[16] = "GRANITE TEXFMT1";
void MemoryMappedTexture::set_generate_mipmaps_on_load(bool enable)
{
mipgen_on_load = enable;
}
void MemoryMappedTexture::set_flags(MemoryMappedTextureFlags flags)
{
bool new_cube = (flags & MEMORY_MAPPED_TEXTURE_CUBE_MAP_COMPATIBLE_BIT) != 0;
if (new_cube != cube)
abort();
set_generate_mipmaps_on_load((flags & MEMORY_MAPPED_TEXTURE_GENERATE_MIPMAP_ON_LOAD_BIT) != 0);
}
MemoryMappedTextureFlags MemoryMappedTexture::get_flags() const
{
MemoryMappedTextureFlags flags = 0;
if (cube)
flags |= MEMORY_MAPPED_TEXTURE_CUBE_MAP_COMPATIBLE_BIT;
if (mipgen_on_load)
flags |= MEMORY_MAPPED_TEXTURE_GENERATE_MIPMAP_ON_LOAD_BIT;
flags |= swizzle.r << MEMORY_MAPPED_TEXTURE_SWIZZLE_R_SHIFT;
flags |= swizzle.g << MEMORY_MAPPED_TEXTURE_SWIZZLE_G_SHIFT;
flags |= swizzle.b << MEMORY_MAPPED_TEXTURE_SWIZZLE_B_SHIFT;
flags |= swizzle.a << MEMORY_MAPPED_TEXTURE_SWIZZLE_A_SHIFT;
return flags;
}
void MemoryMappedTexture::set_1d(VkFormat format, uint32_t width, uint32_t layers, uint32_t levels)
{
layout.set_1d(format, width, layers, levels);
cube = false;
}
void MemoryMappedTexture::set_2d(VkFormat format, uint32_t width, uint32_t height, uint32_t layers, uint32_t levels)
{
layout.set_2d(format, width, height, layers, levels);
cube = false;
}
void MemoryMappedTexture::set_3d(VkFormat format, uint32_t width, uint32_t height, uint32_t depth, uint32_t levels)
{
layout.set_3d(format, width, height, depth, levels);
cube = false;
}
void MemoryMappedTexture::set_cube(VkFormat format, uint32_t size, uint32_t cube_layers, uint32_t levels)
{
layout.set_2d(format, size, size, cube_layers * 6, levels);
cube = true;
}
bool MemoryMappedTexture::copy_to_path(Granite::Filesystem &fs, const std::string &path)
{
if (layout.get_required_size() == 0 || !mapped)
return false;
auto target_file = fs.open(path, Granite::FileMode::WriteOnly);
if (!target_file)
return false;
auto new_mapped = target_file->map_write(get_required_size());
if (!new_mapped)
return false;
memcpy(new_mapped->mutable_data(), mapped, get_required_size());
return true;
}
bool MemoryMappedTexture::map_write(Granite::FileMappingHandle new_file)
{
file = std::move(new_file);
mapped = file->mutable_data<uint8_t>();
MemoryMappedHeader header = {};
memcpy(header.magic, MAGIC, sizeof(MAGIC));
header.width = layout.get_width();
header.height = layout.get_height();
header.depth = layout.get_depth();
header.flags = get_flags();
header.layers = layout.get_layers();
header.levels = layout.get_levels();
header.payload_size = layout.get_required_size();
header.type = layout.get_image_type();
header.format = layout.get_format();
memcpy(mapped, &header, sizeof(header));
layout.set_buffer(mapped + sizeof(header), layout.get_required_size());
return true;
}
bool MemoryMappedTexture::map_write(Granite::Filesystem &fs, const std::string &path)
{
if (layout.get_required_size() == 0)
return false;
auto new_file = fs.open(path, Granite::FileMode::WriteOnly);
if (!new_file)
return false;
auto map_handle = new_file->map_write(get_required_size());
if (!map_handle)
return false;
return map_write(std::move(map_handle));
}
struct ScratchFile final : Granite::File
{
ScratchFile(const void *mapped, size_t size)
{
data.resize(size);
if (mapped)
memcpy(data.data(), mapped, size);
}
Granite::FileMappingHandle map_subset(uint64_t offset, size_t range) override
{
if (offset + range > data.size())
return {};
return Util::make_handle<Granite::FileMapping>(
reference_from_this(), offset,
data.data() + offset, range,
0, range);
}
Granite::FileMappingHandle map_write(size_t size) override
{
data.resize(size);
return map_subset(0, size);
}
void unmap(void *, size_t) override
{
}
uint64_t get_size() override
{
return data.size();
}
std::vector<uint8_t> data;
};
void MemoryMappedTexture::make_local_copy()
{
if (empty())
return;
auto new_file = Util::make_handle<ScratchFile>(mapped, get_required_size());
file = new_file->map();
mapped = file->mutable_data<uint8_t>();
layout.set_buffer(mapped + sizeof(MemoryMappedHeader),
get_required_size() - sizeof(MemoryMappedHeader));
}
bool MemoryMappedTexture::map_write_scratch()
{
if (layout.get_required_size() == 0)
return false;
auto new_file = Util::make_handle<ScratchFile>(nullptr, get_required_size());
if (new_file->get_size() < sizeof(MemoryMappedHeader))
return false;
auto new_mapped = new_file->map_write(get_required_size());
return map_write(std::move(new_mapped));
}
size_t MemoryMappedTexture::get_required_size() const
{
return layout.get_required_size() + sizeof(MemoryMappedHeader);
}
void MemoryMappedTexture::set_swizzle(const VkComponentMapping &swizzle_)
{
swizzle = swizzle_;
}
static void remap(VkComponentSwizzle &output, VkComponentSwizzle input,
const VkComponentMapping &mapping, VkComponentSwizzle identity)
{
if (input == VK_COMPONENT_SWIZZLE_IDENTITY)
input = identity;
switch (input)
{
case VK_COMPONENT_SWIZZLE_R:
output = mapping.r;
break;
case VK_COMPONENT_SWIZZLE_G:
output = mapping.g;
break;
case VK_COMPONENT_SWIZZLE_B:
output = mapping.b;
break;
case VK_COMPONENT_SWIZZLE_A:
output = mapping.a;
break;
case VK_COMPONENT_SWIZZLE_ONE:
case VK_COMPONENT_SWIZZLE_ZERO:
output = input;
break;
default:
output = VK_COMPONENT_SWIZZLE_IDENTITY;
break;
}
}
void MemoryMappedTexture::remap_swizzle(VkComponentMapping &mapping) const
{
VkComponentMapping new_mapping;
remap(new_mapping.r, swizzle.r, mapping, VK_COMPONENT_SWIZZLE_R);
remap(new_mapping.g, swizzle.g, mapping, VK_COMPONENT_SWIZZLE_G);
remap(new_mapping.b, swizzle.b, mapping, VK_COMPONENT_SWIZZLE_B);
remap(new_mapping.a, swizzle.a, mapping, VK_COMPONENT_SWIZZLE_A);
mapping = new_mapping;
}
bool MemoryMappedTexture::map_copy(const void *mapped_, size_t size)
{
auto new_file = Util::make_handle<ScratchFile>(mapped_, size);
if (new_file->get_size() < sizeof(MemoryMappedHeader))
return false;
auto new_mapped = new_file->map();
return map_read(std::move(new_mapped));
}
bool MemoryMappedTexture::map_read(Granite::FileMappingHandle new_file)
{
file = std::move(new_file);
mapped = const_cast<uint8_t *>(file->data<uint8_t>());
auto *header = reinterpret_cast<const MemoryMappedHeader *>(mapped);
switch (header->type)
{
case VK_IMAGE_TYPE_1D:
layout.set_1d(header->format, header->width, header->layers, header->levels);
break;
case VK_IMAGE_TYPE_2D:
layout.set_2d(header->format, header->width, header->height, header->layers, header->levels);
break;
case VK_IMAGE_TYPE_3D:
layout.set_3d(header->format, header->width, header->height, header->depth, header->levels);
break;
default:
return false;
}
cube = (header->flags & MEMORY_MAPPED_TEXTURE_CUBE_MAP_COMPATIBLE_BIT) != 0;
mipgen_on_load = (header->flags & MEMORY_MAPPED_TEXTURE_GENERATE_MIPMAP_ON_LOAD_BIT) != 0;
swizzle.r = static_cast<VkComponentSwizzle>((header->flags >> MEMORY_MAPPED_TEXTURE_SWIZZLE_R_SHIFT) & MEMORY_MAPPED_TEXTURE_SWIZZLE_MASK);
swizzle.g = static_cast<VkComponentSwizzle>((header->flags >> MEMORY_MAPPED_TEXTURE_SWIZZLE_G_SHIFT) & MEMORY_MAPPED_TEXTURE_SWIZZLE_MASK);
swizzle.b = static_cast<VkComponentSwizzle>((header->flags >> MEMORY_MAPPED_TEXTURE_SWIZZLE_B_SHIFT) & MEMORY_MAPPED_TEXTURE_SWIZZLE_MASK);
swizzle.a = static_cast<VkComponentSwizzle>((header->flags >> MEMORY_MAPPED_TEXTURE_SWIZZLE_A_SHIFT) & MEMORY_MAPPED_TEXTURE_SWIZZLE_MASK);
if ((layout.get_required_size() + sizeof(MemoryMappedHeader)) < file->get_size())
return false;
if (header->payload_size != layout.get_required_size())
return false;
layout.set_buffer(static_cast<uint8_t *>(mapped) + sizeof(MemoryMappedHeader), header->payload_size);
return true;
}
bool MemoryMappedTexture::map_read(Granite::Filesystem &fs, const std::string &path)
{
auto loaded_file = fs.open(path, Granite::FileMode::ReadOnly);
if (!loaded_file)
return false;
if (loaded_file->get_size() < sizeof(MemoryMappedHeader))
return false;
auto new_mapped = loaded_file->map();
if (!new_mapped)
return false;
return map_read(std::move(new_mapped));
}
bool MemoryMappedTexture::is_header(const void *mapped_, size_t size)
{
if (size < sizeof(MemoryMappedHeader))
return false;
return memcmp(mapped_, MAGIC, sizeof(MAGIC)) == 0;
}
}
@@ -0,0 +1,94 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "texture_format.hpp"
#include "filesystem.hpp"
namespace Vulkan
{
enum MemoryMappedTextureFlagBits
{
MEMORY_MAPPED_TEXTURE_CUBE_MAP_COMPATIBLE_BIT = 1 << 0,
MEMORY_MAPPED_TEXTURE_GENERATE_MIPMAP_ON_LOAD_BIT = 1 << 1,
MEMORY_MAPPED_TEXTURE_SWIZZLE_R_SHIFT = 16,
MEMORY_MAPPED_TEXTURE_SWIZZLE_G_SHIFT = 19,
MEMORY_MAPPED_TEXTURE_SWIZZLE_B_SHIFT = 22,
MEMORY_MAPPED_TEXTURE_SWIZZLE_A_SHIFT = 25,
MEMORY_MAPPED_TEXTURE_SWIZZLE_MASK = 0x7
};
using MemoryMappedTextureFlags = uint32_t;
class MemoryMappedTexture
{
public:
void set_1d(VkFormat format, uint32_t width, uint32_t layers = 1, uint32_t levels = 1);
void set_2d(VkFormat format, uint32_t width, uint32_t height, uint32_t layers = 1, uint32_t levels = 1);
void set_3d(VkFormat format, uint32_t width, uint32_t height, uint32_t depth, uint32_t levels = 1);
void set_cube(VkFormat format, uint32_t size, uint32_t cube_layers = 1, uint32_t levels = 1);
static bool is_header(const void *mapped, size_t size);
bool map_write(Granite::Filesystem &fs, const std::string &path);
bool map_write(Granite::FileMappingHandle file);
bool map_read(Granite::Filesystem &fs, const std::string &path);
bool map_read(Granite::FileMappingHandle file);
bool map_copy(const void *mapped, size_t size);
bool map_write_scratch();
bool copy_to_path(Granite::Filesystem &fs, const std::string &path);
void make_local_copy();
inline const Vulkan::TextureFormatLayout &get_layout() const
{
return layout;
}
void set_generate_mipmaps_on_load(bool enable = true);
MemoryMappedTextureFlags get_flags() const;
void set_flags(MemoryMappedTextureFlags flags);
size_t get_required_size() const;
void set_swizzle(const VkComponentMapping &swizzle);
void remap_swizzle(VkComponentMapping &mapping) const;
inline bool empty() const
{
return get_layout().get_required_size() == 0;
}
private:
Vulkan::TextureFormatLayout layout;
Granite::FileMappingHandle file;
uint8_t *mapped = nullptr;
bool cube = false;
bool mipgen_on_load = false;
VkComponentMapping swizzle = {
VK_COMPONENT_SWIZZLE_R,
VK_COMPONENT_SWIZZLE_G,
VK_COMPONENT_SWIZZLE_B,
VK_COMPONENT_SWIZZLE_A,
};
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "texture_format.hpp"
#include "command_buffer.hpp"
#include "device.hpp"
namespace Granite
{
Vulkan::ImageHandle decode_compressed_image(Vulkan::CommandBuffer &cmd, const Vulkan::TextureFormatLayout &layout,
VkFormat preferred_decode_format,
const VkComponentMapping &swizzle = {
VK_COMPONENT_SWIZZLE_R,
VK_COMPONENT_SWIZZLE_G,
VK_COMPONENT_SWIZZLE_B,
VK_COMPONENT_SWIZZLE_A,
});
}
@@ -0,0 +1,124 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "texture_files.hpp"
#include "stb_image.h"
#include "filesystem.hpp"
#include "muglm/muglm_impl.hpp"
#include <string.h>
namespace Vulkan
{
static MemoryMappedTexture load_stb(const void *data, size_t size, ColorSpace color)
{
int width, height;
int components;
auto *buffer = stbi_load_from_memory(static_cast<const stbi_uc *>(data), size, &width, &height, &components, 4);
if (!buffer)
return {};
MemoryMappedTexture tex;
tex.set_2d(color == ColorSpace::sRGB ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM, width, height);
tex.set_generate_mipmaps_on_load(true);
if (!tex.map_write_scratch())
return {};
memcpy(tex.get_layout().data(), buffer, width * height * 4);
stbi_image_free(buffer);
return tex;
}
static MemoryMappedTexture load_hdr(const void *data, size_t size)
{
int width, height;
int components;
auto *buffer = stbi_loadf_from_memory(static_cast<const stbi_uc *>(data), size, &width, &height, &components, 3);
MemoryMappedTexture tex;
tex.set_2d(VK_FORMAT_R16G16B16A16_SFLOAT, width, height);
if (!tex.map_write_scratch())
return {};
tex.set_generate_mipmaps_on_load(true);
auto *converted = static_cast<muglm::u16vec4 *>(tex.get_layout().data());
for (int i = 0; i < width * height; i++)
{
converted[i] = muglm::floatToHalf(muglm::vec4(buffer[3 * i + 0], buffer[3 * i + 1], buffer[3 * i + 2], 1.0f));
}
stbi_image_free(buffer);
return tex;
}
MemoryMappedTexture load_texture_from_memory(const void *data, size_t size, ColorSpace color)
{
static const uint8_t png_magic[] = {
0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a,
};
static const uint8_t jpg_magic[] = {
0xff, 0xd8,
};
static const uint8_t hdr_magic[] = {
0x23, 0x3f, 0x52, 0x41, 0x44, 0x49, 0x41, 0x4e, 0x43, 0x45, 0x0a,
};
if (size >= sizeof(png_magic) && memcmp(data, png_magic, sizeof(png_magic)) == 0)
return load_stb(data, size, color);
else if (size >= 2 && memcmp(data, jpg_magic, sizeof(jpg_magic)) == 0)
return load_stb(data, size, color);
else if (size >= sizeof(hdr_magic) && memcmp(data, hdr_magic, sizeof(hdr_magic)) == 0)
return load_hdr(data, size);
else if (MemoryMappedTexture::is_header(data, size))
{
MemoryMappedTexture mapped;
mapped.map_copy(data, size);
return mapped;
}
else
{
// YOLO!
return load_stb(data, size, color);
}
}
MemoryMappedTexture load_texture_from_file(Granite::Filesystem &fs, const std::string &path, ColorSpace color)
{
auto file = fs.open(path, Granite::FileMode::ReadOnly);
if (!file)
return {};
auto mapped = file->map();
if (!mapped)
return {};
if (MemoryMappedTexture::is_header(mapped->data(), mapped->get_size()))
{
MemoryMappedTexture tex;
tex.map_read(std::move(mapped));
return tex;
}
return load_texture_from_memory(mapped->data(), mapped->get_size(), color);
}
}
@@ -0,0 +1,44 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "format.hpp"
#include "memory_mapped_texture.hpp"
namespace Granite
{
class Filesystem;
}
namespace Vulkan
{
enum class ColorSpace
{
Linear,
sRGB
};
MemoryMappedTexture load_texture_from_file(Granite::Filesystem &fs, const std::string &path, ColorSpace color = ColorSpace::sRGB);
MemoryMappedTexture load_texture_from_memory(const void *data, size_t size,
ColorSpace color = ColorSpace::sRGB);
}
@@ -0,0 +1,518 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#define NOMINMAX
#include "texture_format.hpp"
#include "format.hpp"
#include <algorithm>
namespace Vulkan
{
uint32_t TextureFormatLayout::num_miplevels(uint32_t width, uint32_t height, uint32_t depth)
{
uint32_t size = unsigned(std::max(std::max(width, height), depth));
uint32_t levels = 0;
while (size)
{
levels++;
size >>= 1;
}
return levels;
}
void TextureFormatLayout::format_block_dim(VkFormat format, uint32_t &width, uint32_t &height)
{
#define fmt(x, w, h) \
case VK_FORMAT_##x: \
width = w; \
height = h; \
break
switch (format)
{
fmt(ETC2_R8G8B8A8_UNORM_BLOCK, 4, 4);
fmt(ETC2_R8G8B8A8_SRGB_BLOCK, 4, 4);
fmt(ETC2_R8G8B8A1_UNORM_BLOCK, 4, 4);
fmt(ETC2_R8G8B8A1_SRGB_BLOCK, 4, 4);
fmt(ETC2_R8G8B8_UNORM_BLOCK, 4, 4);
fmt(ETC2_R8G8B8_SRGB_BLOCK, 4, 4);
fmt(EAC_R11_UNORM_BLOCK, 4, 4);
fmt(EAC_R11_SNORM_BLOCK, 4, 4);
fmt(EAC_R11G11_UNORM_BLOCK, 4, 4);
fmt(EAC_R11G11_SNORM_BLOCK, 4, 4);
fmt(BC1_RGB_UNORM_BLOCK, 4, 4);
fmt(BC1_RGB_SRGB_BLOCK, 4, 4);
fmt(BC1_RGBA_UNORM_BLOCK, 4, 4);
fmt(BC1_RGBA_SRGB_BLOCK, 4, 4);
fmt(BC2_UNORM_BLOCK, 4, 4);
fmt(BC2_SRGB_BLOCK, 4, 4);
fmt(BC3_UNORM_BLOCK, 4, 4);
fmt(BC3_SRGB_BLOCK, 4, 4);
fmt(BC4_UNORM_BLOCK, 4, 4);
fmt(BC4_SNORM_BLOCK, 4, 4);
fmt(BC5_UNORM_BLOCK, 4, 4);
fmt(BC5_SNORM_BLOCK, 4, 4);
fmt(BC6H_UFLOAT_BLOCK, 4, 4);
fmt(BC6H_SFLOAT_BLOCK, 4, 4);
fmt(BC7_SRGB_BLOCK, 4, 4);
fmt(BC7_UNORM_BLOCK, 4, 4);
#define astc_fmt(w, h) \
fmt(ASTC_##w##x##h##_UNORM_BLOCK, w, h); \
fmt(ASTC_##w##x##h##_SRGB_BLOCK, w, h); \
fmt(ASTC_##w##x##h##_SFLOAT_BLOCK_EXT, w, h)
astc_fmt(4, 4);
astc_fmt(5, 4);
astc_fmt(5, 5);
astc_fmt(6, 5);
astc_fmt(6, 6);
astc_fmt(8, 5);
astc_fmt(8, 6);
astc_fmt(8, 8);
astc_fmt(10, 5);
astc_fmt(10, 6);
astc_fmt(10, 8);
astc_fmt(10, 10);
astc_fmt(12, 10);
astc_fmt(12, 12);
default:
width = 1;
height = 1;
break;
}
#undef fmt
#undef astc_fmt
}
uint32_t TextureFormatLayout::format_block_size(VkFormat format, VkImageAspectFlags aspect)
{
#define fmt(x, bpp) \
case VK_FORMAT_##x: \
return bpp
#define fmt2(x, bpp0, bpp1) \
case VK_FORMAT_##x: \
return aspect == VK_IMAGE_ASPECT_PLANE_0_BIT ? bpp0 : bpp1
switch (format)
{
fmt(R4G4_UNORM_PACK8, 1);
fmt(R4G4B4A4_UNORM_PACK16, 2);
fmt(B4G4R4A4_UNORM_PACK16, 2);
fmt(R5G6B5_UNORM_PACK16, 2);
fmt(B5G6R5_UNORM_PACK16, 2);
fmt(R5G5B5A1_UNORM_PACK16, 2);
fmt(B5G5R5A1_UNORM_PACK16, 2);
fmt(A1R5G5B5_UNORM_PACK16, 2);
fmt(R8_UNORM, 1);
fmt(R8_SNORM, 1);
fmt(R8_USCALED, 1);
fmt(R8_SSCALED, 1);
fmt(R8_UINT, 1);
fmt(R8_SINT, 1);
fmt(R8_SRGB, 1);
fmt(R8G8_UNORM, 2);
fmt(R8G8_SNORM, 2);
fmt(R8G8_USCALED, 2);
fmt(R8G8_SSCALED, 2);
fmt(R8G8_UINT, 2);
fmt(R8G8_SINT, 2);
fmt(R8G8_SRGB, 2);
fmt(R8G8B8_UNORM, 3);
fmt(R8G8B8_SNORM, 3);
fmt(R8G8B8_USCALED, 3);
fmt(R8G8B8_SSCALED, 3);
fmt(R8G8B8_UINT, 3);
fmt(R8G8B8_SINT, 3);
fmt(R8G8B8_SRGB, 3);
fmt(R8G8B8A8_UNORM, 4);
fmt(R8G8B8A8_SNORM, 4);
fmt(R8G8B8A8_USCALED, 4);
fmt(R8G8B8A8_SSCALED, 4);
fmt(R8G8B8A8_UINT, 4);
fmt(R8G8B8A8_SINT, 4);
fmt(R8G8B8A8_SRGB, 4);
fmt(B8G8R8A8_UNORM, 4);
fmt(B8G8R8A8_SNORM, 4);
fmt(B8G8R8A8_USCALED, 4);
fmt(B8G8R8A8_SSCALED, 4);
fmt(B8G8R8A8_UINT, 4);
fmt(B8G8R8A8_SINT, 4);
fmt(B8G8R8A8_SRGB, 4);
fmt(A8B8G8R8_UNORM_PACK32, 4);
fmt(A8B8G8R8_SNORM_PACK32, 4);
fmt(A8B8G8R8_USCALED_PACK32, 4);
fmt(A8B8G8R8_SSCALED_PACK32, 4);
fmt(A8B8G8R8_UINT_PACK32, 4);
fmt(A8B8G8R8_SINT_PACK32, 4);
fmt(A8B8G8R8_SRGB_PACK32, 4);
fmt(A2B10G10R10_UNORM_PACK32, 4);
fmt(A2B10G10R10_SNORM_PACK32, 4);
fmt(A2B10G10R10_USCALED_PACK32, 4);
fmt(A2B10G10R10_SSCALED_PACK32, 4);
fmt(A2B10G10R10_UINT_PACK32, 4);
fmt(A2B10G10R10_SINT_PACK32, 4);
fmt(A2R10G10B10_UNORM_PACK32, 4);
fmt(A2R10G10B10_SNORM_PACK32, 4);
fmt(A2R10G10B10_USCALED_PACK32, 4);
fmt(A2R10G10B10_SSCALED_PACK32, 4);
fmt(A2R10G10B10_UINT_PACK32, 4);
fmt(A2R10G10B10_SINT_PACK32, 4);
fmt(R16_UNORM, 2);
fmt(R16_SNORM, 2);
fmt(R16_USCALED, 2);
fmt(R16_SSCALED, 2);
fmt(R16_UINT, 2);
fmt(R16_SINT, 2);
fmt(R16_SFLOAT, 2);
fmt(R16G16_UNORM, 4);
fmt(R16G16_SNORM, 4);
fmt(R16G16_USCALED, 4);
fmt(R16G16_SSCALED, 4);
fmt(R16G16_UINT, 4);
fmt(R16G16_SINT, 4);
fmt(R16G16_SFLOAT, 4);
fmt(R16G16B16_UNORM, 6);
fmt(R16G16B16_SNORM, 6);
fmt(R16G16B16_USCALED, 6);
fmt(R16G16B16_SSCALED, 6);
fmt(R16G16B16_UINT, 6);
fmt(R16G16B16_SINT, 6);
fmt(R16G16B16_SFLOAT, 6);
fmt(R16G16B16A16_UNORM, 8);
fmt(R16G16B16A16_SNORM, 8);
fmt(R16G16B16A16_USCALED, 8);
fmt(R16G16B16A16_SSCALED, 8);
fmt(R16G16B16A16_UINT, 8);
fmt(R16G16B16A16_SINT, 8);
fmt(R16G16B16A16_SFLOAT, 8);
fmt(R32_UINT, 4);
fmt(R32_SINT, 4);
fmt(R32_SFLOAT, 4);
fmt(R32G32_UINT, 8);
fmt(R32G32_SINT, 8);
fmt(R32G32_SFLOAT, 8);
fmt(R32G32B32_UINT, 12);
fmt(R32G32B32_SINT, 12);
fmt(R32G32B32_SFLOAT, 12);
fmt(R32G32B32A32_UINT, 16);
fmt(R32G32B32A32_SINT, 16);
fmt(R32G32B32A32_SFLOAT, 16);
fmt(R64_UINT, 8);
fmt(R64_SINT, 8);
fmt(R64_SFLOAT, 8);
fmt(R64G64_UINT, 16);
fmt(R64G64_SINT, 16);
fmt(R64G64_SFLOAT, 16);
fmt(R64G64B64_UINT, 24);
fmt(R64G64B64_SINT, 24);
fmt(R64G64B64_SFLOAT, 24);
fmt(R64G64B64A64_UINT, 32);
fmt(R64G64B64A64_SINT, 32);
fmt(R64G64B64A64_SFLOAT, 32);
fmt(B10G11R11_UFLOAT_PACK32, 4);
fmt(E5B9G9R9_UFLOAT_PACK32, 4);
fmt(D16_UNORM, 2);
fmt(X8_D24_UNORM_PACK32, 4);
fmt(D32_SFLOAT, 4);
fmt(S8_UINT, 1);
case VK_FORMAT_D16_UNORM_S8_UINT:
return aspect == VK_IMAGE_ASPECT_DEPTH_BIT ? 2 : 1;
case VK_FORMAT_D24_UNORM_S8_UINT:
case VK_FORMAT_D32_SFLOAT_S8_UINT:
return aspect == VK_IMAGE_ASPECT_DEPTH_BIT ? 4 : 1;
// ETC2
fmt(ETC2_R8G8B8A8_UNORM_BLOCK, 16);
fmt(ETC2_R8G8B8A8_SRGB_BLOCK, 16);
fmt(ETC2_R8G8B8A1_UNORM_BLOCK, 8);
fmt(ETC2_R8G8B8A1_SRGB_BLOCK, 8);
fmt(ETC2_R8G8B8_UNORM_BLOCK, 8);
fmt(ETC2_R8G8B8_SRGB_BLOCK, 8);
fmt(EAC_R11_UNORM_BLOCK, 8);
fmt(EAC_R11_SNORM_BLOCK, 8);
fmt(EAC_R11G11_UNORM_BLOCK, 16);
fmt(EAC_R11G11_SNORM_BLOCK, 16);
// BC
fmt(BC1_RGB_UNORM_BLOCK, 8);
fmt(BC1_RGB_SRGB_BLOCK, 8);
fmt(BC1_RGBA_UNORM_BLOCK, 8);
fmt(BC1_RGBA_SRGB_BLOCK, 8);
fmt(BC2_UNORM_BLOCK, 16);
fmt(BC2_SRGB_BLOCK, 16);
fmt(BC3_UNORM_BLOCK, 16);
fmt(BC3_SRGB_BLOCK, 16);
fmt(BC4_UNORM_BLOCK, 8);
fmt(BC4_SNORM_BLOCK, 8);
fmt(BC5_UNORM_BLOCK, 16);
fmt(BC5_SNORM_BLOCK, 16);
fmt(BC6H_UFLOAT_BLOCK, 16);
fmt(BC6H_SFLOAT_BLOCK, 16);
fmt(BC7_SRGB_BLOCK, 16);
fmt(BC7_UNORM_BLOCK, 16);
// ASTC
#define astc_fmt(w, h) \
fmt(ASTC_##w##x##h##_UNORM_BLOCK, 16); \
fmt(ASTC_##w##x##h##_SRGB_BLOCK, 16); \
fmt(ASTC_##w##x##h##_SFLOAT_BLOCK_EXT, 16)
astc_fmt(4, 4);
astc_fmt(5, 4);
astc_fmt(5, 5);
astc_fmt(6, 5);
astc_fmt(6, 6);
astc_fmt(8, 5);
astc_fmt(8, 6);
astc_fmt(8, 8);
astc_fmt(10, 5);
astc_fmt(10, 6);
astc_fmt(10, 8);
astc_fmt(10, 10);
astc_fmt(12, 10);
astc_fmt(12, 12);
fmt(G8B8G8R8_422_UNORM, 4);
fmt(B8G8R8G8_422_UNORM, 4);
fmt(G8_B8_R8_3PLANE_420_UNORM, 1);
fmt2(G8_B8R8_2PLANE_420_UNORM, 1, 2);
fmt(G8_B8_R8_3PLANE_422_UNORM, 1);
fmt2(G8_B8R8_2PLANE_422_UNORM, 1, 2);
fmt(G8_B8_R8_3PLANE_444_UNORM, 1);
fmt(R10X6_UNORM_PACK16, 2);
fmt(R10X6G10X6_UNORM_2PACK16, 4);
fmt(R10X6G10X6B10X6A10X6_UNORM_4PACK16, 8);
fmt(G10X6B10X6G10X6R10X6_422_UNORM_4PACK16, 8);
fmt(B10X6G10X6R10X6G10X6_422_UNORM_4PACK16, 8);
fmt(G10X6_B10X6_R10X6_3PLANE_420_UNORM_3PACK16, 2);
fmt(G10X6_B10X6_R10X6_3PLANE_422_UNORM_3PACK16, 2);
fmt(G10X6_B10X6_R10X6_3PLANE_444_UNORM_3PACK16, 2);
fmt2(G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16, 2, 4);
fmt2(G10X6_B10X6R10X6_2PLANE_422_UNORM_3PACK16, 2, 4);
fmt(R12X4_UNORM_PACK16, 2);
fmt(R12X4G12X4_UNORM_2PACK16, 4);
fmt(R12X4G12X4B12X4A12X4_UNORM_4PACK16, 8);
fmt(G12X4B12X4G12X4R12X4_422_UNORM_4PACK16, 8);
fmt(B12X4G12X4R12X4G12X4_422_UNORM_4PACK16, 8);
fmt(G12X4_B12X4_R12X4_3PLANE_420_UNORM_3PACK16, 2);
fmt(G12X4_B12X4_R12X4_3PLANE_422_UNORM_3PACK16, 2);
fmt(G12X4_B12X4_R12X4_3PLANE_444_UNORM_3PACK16, 2);
fmt2(G12X4_B12X4R12X4_2PLANE_420_UNORM_3PACK16, 2, 4);
fmt2(G12X4_B12X4R12X4_2PLANE_422_UNORM_3PACK16, 2, 4);
fmt(G16B16G16R16_422_UNORM, 8);
fmt(B16G16R16G16_422_UNORM, 8);
fmt(G16_B16_R16_3PLANE_420_UNORM, 2);
fmt(G16_B16_R16_3PLANE_422_UNORM, 2);
fmt(G16_B16_R16_3PLANE_444_UNORM, 2);
fmt2(G16_B16R16_2PLANE_420_UNORM, 2, 4);
fmt2(G16_B16R16_2PLANE_422_UNORM, 2, 4);
default:
assert(0 && "Unknown format.");
return 0;
}
#undef fmt
#undef fmt2
#undef astc_fmt
}
void TextureFormatLayout::fill_mipinfo(uint32_t width, uint32_t height, uint32_t depth)
{
block_stride = format_block_size(format, 0);
format_block_dim(format, block_dim_x, block_dim_y);
if (mip_levels == 0)
mip_levels = num_miplevels(width, height, depth);
size_t offset = 0;
for (uint32_t mip = 0; mip < mip_levels; mip++)
{
offset = (offset + 15) & ~15;
uint32_t blocks_x = (width + block_dim_x - 1) / block_dim_x;
uint32_t blocks_y = (height + block_dim_y - 1) / block_dim_y;
size_t mip_size = blocks_x * blocks_y * array_layers * depth * block_stride;
mips[mip].offset = offset;
mips[mip].block_row_length = blocks_x;
mips[mip].block_image_height = blocks_y;
mips[mip].row_length = blocks_x * block_dim_x;
mips[mip].image_height = blocks_y * block_dim_y;
mips[mip].width = width;
mips[mip].height = height;
mips[mip].depth = depth;
offset += mip_size;
width = std::max((width >> 1u), 1u);
height = std::max((height >> 1u), 1u);
depth = std::max((depth >> 1u), 1u);
}
required_size = offset;
}
void TextureFormatLayout::set_1d(VkFormat format_, uint32_t width, uint32_t array_layers_, uint32_t mip_levels_)
{
image_type = VK_IMAGE_TYPE_1D;
format = format_;
array_layers = array_layers_;
mip_levels = mip_levels_;
fill_mipinfo(width, 1, 1);
}
void TextureFormatLayout::set_2d(VkFormat format_, uint32_t width, uint32_t height,
uint32_t array_layers_, uint32_t mip_levels_)
{
image_type = VK_IMAGE_TYPE_2D;
format = format_;
array_layers = array_layers_;
mip_levels = mip_levels_;
fill_mipinfo(width, height, 1);
}
void TextureFormatLayout::set_3d(VkFormat format_, uint32_t width, uint32_t height, uint32_t depth, uint32_t mip_levels_)
{
image_type = VK_IMAGE_TYPE_3D;
format = format_;
array_layers = 1;
mip_levels = mip_levels_;
fill_mipinfo(width, height, depth);
}
void TextureFormatLayout::set_buffer(void *buffer_, size_t size)
{
buffer = static_cast<uint8_t *>(buffer_);
buffer_size = size;
}
uint32_t TextureFormatLayout::get_width(uint32_t mip) const
{
return mips[mip].width;
}
uint32_t TextureFormatLayout::get_height(uint32_t mip) const
{
return mips[mip].height;
}
uint32_t TextureFormatLayout::get_depth(uint32_t mip) const
{
return mips[mip].depth;
}
uint32_t TextureFormatLayout::get_layers() const
{
return array_layers;
}
VkImageType TextureFormatLayout::get_image_type() const
{
return image_type;
}
VkFormat TextureFormatLayout::get_format() const
{
return format;
}
uint32_t TextureFormatLayout::get_block_stride() const
{
return block_stride;
}
uint32_t TextureFormatLayout::get_levels() const
{
return mip_levels;
}
size_t TextureFormatLayout::get_required_size() const
{
return required_size;
}
const TextureFormatLayout::MipInfo &TextureFormatLayout::get_mip_info(uint32_t mip) const
{
return mips[mip];
}
uint32_t TextureFormatLayout::get_block_dim_x() const
{
return block_dim_x;
}
uint32_t TextureFormatLayout::get_block_dim_y() const
{
return block_dim_y;
}
size_t TextureFormatLayout::row_byte_stride(uint32_t row_length) const
{
return ((row_length + block_dim_x - 1) / block_dim_x) * block_stride;
}
size_t TextureFormatLayout::layer_byte_stride(uint32_t image_height, size_t row_byte_stride) const
{
return ((image_height + block_dim_y - 1) / block_dim_y) * row_byte_stride;
}
void TextureFormatLayout::build_buffer_image_copies(Util::SmallVector<VkBufferImageCopy, 32> &copies) const
{
copies.resize(mip_levels);
for (unsigned level = 0; level < mip_levels; level++)
{
const auto &mip_info = mips[level];
auto &blit = copies[level];
blit = {};
blit.bufferOffset = mip_info.offset;
blit.bufferRowLength = mip_info.row_length;
blit.bufferImageHeight = mip_info.image_height;
blit.imageSubresource.aspectMask = format_to_aspect_mask(format);
blit.imageSubresource.mipLevel = level;
blit.imageSubresource.baseArrayLayer = 0;
blit.imageSubresource.layerCount = array_layers;
blit.imageExtent.width = mip_info.width;
blit.imageExtent.height = mip_info.height;
blit.imageExtent.depth = mip_info.depth;
}
}
}
@@ -0,0 +1,178 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_headers.hpp"
#include "small_vector.hpp"
#include <vector>
#include <stddef.h>
#include <assert.h>
namespace Vulkan
{
class TextureFormatLayout
{
public:
void set_1d(VkFormat format, uint32_t width, uint32_t array_layers = 1, uint32_t mip_levels = 1);
void set_2d(VkFormat format, uint32_t width, uint32_t height, uint32_t array_layers = 1, uint32_t mip_levels = 1);
void set_3d(VkFormat format, uint32_t width, uint32_t height, uint32_t depth, uint32_t mip_levels = 1);
static uint32_t format_block_size(VkFormat format, VkImageAspectFlags aspect);
static void format_block_dim(VkFormat format, uint32_t &width, uint32_t &height);
static uint32_t num_miplevels(uint32_t width, uint32_t height = 1, uint32_t depth = 1);
void set_buffer(void *buffer, size_t size);
inline void *get_buffer()
{
return buffer;
}
uint32_t get_width(uint32_t mip = 0) const;
uint32_t get_height(uint32_t mip = 0) const;
uint32_t get_depth(uint32_t mip = 0) const;
uint32_t get_levels() const;
uint32_t get_layers() const;
uint32_t get_block_stride() const;
uint32_t get_block_dim_x() const;
uint32_t get_block_dim_y() const;
VkImageType get_image_type() const;
VkFormat get_format() const;
size_t get_required_size() const;
size_t row_byte_stride(uint32_t row_length) const;
size_t layer_byte_stride(uint32_t row_length, size_t row_byte_stride) const;
inline size_t get_row_size(uint32_t mip) const
{
return size_t(mips[mip].block_row_length) * block_stride;
}
inline size_t get_layer_size(uint32_t mip) const
{
return size_t(mips[mip].block_image_height) * get_row_size(mip);
}
struct MipInfo
{
size_t offset = 0;
uint32_t width = 1;
uint32_t height = 1;
uint32_t depth = 1;
uint32_t block_image_height = 0;
uint32_t block_row_length = 0;
uint32_t image_height = 0;
uint32_t row_length = 0;
};
const MipInfo &get_mip_info(uint32_t mip) const;
inline void *data(uint32_t layer = 0, uint32_t mip = 0) const
{
assert(buffer);
assert(buffer_size == required_size);
auto &mip_info = mips[mip];
uint8_t *slice = buffer + mip_info.offset;
slice += block_stride * layer * mip_info.block_row_length * mip_info.block_image_height;
return slice;
}
template <typename T>
inline T *data_generic(uint32_t x, uint32_t y, uint32_t slice_index, uint32_t mip = 0) const
{
auto &mip_info = mips[mip];
T *slice = reinterpret_cast<T *>(buffer + mip_info.offset);
slice += slice_index * mip_info.block_row_length * mip_info.block_image_height;
slice += y * mip_info.block_row_length;
slice += x;
return slice;
}
inline void *data_opaque(uint32_t x, uint32_t y, uint32_t slice_index, uint32_t mip = 0) const
{
auto &mip_info = mips[mip];
uint8_t *slice = buffer + mip_info.offset;
size_t off = slice_index * mip_info.block_row_length * mip_info.block_image_height;
off += y * mip_info.block_row_length;
off += x;
return slice + off * block_stride;
}
template <typename T>
inline T *data_generic() const
{
return data_generic<T>(0, 0, 0, 0);
}
template <typename T>
inline T *data_1d(uint32_t x, uint32_t layer = 0, uint32_t mip = 0) const
{
assert(sizeof(T) == block_stride);
assert(buffer);
assert(image_type == VK_IMAGE_TYPE_1D);
assert(buffer_size == required_size);
return data_generic<T>(x, 0, layer, mip);
}
template <typename T>
inline T *data_2d(uint32_t x, uint32_t y, uint32_t layer = 0, uint32_t mip = 0) const
{
assert(sizeof(T) == block_stride);
assert(buffer);
assert(image_type == VK_IMAGE_TYPE_2D);
assert(buffer_size == required_size);
return data_generic<T>(x, y, layer, mip);
}
template <typename T>
inline T *data_3d(uint32_t x, uint32_t y, uint32_t z, uint32_t mip = 0) const
{
assert(sizeof(T) == block_stride);
assert(buffer);
assert(image_type == VK_IMAGE_TYPE_3D);
assert(buffer_size == required_size);
return data_generic<T>(x, y, z, mip);
}
void build_buffer_image_copies(Util::SmallVector<VkBufferImageCopy, 32> &copies) const;
private:
uint8_t *buffer = nullptr;
size_t buffer_size = 0;
VkImageType image_type = VK_IMAGE_TYPE_MAX_ENUM;
VkFormat format = VK_FORMAT_UNDEFINED;
size_t required_size = 0;
uint32_t block_stride = 1;
uint32_t mip_levels = 1;
uint32_t array_layers = 1;
uint32_t block_dim_x = 1;
uint32_t block_dim_y = 1;
MipInfo mips[16];
void fill_mipinfo(uint32_t width, uint32_t height, uint32_t depth);
};
}
@@ -0,0 +1,117 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <string>
namespace Vulkan
{
static inline const char *layout_to_string(VkImageLayout layout)
{
switch (layout)
{
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
return "SHADER_READ_ONLY";
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL:
return "DS_READ_ONLY";
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
return "DS";
case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
return "COLOR";
case VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL:
return "ATTACHMENT";
case VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL:
return "READ_ONLY";
case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
return "TRANSFER_DST";
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
return "TRANSFER_SRC";
case VK_IMAGE_LAYOUT_GENERAL:
return "GENERAL";
case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR:
return "PRESENT";
default:
return "UNDEFINED";
}
}
static inline std::string access_flags_to_string(VkAccessFlags2 flags)
{
std::string result;
if (flags & VK_ACCESS_SHADER_READ_BIT)
result += "SHADER_READ ";
if (flags & VK_ACCESS_SHADER_WRITE_BIT)
result += "SHADER_WRITE ";
if (flags & VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT)
result += "DS_WRITE ";
if (flags & VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT)
result += "DS_READ ";
if (flags & VK_ACCESS_COLOR_ATTACHMENT_READ_BIT)
result += "COLOR_READ ";
if (flags & VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT)
result += "COLOR_WRITE ";
if (flags & VK_ACCESS_INPUT_ATTACHMENT_READ_BIT)
result += "INPUT_READ ";
if (flags & VK_ACCESS_TRANSFER_WRITE_BIT)
result += "TRANSFER_WRITE ";
if (flags & VK_ACCESS_TRANSFER_READ_BIT)
result += "TRANSFER_READ ";
if (flags & VK_ACCESS_UNIFORM_READ_BIT)
result += "UNIFORM_READ ";
if (!result.empty())
result.pop_back();
else
result = "NONE";
return result;
}
static inline std::string stage_flags_to_string(VkPipelineStageFlags2 flags)
{
std::string result;
if (flags & VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT)
result += "GRAPHICS ";
if (flags & (VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT))
result += "DEPTH ";
if (flags & VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT)
result += "COLOR ";
if (flags & VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT)
result += "FRAGMENT ";
if (flags & VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT)
result += "COMPUTE ";
if (flags & VK_PIPELINE_STAGE_TRANSFER_BIT)
result += "TRANSFER ";
if (flags & (VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT))
result += "VERTEX ";
if (!result.empty())
result.pop_back();
else
result = "NONE";
return result;
}
}
@@ -0,0 +1,141 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "intrusive.hpp"
#include "object_pool.hpp"
#include "intrusive_hash_map.hpp"
#include "vulkan_headers.hpp"
namespace Vulkan
{
using HandleCounter = Util::MultiThreadCounter;
template <typename T>
using VulkanObjectPool = Util::ThreadSafeObjectPool<T>;
template <typename T>
using VulkanCache = Util::ThreadSafeIntrusiveHashMapReadCached<T>;
template <typename T>
using VulkanCacheReadWrite = Util::ThreadSafeIntrusiveHashMap<T>;
enum QueueIndices
{
QUEUE_INDEX_GRAPHICS,
QUEUE_INDEX_COMPUTE,
QUEUE_INDEX_TRANSFER,
QUEUE_INDEX_VIDEO_DECODE,
QUEUE_INDEX_VIDEO_ENCODE,
QUEUE_INDEX_COUNT
};
struct ExternalHandle
{
#ifdef _WIN32
using NativeHandle = void *;
NativeHandle handle = nullptr;
#else
using NativeHandle = int;
NativeHandle handle = -1;
#endif
VkExternalMemoryHandleTypeFlagBits memory_handle_type = get_opaque_memory_handle_type();
VkExternalSemaphoreHandleTypeFlagBits semaphore_handle_type = get_opaque_semaphore_handle_type();
constexpr static VkExternalMemoryHandleTypeFlagBits get_opaque_memory_handle_type()
{
#ifdef _WIN32
return VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT;
#else
return VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
#endif
}
constexpr static VkExternalSemaphoreHandleTypeFlagBits get_opaque_semaphore_handle_type()
{
#ifdef _WIN32
return VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT;
#else
return VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT;
#endif
}
inline explicit operator bool() const
{
#ifdef _WIN32
return handle != nullptr;
#else
return handle >= 0;
#endif
}
static bool memory_handle_type_imports_by_reference(VkExternalMemoryHandleTypeFlagBits type)
{
VK_ASSERT(type == VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT ||
type == VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT ||
type == VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_KMT_BIT ||
type == VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_BIT ||
type == VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_KMT_BIT ||
type == VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_HEAP_BIT ||
type == VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT ||
type == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT);
return type != VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT &&
type != VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_KMT_BIT &&
type != VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_KMT_BIT &&
type != VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT;
}
static bool semaphore_handle_type_imports_by_reference(VkExternalSemaphoreHandleTypeFlagBits type)
{
VK_ASSERT(type == VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT ||
type == VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT ||
type == VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_KMT_BIT ||
type == VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT ||
type == VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT);
// D3D11 fence aliases D3D12 fence. It's basically the same thing, just D3D11.3.
return type != VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT &&
type != VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_KMT_BIT &&
type != VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT;
}
};
template <typename T>
static inline const T *find_pnext(const void *pnext, VkStructureType sType)
{
auto *chain = static_cast<const VkBaseInStructure *>(pnext);
while (chain)
{
if (chain->sType == sType)
break;
chain = static_cast<const VkBaseInStructure *>(chain->pNext);
}
return reinterpret_cast<const T *>(chain);
}
struct BufferMarkerHandle
{
enum : uint32_t { Invalid = UINT32_MAX };
uint32_t index = Invalid;
};
}
@@ -0,0 +1,71 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#if defined(_WIN32) && !defined(VK_USE_PLATFORM_WIN32_KHR)
#define VK_USE_PLATFORM_WIN32_KHR
#endif
#if defined(VULKAN_H_) || defined(VULKAN_CORE_H_)
#error "Must include vulkan_headers.hpp before Vulkan headers"
#endif
#include "volk.h"
#include <stdlib.h>
#include "logging.hpp"
#include <utility>
// Workaround silly Xlib headers that define macros for these globally :(
#ifdef None
#undef None
#endif
#ifdef Bool
#undef Bool
#endif
#ifdef Status
#undef Status
#endif
#ifdef VULKAN_DEBUG
#define VK_ASSERT(x) \
do \
{ \
if (!bool(x)) \
{ \
LOGE("Vulkan error at %s:%d.\n", __FILE__, __LINE__); \
abort(); \
} \
} while (0)
#else
#define VK_ASSERT(x) ((void)0)
#endif
namespace Vulkan
{
struct NoCopyNoMove
{
NoCopyNoMove() = default;
NoCopyNoMove(const NoCopyNoMove &) = delete;
void operator=(const NoCopyNoMove &) = delete;
};
}
@@ -0,0 +1,169 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "vulkan_headers.hpp"
namespace Vulkan
{
// FIXME: Also consider that we might have to flip X or Y w.r.t. dimensions,
// but that only matters for partial rendering ...
static inline bool surface_transform_swaps_xy(VkSurfaceTransformFlagBitsKHR transform)
{
return (transform & (
VK_SURFACE_TRANSFORM_HORIZONTAL_MIRROR_ROTATE_90_BIT_KHR |
VK_SURFACE_TRANSFORM_HORIZONTAL_MIRROR_ROTATE_270_BIT_KHR |
VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR |
VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR)) != 0;
}
static inline void viewport_transform_xy(VkViewport &vp, VkSurfaceTransformFlagBitsKHR transform,
uint32_t fb_width, uint32_t fb_height)
{
switch (transform)
{
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
{
float new_y = vp.x;
float new_x = float(fb_width) - (vp.y + vp.height);
vp.x = new_x;
vp.y = new_y;
std::swap(vp.width, vp.height);
break;
}
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
{
// Untested. Cannot make Android trigger this mode.
float new_left = float(fb_width) - (vp.x + vp.width);
float new_top = float(fb_height) - (vp.y + vp.height);
vp.x = new_left;
vp.y = new_top;
break;
}
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
{
float new_x = vp.y;
float new_y = float(fb_height) - (vp.x + vp.width);
vp.x = new_x;
vp.y = new_y;
std::swap(vp.width, vp.height);
break;
}
default:
break;
}
}
static inline void rect2d_clip(VkRect2D &rect)
{
if (rect.offset.x < 0)
{
rect.extent.width += rect.offset.x;
rect.offset.x = 0;
}
if (rect.offset.y < 0)
{
rect.extent.height += rect.offset.y;
rect.offset.y = 0;
}
rect.extent.width = std::min<uint32_t>(rect.extent.width, 0x7fffffffu - rect.offset.x);
rect.extent.height = std::min<uint32_t>(rect.extent.height, 0x7fffffffu - rect.offset.y);
}
static inline void rect2d_transform_xy(VkRect2D &rect, VkSurfaceTransformFlagBitsKHR transform,
uint32_t fb_width, uint32_t fb_height)
{
switch (transform)
{
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
{
int new_y = rect.offset.x;
int new_x = int(fb_width) - int(rect.offset.y + rect.extent.height);
rect.offset = { new_x, new_y };
std::swap(rect.extent.width, rect.extent.height);
break;
}
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
{
// Untested. Cannot make Android trigger this mode.
int new_left = int(fb_width) - int(rect.offset.x + rect.extent.width);
int new_top = int(fb_height) - int(rect.offset.y + rect.extent.height);
rect.offset = { new_left, new_top };
break;
}
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
{
int new_x = rect.offset.y;
int new_y = int(fb_height) - int(rect.offset.x + rect.extent.width);
rect.offset = { new_x, new_y };
std::swap(rect.extent.width, rect.extent.height);
break;
}
default:
break;
}
}
static inline void build_prerotate_matrix_2x2(VkSurfaceTransformFlagBitsKHR pre_rotate, float mat[4])
{
// TODO: HORIZONTAL_MIRROR.
switch (pre_rotate)
{
default:
mat[0] = 1.0f;
mat[1] = 0.0f;
mat[2] = 0.0f;
mat[3] = 1.0f;
break;
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
mat[0] = 0.0f;
mat[1] = 1.0f;
mat[2] = -1.0f;
mat[3] = 0.0f;
break;
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
mat[0] = 0.0f;
mat[1] = -1.0f;
mat[2] = 1.0f;
mat[3] = 0.0f;
break;
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
mat[0] = -1.0f;
mat[1] = 0.0f;
mat[2] = 0.0f;
mat[3] = -1.0f;
break;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,576 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "device.hpp"
#include "semaphore_manager.hpp"
#include "vulkan_headers.hpp"
#include "wsi_pacer.hpp"
#include "timer.hpp"
#include <vector>
#include <thread>
#include <chrono>
#include <memory>
#ifdef HAVE_WSI_DXGI_INTEROP
#include "wsi_dxgi.hpp"
#endif
namespace Granite
{
class InputTrackerHandler;
}
namespace Vulkan
{
class WSI;
class WSIPlatform
{
public:
virtual ~WSIPlatform() = default;
virtual VkSurfaceKHR create_surface(VkInstance instance, VkPhysicalDevice gpu) = 0;
// This is virtual so that application can hold ownership over the surface handle, for e.g. Qt interop.
virtual void destroy_surface(VkInstance instance, VkSurfaceKHR surface);
virtual std::vector<const char *> get_instance_extensions() = 0;
virtual std::vector<const char *> get_device_extensions()
{
return { "VK_KHR_swapchain" };
}
virtual VkFormat get_preferred_format()
{
return VK_FORMAT_B8G8R8A8_SRGB;
}
bool should_resize()
{
return resize;
}
virtual void notify_current_swapchain_dimensions(unsigned width, unsigned height)
{
resize = false;
current_swapchain_width = width;
current_swapchain_height = height;
}
virtual uint32_t get_surface_width() = 0;
virtual uint32_t get_surface_height() = 0;
virtual float get_aspect_ratio()
{
return float(get_surface_width()) / float(get_surface_height());
}
virtual bool alive(WSI &wsi) = 0;
virtual void poll_input() = 0;
virtual void poll_input_async(Granite::InputTrackerHandler *handler) = 0;
virtual bool has_external_swapchain()
{
return false;
}
virtual void block_until_wsi_forward_progress(WSI &wsi)
{
get_frame_timer().enter_idle();
while (!resize && alive(wsi))
{
poll_input();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
get_frame_timer().leave_idle();
}
Util::FrameTimer &get_frame_timer()
{
return timer;
}
virtual void release_resources()
{
}
virtual void event_device_created(Device *device);
virtual void event_device_destroyed();
virtual void event_swapchain_created(Device *device, VkSwapchainKHR swapchain,
unsigned width, unsigned height,
float aspect_ratio, size_t num_swapchain_images,
VkFormat format, VkColorSpaceKHR color_space,
VkSurfaceTransformFlagBitsKHR pre_rotate);
virtual void destroy_swapchain_resources(VkSwapchainKHR swapchain);
virtual void event_swapchain_destroyed();
virtual void event_frame_tick(double frame, double elapsed);
virtual void event_swapchain_index(Device *device, unsigned index);
virtual void set_window_title(const std::string &title);
virtual uintptr_t get_fullscreen_monitor();
virtual uintptr_t get_native_window();
virtual const VkApplicationInfo *get_application_info();
virtual void begin_drop_event();
virtual void begin_soft_keyboard(const std::string &initial);
virtual void end_soft_keyboard();
enum class MessageType { Error, Warning, Info };
virtual void show_message_box(const std::string &str, MessageType type);
protected:
unsigned current_swapchain_width = 0;
unsigned current_swapchain_height = 0;
bool resize = false;
private:
Util::FrameTimer timer;
};
enum class PresentMode
{
SyncToVBlank, // Force FIFO
UnlockedMaybeTear, // MAILBOX or IMMEDIATE
UnlockedForceTearing, // Force IMMEDIATE
UnlockedNoTearing // Force MAILBOX
};
enum class BackbufferFormat
{
UNORM,
sRGB,
HDR10,
DisplayP3,
UNORMPassthrough,
scRGB,
Custom
};
struct PresentationStats
{
// Correlate with WSI::get_last_submitted_present_id() + 1.
uint64_t feedback_present_id;
// QUEUE_COMPLETE query. May be 0 if implementation does not support it.
// Application can trivially implement this on its own if needed.
uint64_t gpu_done_ts;
// This is the latest stage that is reported.
uint64_t present_done_ts;
// actual presented - intended target presentation.
// Intended target presentation may be adjusted internally,
// especially when emulating absolute over relative and vice versa.
int64_t error;
};
enum class RefreshMode { Unknown, FRR, VRR };
struct RefreshRateInfo
{
RefreshMode mode;
uint64_t refresh_duration;
uint64_t refresh_interval;
};
class WSI
{
public:
WSI();
void set_platform(WSIPlatform *platform);
void set_present_mode(PresentMode mode);
void set_backbuffer_format(BackbufferFormat format);
// This is mostly for debug/development, ignores exposed formats and forces it.
void set_custom_backbuffer_format(VkSurfaceFormatKHR format);
struct ImageCompression
{
VkImageCompressionFlagBitsEXT type = VK_IMAGE_COMPRESSION_DEFAULT_EXT;
VkImageCompressionFixedRateFlagsEXT fixed_rates = 0;
};
void set_image_compression_control(const ImageCompression &compression);
// Latency is normally pretty low, but this aims to target
// really low latency. Only suitable for cases where rendering loads are extremely simple.
void set_present_low_latency_mode(bool enable);
// Engages NV_low_latency2 / AMD_anti_lag, etc, which aim to reduce CPU <-> GPU submit delays.
void set_gpu_submit_low_latency_mode(bool enable);
inline BackbufferFormat get_backbuffer_format() const
{
return backbuffer_format;
}
inline VkColorSpaceKHR get_backbuffer_color_space() const
{
return swapchain_surface_format.colorSpace;
}
void set_support_prerotate(bool enable);
void set_extra_usage_flags(VkImageUsageFlags usage);
VkSurfaceTransformFlagBitsKHR get_current_prerotate() const;
inline PresentMode get_present_mode() const
{
return present_mode;
}
// Deprecated, use set_backbuffer_format().
void set_backbuffer_srgb(bool enable);
inline bool get_backbuffer_srgb() const
{
return backbuffer_format == BackbufferFormat::sRGB;
}
void set_hdr_metadata(const VkHdrMetadataEXT &metadata);
inline const VkHdrMetadataEXT &get_hdr_metadata() const
{
return hdr_metadata;
}
// First, we need a Util::IntrinsivePtr<Vulkan::Context>.
// This holds the instance and device.
// The simple approach. WSI internally creates the context with instance + device.
// Required information about extensions etc, is pulled from the platform.
bool init_context_from_platform(unsigned num_thread_indices, const Context::SystemHandles &system_handles);
// If you have your own VkInstance and/or VkDevice, you must create your own Vulkan::Context with
// the appropriate init() call. Based on the platform you use, you must make sure to enable the
// required extensions.
bool init_from_existing_context(ContextHandle context);
// Then we initialize the Vulkan::Device. Either lets WSI create its own device or reuse an existing handle.
// A device provided here must have been bound to the context.
bool init_device();
bool init_device(DeviceHandle device);
// Called after we have a device and context.
// Either we can use a swapchain based on VkSurfaceKHR, or we can supply our own images
// to create a virtual swapchain.
// init_surface_swapchain() is called once.
// Here we create the surface and perform creation of the first swapchain.
bool init_surface_swapchain();
bool init_external_swapchain(std::vector<ImageHandle> external_images);
// Calls init_context_from_platform -> init_device -> init_surface_swapchain in succession.
bool init_simple(unsigned num_thread_indices, const Context::SystemHandles &system_handles);
~WSI();
inline Context &get_context()
{
return *context;
}
inline Device &get_device()
{
return *device;
}
// Acquires a frame from swapchain, also calls poll_input() after acquire
// since acquire tends to block.
bool begin_frame();
// Presents and iterates frame context.
// Present is skipped if swapchain resource was not touched.
// The normal app loop is something like begin_frame() -> submit work -> end_frame().
bool end_frame();
// Signals that the next present is merely a dupe (or generated) of a previous one,
// and that frame should not participate in present wait.
void set_next_present_is_duplicated();
// If true, and present wait is supported, the implementation will use more swapchain images than normal,
// and make it feasible to render duplicate frames without needlessly draining the GPU of work.
// This is mostly just a thing for an emulator which may be outputting at 30 unique FPS, but at 60 VI/s,
// meaning the same frames is duplicated.
// With a large enough image count, can feasibly be used for multi-frame-gen or similar shenanigans.
void set_frame_duplication_aware(bool enable, uint32_t target_swapchain_images = 5);
// Overrides the present wait latency.
// 0 -> waits until last frame was presented before polling input. Hardest low-latency situation.
// Forced when low-latency present is enabled.
// 1 -> Normal ideal situation where we wait for previous frame's present to complete.
// Allows 1 frame worth of work for CPU to keep GPU fed properly.
// 2 -> Conservative, but useful when GPU bound to avoid pumping. Forced when using low gpu submit latency path.
void set_present_wait_latency(uint32_t latency);
// For external swapchains we don't have a normal acquire -> present cycle.
// - set_external_frame()
// - index replaces the acquire next image index.
// - acquire_semaphore replaces semaphore from acquire next image.
// - frame_time controls the frame time passed down.
// - begin_frame()
// - submit work
// - end_frame()
// - consume_external_release_semaphore()
// - Returns the release semaphore that can passed to the equivalent of QueuePresentKHR.
void set_external_frame(unsigned index, Semaphore acquire_semaphore, double frame_time);
Semaphore consume_external_release_semaphore();
CommandBuffer::Type get_current_present_queue_type() const;
// Equivalent to calling destructor.
void teardown();
WSIPlatform &get_platform()
{
VK_ASSERT(platform);
return *platform;
}
// For Android. Used in response to APP_CMD_{INIT,TERM}_WINDOW once
// we have a proper swapchain going.
// We have to completely drain swapchain before the window is terminated on Android.
void deinit_surface_and_swapchain();
void reinit_surface_and_swapchain(VkSurfaceKHR new_surface);
void set_window_title(const std::string &title);
double get_smooth_frame_time() const;
double get_smooth_elapsed_time() const;
bool get_presentation_stats(PresentationStats &stats) const;
bool get_refresh_rate_info(RefreshRateInfo &info) const;
uint64_t get_last_submitted_present_id() const;
// Absolute time is in terms of the Util::get_current_time_nsec() domain,
// i.e., time reported in presentation stats.
//
// For absolute time, the image must not be presented before the time given.
// For relative time, the time is intended to be abs_time = last_present + rel_time.
//
// If relative time is supported by implementation and relative_time_ns is not 0,
// absolute time requests are ignored. If absolute time is supported by implementation,
// a target absolute time will be computed based on absolute_time_ns and relative_time_ns.
//
// Rounding adjustments are allowed when the absolute time aligns closely
// with a refresh cycle, up to half a refresh cycle for FRR. For VRR, no adjustments are made.
//
// If this returns true, the request is expected to work as intended.
// If false, either it's unsupported, or we're not yet in a steady state where
// presentation timing reliably work (the first few frames after a swapchain is created may hit this).
// This state remains set until another request is made.
// For relative timings, this will work as expected as a method to set absolute target automatically,
// but absolute time obviously will not, since application is expected to set a new target time every frame.
// If force_vrr is true, the relative or absolute times are passed down with no bias.
bool set_target_presentation_time(uint64_t absolute_time_ns, uint64_t relative_time_ns, bool force_vrr);
void set_enable_timing_feedback(bool enable);
FixedRefreshRatePacer &get_fixed_rate_pacer();
void set_fixed_rate_low_latency_pacer(bool enable);
private:
void update_framebuffer(unsigned width, unsigned height);
ContextHandle context;
VkSurfaceKHR surface = VK_NULL_HANDLE;
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
std::vector<VkImage> swapchain_images;
std::vector<Semaphore> release_semaphores;
DeviceHandle device;
const VolkDeviceTable *table = nullptr;
FixedRefreshRatePacer frr_pacer;
bool frr_pacer_enable = false;
unsigned swapchain_width = 0;
unsigned swapchain_height = 0;
float swapchain_aspect_ratio = 1.0f;
VkSurfaceFormatKHR swapchain_surface_format = { VK_FORMAT_UNDEFINED, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR };
PresentMode current_present_mode = PresentMode::SyncToVBlank;
PresentMode present_mode = PresentMode::SyncToVBlank;
bool low_latency_mode_enable_present = false;
bool low_latency_mode_enable_gpu_submit = false;
bool low_latency_anti_lag_present_valid = false;
void emit_marker_pre_present();
void emit_marker_post_present();
void emit_end_of_frame_markers();
VkPresentModeKHR active_present_mode = VK_PRESENT_MODE_FIFO_KHR;
std::vector<VkPresentModeKHR> present_mode_compat_group;
bool update_active_presentation_mode(PresentMode mode);
VkImageUsageFlags current_extra_usage = 0;
VkImageUsageFlags extra_usage = 0;
bool swapchain_is_suboptimal = false;
ImageCompression current_compression, compression;
enum class SwapchainError
{
None,
NoSurface,
Error
};
SwapchainError init_swapchain(unsigned width, unsigned height);
bool blocking_init_swapchain(unsigned width, unsigned height);
uint32_t swapchain_index = 0;
bool has_acquired_swapchain_index = false;
WSIPlatform *platform = nullptr;
std::vector<ImageHandle> external_swapchain_images;
unsigned external_frame_index = 0;
Semaphore external_acquire;
Semaphore external_release;
bool frame_is_external = false;
BackbufferFormat backbuffer_format = BackbufferFormat::sRGB;
BackbufferFormat current_backbuffer_format = BackbufferFormat::sRGB;
VkSurfaceFormatKHR current_custom_backbuffer_format = {};
VkSurfaceFormatKHR custom_backbuffer_format = {};
bool has_backbuffer_format_delta() const;
bool support_prerotate = false;
VkSurfaceTransformFlagBitsKHR swapchain_current_prerotate = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
bool begin_frame_external();
double external_frame_time = 0.0;
double smooth_frame_time = 0.0;
double smooth_elapsed_time = 0.0;
uint64_t next_present_id = 1;
uint64_t present_last_id = 0;
unsigned present_frame_latency = 0;
bool supports_present_wait2 = false;
struct
{
VkPresentStageFlagsEXT feedback;
bool absolute;
bool relative;
} supports_present_timing = {};
struct CalibratedTimestamp
{
uint64_t host_time;
uint64_t stage_times[4];
};
struct ErrorStats
{
uint64_t present_id;
uint64_t target_absolute;
int64_t compensation;
};
struct
{
// Used for setting target time.
VkPresentStageFlagsEXT present_stage;
uint64_t reference_time;
VkTimeDomainKHR time_domain;
uint64_t time_domain_id;
// Target
uint64_t target_absolute_time;
uint64_t target_relative_time;
uint64_t last_absolute_target_time;
bool force_vrr;
// Feedback.
uint64_t gpu_done_host_time;
uint64_t present_done_host_time;
uint64_t present_id;
// Display refresh rate information.
uint64_t refresh_duration;
uint64_t refresh_interval;
uint64_t refresh_counter;
bool has_refresh_feedback;
RefreshMode refresh_mode = RefreshMode::Unknown;
// Calibration information.
uint64_t time_domain_counter;
bool has_time_domain_props;
Util::SmallVector<VkTimeDomainKHR> time_domains;
Util::SmallVector<uint64_t> time_domain_ids;
Util::SmallVector<CalibratedTimestamp> calibration;
bool need_recalibration;
int64_t last_recalibration_time;
int64_t presentation_time_error;
int64_t pending_compensation;
Util::SmallVector<ErrorStats, 16> error_stats;
} present_timing = {};
bool present_feedback_enable = false;
void update_present_timing_properties();
void poll_present_timing_feedback();
void recalibrate_present_timing_domains();
void update_time_domain_properties();
void set_present_timing_request(VkPresentTimingInfoEXT &timing);
Semaphore low_latency_semaphore;
uint64_t low_latency_semaphore_value = 0;
bool next_present_is_dupe = false;
bool frame_dupe_aware = false;
bool current_frame_dupe_aware = false;
unsigned frame_dupe_target_images = 5;
unsigned current_frame_dupe_target_images = 5;
unsigned duplicated_frames = 0;
unsigned last_duplicated_frames = 0;
void tear_down_swapchain();
void drain_swapchain(bool in_tear_down);
void wait_swapchain_latency();
VkHdrMetadataEXT hdr_metadata = { VK_STRUCTURE_TYPE_HDR_METADATA_EXT };
bool valid_hdr_metadata = false;
struct DeferredDeletionSwapchain
{
VkSwapchainKHR swapchain;
Fence fence;
};
struct DeferredDeletionSemaphore
{
Semaphore semaphore;
Fence fence;
};
Util::SmallVector<DeferredDeletionSwapchain> deferred_swapchains;
Util::SmallVector<DeferredDeletionSemaphore> deferred_semaphore;
Vulkan::Fence last_present_fence;
void nonblock_delete_swapchain_resources();
VkSurfaceFormatKHR find_suitable_present_format(const std::vector<VkSurfaceFormatKHR> &formats, BackbufferFormat desired_format) const;
VkResult wait_for_present(uint64_t id, uint64_t timeout = UINT64_MAX);
#ifdef HAVE_WSI_DXGI_INTEROP
std::unique_ptr<DXGIInteropSwapchain> dxgi;
bool init_surface_swapchain_dxgi(unsigned width, unsigned height);
bool begin_frame_dxgi();
bool end_frame_dxgi();
#endif
};
}
@@ -0,0 +1,519 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "wsi_dxgi.hpp"
#include <windows.h>
namespace Vulkan
{
DXGIInteropSwapchain::~DXGIInteropSwapchain()
{
// Wait-for-idle before teardown.
if (fence)
fence->SetEventOnCompletion(fence_value, nullptr);
if (latency_handle)
CloseHandle(latency_handle);
}
static bool is_running_on_wine()
{
// If we're running in Wine for whatever reason, interop like this is completely useless.
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
return !ntdll || GetProcAddress(ntdll, "wine_get_version");
}
static bool is_running_in_tool(Device &device)
{
auto &ext = device.get_device_features();
if (ext.supports_tooling_info && vkGetPhysicalDeviceToolPropertiesEXT)
{
auto gpu = device.get_physical_device();
uint32_t count = 0;
vkGetPhysicalDeviceToolPropertiesEXT(gpu, &count, nullptr);
Util::SmallVector<VkPhysicalDeviceToolPropertiesEXT> tool_props(count);
for (auto &t : tool_props)
t = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TOOL_PROPERTIES_EXT };
vkGetPhysicalDeviceToolPropertiesEXT(gpu, &count, tool_props.data());
// It's okay for validation to not force this path. We're mostly concerned with RenderDoc, RGP and Nsight.
for (auto &t : tool_props)
if (t.purposes & (VK_TOOL_PURPOSE_PROFILING_BIT | VK_TOOL_PURPOSE_TRACING_BIT))
return true;
}
return false;
}
bool DXGIInteropSwapchain::init_interop_device(Device &vk_device_)
{
vk_device = &vk_device_;
// If we're running in Wine for whatever reason, interop like this is more harmful than good.
if (is_running_on_wine())
return false;
// If we're running in some capture tool, we need to use Vulkan WSI to avoid confusing it.
if (is_running_in_tool(*vk_device))
return false;
if (!vk_device->get_device_features().vk11_props.deviceLUIDValid)
return false;
d3d12_lib = Util::DynamicLibrary("d3d12.dll");
dxgi_lib = Util::DynamicLibrary("dxgi.dll");
if (!d3d12_lib)
{
LOGE("Failed to find d3d12.dll. Ignoring interop device.\n");
return false;
}
if (!dxgi_lib)
{
LOGE("Failed to find dxgi.dll. Ignoring interop device.\n");
return false;
}
auto pfn_CreateDXGIFactory1 =
dxgi_lib.get_symbol<decltype(&CreateDXGIFactory1)>("CreateDXGIFactory1");
auto pfn_D3D12CreateDevice =
d3d12_lib.get_symbol<decltype(&D3D12CreateDevice)>("D3D12CreateDevice");
if (!pfn_CreateDXGIFactory1 || !pfn_D3D12CreateDevice)
{
LOGE("Failed to find entry points.\n");
return false;
}
HRESULT hr;
if (FAILED(hr = pfn_CreateDXGIFactory1(IID_PPV_ARGS(&dxgi_factory))))
{
LOGE("Failed to create DXGI factory, hr #%x.\n", unsigned(hr));
return false;
}
LUID luid = {};
ComPtr<IDXGIAdapter> adapter;
memcpy(&luid, vk_device->get_device_features().vk11_props.deviceLUID, VK_LUID_SIZE);
if (FAILED(hr = dxgi_factory->EnumAdapterByLuid(luid, IID_PPV_ARGS(&adapter))))
{
LOGE("Failed to enumerate DXGI adapter by LUID.\n");
return false;
}
if (FAILED(hr = pfn_D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device))))
{
LOGE("Failed to create D3D12Device, hr #%x.\n", unsigned(hr));
return false;
}
D3D12_COMMAND_QUEUE_DESC queue_desc = {};
queue_desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
if (FAILED(hr = device->CreateCommandQueue(&queue_desc, IID_PPV_ARGS(&queue))))
{
LOGE("Failed to create command queue, hr #%x.\n", unsigned(hr));
return false;
}
if (FAILED(hr = device->CreateCommandList1(
0, D3D12_COMMAND_LIST_TYPE_DIRECT, D3D12_COMMAND_LIST_FLAG_NONE, IID_PPV_ARGS(&list))))
{
LOGE("Failed to create command list, hr #%x.\n", unsigned(hr));
return false;
}
if (FAILED(hr = device->CreateFence(0, D3D12_FENCE_FLAG_SHARED, IID_PPV_ARGS(&fence))))
{
LOGE("Failed to create shared fence, hr #%x.\n", unsigned(hr));
return false;
}
// Import D3D12 timeline into Vulkan.
// Other way around is not as well-supported.
vk_fence = vk_device->request_semaphore_external(
VK_SEMAPHORE_TYPE_TIMELINE, VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT);
if (!vk_fence)
{
LOGE("Failed to create timeline.\n");
return EXIT_FAILURE;
}
ExternalHandle fence_handle;
fence_handle.semaphore_handle_type = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT;
if (FAILED(device->CreateSharedHandle(fence.Get(), nullptr,
GENERIC_ALL, nullptr, &fence_handle.handle)))
{
LOGE("Failed to create shared fence handle.\n");
return EXIT_FAILURE;
}
if (!vk_fence->import_from_handle(fence_handle))
{
LOGE("Failed to import timeline.\n");
CloseHandle(fence_handle.handle);
return false;
}
return true;
}
VkImage DXGIInteropSwapchain::get_vulkan_image() const
{
return vulkan_backbuffer->get_image();
}
static DXGI_FORMAT convert_vk_format(VkFormat fmt)
{
switch (fmt)
{
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_R8G8B8A8_SRGB:
// D3D12 fails to create SRGB swapchain for some reason.
// We'll import the memory as sRGB however, and it works fine ...
return DXGI_FORMAT_R8G8B8A8_UNORM;
case VK_FORMAT_B8G8R8A8_UNORM:
case VK_FORMAT_B8G8R8A8_SRGB:
return DXGI_FORMAT_B8G8R8A8_UNORM;
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
return DXGI_FORMAT_R10G10B10A2_UNORM;
case VK_FORMAT_R16G16B16A16_SFLOAT:
return DXGI_FORMAT_R16G16B16A16_FLOAT;
default:
return DXGI_FORMAT_UNKNOWN;
}
}
static DXGI_COLOR_SPACE_TYPE convert_vk_color_space(VkColorSpaceKHR colspace)
{
switch (colspace)
{
case VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT:
return DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709;
case VK_COLOR_SPACE_HDR10_ST2084_EXT:
return DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020;
case VK_COLOR_SPACE_SRGB_NONLINEAR_KHR:
return DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
default:
return DXGI_COLOR_SPACE_RESERVED;
}
}
void DXGIInteropSwapchain::reset_backbuffer_state()
{
for (auto &buf : backbuffers)
if (fence)
fence->SetEventOnCompletion(buf.wait_fence_value, nullptr);
backbuffers.clear();
}
bool DXGIInteropSwapchain::setup_per_frame_state(PerFrameState &state, unsigned index)
{
HRESULT hr;
if (FAILED(hr = swapchain->GetBuffer(index, IID_PPV_ARGS(&state.backbuffer))))
{
LOGE("Failed to get backbuffer, hr #%x.\n", unsigned(hr));
return false;
}
if (FAILED(hr = device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(&state.allocator))))
{
LOGE("Failed to create command allocator, hr #%x.\n", unsigned(hr));
return false;
}
return true;
}
bool DXGIInteropSwapchain::init_swapchain(HWND hwnd_, VkSurfaceFormatKHR format,
unsigned width, unsigned height, unsigned count)
{
if (hwnd && hwnd_ != hwnd)
{
reset_backbuffer_state();
swapchain.Reset();
}
hwnd = hwnd_;
DXGI_SWAP_CHAIN_DESC1 desc = {};
desc.Width = width;
desc.Height = height;
desc.BufferCount = count;
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
desc.SampleDesc.Count = 1;
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.Format = convert_vk_format(format.format);
if (!desc.Format)
return false;
auto color_space = convert_vk_color_space(format.colorSpace);
if (color_space == DXGI_COLOR_SPACE_RESERVED)
return false;
BOOL allow_tear = FALSE;
if (SUCCEEDED(dxgi_factory->CheckFeatureSupport(
DXGI_FEATURE_PRESENT_ALLOW_TEARING,
&allow_tear, sizeof(allow_tear)) && allow_tear))
{
desc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING;
allow_tearing = true;
}
desc.Flags |= DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT;
ComPtr<IDXGISwapChain1> swap;
HRESULT hr;
reset_backbuffer_state();
// If we already have a swapchain we can just use ResizeBuffers.
if (!swapchain)
{
if (FAILED(hr = dxgi_factory->CreateSwapChainForHwnd(
queue.Get(), hwnd, &desc, nullptr, nullptr, &swap)))
{
LOGE("Failed to create swapchain, hr #%x.\n", unsigned(hr));
return false;
}
completed_presents = 0;
completed_waits = 0;
if (FAILED(swap.As(&swapchain)))
{
LOGE("Failed to query swapchain interface.\n");
return false;
}
if (latency_handle)
CloseHandle(latency_handle);
latency_handle = swapchain->GetFrameLatencyWaitableObject();
if (!latency_handle)
{
LOGE("Failed to query latency handle.\n");
return false;
}
// Drop semaphore to 0 right away to make code less awkward later.
if (WaitForSingleObject(latency_handle, INFINITE) != WAIT_OBJECT_0)
{
LOGE("Failed to wait for latency object.\n");
return false;
}
}
else
{
if (FAILED(hr = swapchain->ResizeBuffers(count, width, height, desc.Format, desc.Flags)))
{
LOGE("Failed to resize buffers, hr #%x.\n", unsigned(hr));
return false;
}
}
if (FAILED(dxgi_factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_WINDOW_CHANGES)))
{
LOGE("Failed to make window association.\n");
return false;
}
surface_format = format;
UINT space_support = 0;
if (FAILED(swapchain->CheckColorSpaceSupport(color_space, &space_support)) ||
((space_support & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT) == 0))
{
// Fallback to SDR if HDR doesn't pass check.
if (FAILED(swapchain->CheckColorSpaceSupport(DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709, &space_support)) ||
((space_support & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT) == 0))
{
return false;
}
LOGW("HDR10 not supported by DXGI swapchain, falling back to SDR.\n");
surface_format.colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
color_space = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
}
if (FAILED(swapchain->SetColorSpace1(color_space)))
{
LOGE("Failed to set color space.\n");
return false;
}
backbuffers.resize(desc.BufferCount);
for (unsigned i = 0; i < desc.BufferCount; i++)
if (!setup_per_frame_state(backbuffers[i], i))
return false;
ExternalHandle imported_image;
imported_image.memory_handle_type = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT;
D3D12_RESOURCE_DESC blit_desc = {};
blit_desc.Width = width;
blit_desc.Height = height;
blit_desc.Format = desc.Format;
blit_desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET;
blit_desc.SampleDesc.Count = 1;
blit_desc.DepthOrArraySize = 1;
blit_desc.MipLevels = 1;
blit_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
blit_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
D3D12_HEAP_PROPERTIES heap_props = {};
heap_props.Type = D3D12_HEAP_TYPE_DEFAULT;
blit_backbuffer.Reset();
if (FAILED(hr = device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_SHARED, &blit_desc,
D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&blit_backbuffer))))
{
LOGE("Failed to create blit render target, hr #%x.\n", unsigned(hr));
return false;
}
if (FAILED(hr = device->CreateSharedHandle(blit_backbuffer.Get(), nullptr, GENERIC_ALL, nullptr,
&imported_image.handle)))
{
LOGE("Failed to create shared handle, hr #%x.\n", unsigned(hr));
return false;
}
auto image_info = ImageCreateInfo::render_target(width, height, format.format);
image_info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
image_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
image_info.misc = IMAGE_MISC_EXTERNAL_MEMORY_BIT;
image_info.external = imported_image;
vulkan_backbuffer = vk_device->create_image(image_info);
if (!vulkan_backbuffer)
{
LOGE("Failed to create shared Vulkan image, hr #%x.\n", unsigned(hr));
return false;
}
vulkan_backbuffer->set_swapchain_layout(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
return true;
}
VkSurfaceFormatKHR DXGIInteropSwapchain::get_current_surface_format() const
{
return surface_format;
}
bool DXGIInteropSwapchain::wait_latency(unsigned latency_frames)
{
uint64_t target_wait_count = completed_presents - latency_frames;
if (latency_handle && (target_wait_count & (1ull << 63)) == 0)
{
while (completed_waits < target_wait_count)
{
if (WaitForSingleObject(latency_handle, INFINITE) != WAIT_OBJECT_0)
{
LOGE("Failed to wait for latency object.\n");
return false;
}
completed_waits++;
}
}
return true;
}
bool DXGIInteropSwapchain::acquire(Semaphore &acquire_semaphore)
{
// AMD workaround. Driver freaks out if trying to wait for D3D12 timeline value of 0.
queue->Signal(fence.Get(), ++fence_value);
acquire_semaphore = vk_device->request_timeline_semaphore_as_binary(*vk_fence, fence_value);
return true;
}
bool DXGIInteropSwapchain::present(Vulkan::Semaphore release_semaphore, bool vsync)
{
unsigned index = swapchain->GetCurrentBackBufferIndex();
auto &per_frame = backbuffers[index];
vk_device->add_wait_semaphore(CommandBuffer::Type::Generic, std::move(release_semaphore),
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, true);
auto cmd = vk_device->request_command_buffer();
cmd->release_image_barrier(*vulkan_backbuffer,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_GENERAL,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0);
vk_device->submit(cmd);
auto timeline_signal = vk_device->request_timeline_semaphore_as_binary(*vk_fence, ++fence_value);
vk_device->submit_empty(CommandBuffer::Type::Generic, nullptr, timeline_signal.get());
queue->Wait(fence.Get(), fence_value);
fence->SetEventOnCompletion(per_frame.wait_fence_value, nullptr);
if (FAILED(per_frame.allocator->Reset()))
{
LOGE("Failed to reset command allocator.\n");
return false;
}
list->Reset(per_frame.allocator.Get(), nullptr);
D3D12_RESOURCE_BARRIER barrier = {};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
barrier.Transition.pResource = per_frame.backbuffer.Get();
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_DEST;
list->ResourceBarrier(1, &barrier);
D3D12_TEXTURE_COPY_LOCATION dst = {}, src = {};
dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
src.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
dst.pResource = per_frame.backbuffer.Get();
src.pResource = blit_backbuffer.Get();
list->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
list->ResourceBarrier(1, &barrier);
if (FAILED(list->Close()))
{
LOGE("Failed to close command list.\n");
return false;
}
ID3D12CommandList *cmdlist = list.Get();
queue->ExecuteCommandLists(1, &cmdlist);
queue->Signal(fence.Get(), ++fence_value);
per_frame.wait_fence_value = fence_value;
HRESULT hr = swapchain->Present(vsync ? 1 : 0, !vsync && allow_tearing ? DXGI_PRESENT_ALLOW_TEARING : 0);
if (FAILED(hr))
{
LOGE("Failed to present, hr #%x.\n", unsigned(hr));
return false;
}
completed_presents++;
return true;
}
}
@@ -0,0 +1,84 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "device.hpp"
#include "image.hpp"
#include "d3d12.h"
#include "dxgi1_6.h"
#include "small_vector.hpp"
#include "dynamic_library.hpp"
#include <wrl.h>
namespace Vulkan
{
template <typename T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
class DXGIInteropSwapchain
{
public:
bool init_interop_device(Device &device);
~DXGIInteropSwapchain();
bool init_swapchain(HWND hwnd, VkSurfaceFormatKHR format, unsigned width, unsigned height, unsigned count);
VkImage get_vulkan_image() const;
VkSurfaceFormatKHR get_current_surface_format() const;
bool acquire(Semaphore &acquire_semaphore);
bool present(Semaphore release_semaphore, bool vsync);
bool wait_latency(unsigned latency_frames);
private:
Device *vk_device = nullptr;
Util::DynamicLibrary d3d12_lib, dxgi_lib;
HWND hwnd = nullptr;
HANDLE latency_handle = nullptr;
ComPtr<ID3D12Device4> device;
ComPtr<ID3D12CommandQueue> queue;
ComPtr<IDXGIFactory5> dxgi_factory;
ComPtr<IDXGISwapChain3> swapchain;
ComPtr<ID3D12GraphicsCommandList> list;
ComPtr<ID3D12Fence> fence;
Semaphore vk_fence;
uint64_t fence_value = 0;
VkSurfaceFormatKHR surface_format = {};
bool allow_tearing = false;
struct PerFrameState
{
ComPtr<ID3D12CommandAllocator> allocator;
ComPtr<ID3D12Resource> backbuffer;
uint64_t wait_fence_value = 0;
};
Util::SmallVector<PerFrameState> backbuffers;
ComPtr<ID3D12Resource> blit_backbuffer;
ImageHandle vulkan_backbuffer;
bool setup_per_frame_state(PerFrameState &state, unsigned index);
void reset_backbuffer_state();
uint64_t completed_presents = 0;
uint64_t completed_waits = 0;
};
}
@@ -0,0 +1,324 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "wsi_pacer.hpp"
#include "timer.hpp"
FixedRefreshRatePacer::FixedRefreshRatePacer()
{
reset();
}
FixedRefreshRatePacer::~FixedRefreshRatePacer()
{
#if 0
for (int i = 0; i < NumHistogramEntries; i++)
{
LOGI("Success rate: band [%.3f ms - %.3f ms] -> %.3f %% (last failure %llu) (overall %llu failures)\n", double(i * GapQuantumNS) * 1e-6,
double((i + 1) * GapQuantumNS) * 1e-6, histogram[i].confidence * 100.0,
static_cast<unsigned long long>(histogram[i].last_failure_present_id),
static_cast<unsigned long long>(histogram[i].num_relevant_present_failures));
}
#endif
}
void FixedRefreshRatePacer::reset()
{
feedbacks.clear();
last_feedback = {};
frame_time_ns = 0;
estimated_present_gap_ns = UINT64_MAX;
estimated_frame_latency_ns = UINT64_MAX;
for (auto &entry : histogram)
entry = {};
overall = {};
minimum_confidence_for_promotion = 0.0;
}
void FixedRefreshRatePacer::discard_pacing_statistics(uint64_t present_id)
{
find_and_remove_feedback(present_id);
}
FixedRefreshRatePacer::Feedback FixedRefreshRatePacer::find_and_remove_feedback(uint64_t present_id)
{
auto itr = std::find_if(feedbacks.begin(), feedbacks.end(), [=](const Feedback &f)
{
return f.present_id == present_id;
});
if (itr == feedbacks.end())
return {};
auto ret = *itr;
feedbacks.erase(itr);
return ret;
}
void FixedRefreshRatePacer::set_frame_time_ns(uint64_t frame_time)
{
frame_time_ns = frame_time;
// Dynamically adjust as we get completed frames coming in.
estimated_present_gap_ns = std::min<uint64_t>(frame_time_ns / 2, estimated_present_gap_ns);
// Stay in the center of the histogram range to avoid annoying rounding errors when quantizing results
// to histogram.
estimated_present_gap_ns = (estimated_present_gap_ns / GapQuantumNS) * GapQuantumNS + (GapQuantumNS / 2);
}
void FixedRefreshRatePacer::override_gpu_done_time(uint64_t present_id, uint64_t queue_done_ns)
{
auto itr =
std::find_if(feedbacks.begin(), feedbacks.end(), [=](const Feedback &f) { return f.present_id == present_id; });
if (itr != feedbacks.end())
itr->queue_done_ns = queue_done_ns;
}
void FixedRefreshRatePacer::update_feedback(uint64_t present_id, uint64_t queue_done_ns, uint64_t complete_ns)
{
auto feedback = find_and_remove_feedback(present_id);
if (feedback.queue_done_ns)
queue_done_ns = feedback.queue_done_ns;
last_feedback.present_id = present_id;
last_feedback.queue_done_ns = queue_done_ns;
last_feedback.complete_ns = complete_ns;
if (feedback.present_id != present_id)
return;
// Safety.
queue_done_ns = std::min<uint64_t>(queue_done_ns, complete_ns);
// The present happened when we expected it to. If it falls meaningfully outside of this, we have VRR
// and a FRR pacer isn't super useful to begin with.
if (complete_ns <= feedback.estimated_complete_ns + frame_time_ns / 8)
{
// With an ideal setup queue_done_ns can be arbitrarily close to complete_ns before we start dropping frames,
// but that's not realistic.
uint64_t present_gap_ns = complete_ns - queue_done_ns;
if (estimated_present_gap_ns == UINT64_MAX)
{
// Initialize the estimate.
estimated_present_gap_ns = frame_time_ns / 2;
}
register_present_gap(present_id, present_gap_ns, true);
if (present_gap_ns <= estimated_present_gap_ns)
{
// The compositor was able to deal with current tight timings, update our estimate accordingly (slowly).
uint64_t candidate_ns = std::max<uint64_t>(estimated_present_gap_ns, GapQuantumNS) - GapQuantumNS;
// Only lower if history shows this to be a sound decision.
if (safe_to_lower_gap_to(present_id, present_gap_ns, candidate_ns))
estimated_present_gap_ns = candidate_ns;
}
}
else if (complete_ns > feedback.estimated_complete_ns + frame_time_ns / 2 &&
queue_done_ns < feedback.estimated_complete_ns)
{
// The GPU was done in time, but we dropped a frame regardless.
// Likely the compositor has a queueing delay we need to consider.
estimated_present_gap_ns += GapQuantumNS;
uint64_t potential_present_gap_ns = feedback.estimated_complete_ns - queue_done_ns;
// If the present gap is over half a frame, it's likely the compositor just derping out for no good reason.
if (potential_present_gap_ns < frame_time_ns / 2)
{
// We have a potential present gap that failed to flip properly.
register_present_gap(present_id, potential_present_gap_ns, false);
}
}
// Otherwise the GPU was simply too slow to render.
// The frame latency will have to increase as expected.
uint64_t frame_latency_ns = std::max<uint64_t>(feedback.frame_submission_ns, queue_done_ns) -
feedback.frame_submission_ns;
// Keep a running estimate of how long time it takes to process CPU -> GPU.
// This will inform sleep targets.
if (estimated_frame_latency_ns != UINT64_MAX)
estimated_frame_latency_ns = (estimated_frame_latency_ns * 15 + frame_latency_ns) / 16;
else
estimated_frame_latency_ns = frame_latency_ns;
}
void FixedRefreshRatePacer::register_present_gap(uint64_t present_id, uint64_t gap_ns, bool success)
{
if (!success)
{
overall.confidence *= 0.5;
overall.last_failure_present_id = present_id;
overall.num_relevant_present_failures++;
}
else
{
// This estimate should move very slowly.
overall.confidence = overall.confidence * 0.99 + 0.01;
if (overall.confidence > 0.9999999)
{
if (overall.num_relevant_present_failures)
{
// If we're resetting, put a higher bar of confidence if we want to promote so
// that we don't instantly start to see failures.
minimum_confidence_for_promotion = 0.7499 + minimum_confidence_for_promotion * 0.25;
}
overall.num_relevant_present_failures = 0;
}
}
int index = int(gap_ns / GapQuantumNS);
if (index >= NumHistogramEntries)
return;
auto &entry = histogram[index];
if (!success)
{
// Avoid denorm hell, but also reset the counter if we cannot get reliable successes multiple times in a row.
if (entry.confidence < 0.25)
entry.confidence = 0.0;
entry.confidence = entry.confidence * 0.5;
entry.last_failure_present_id = present_id;
entry.num_relevant_present_failures++;
}
else
{
// This gap seems super stable now.
entry.confidence = entry.confidence * 0.95 + 0.05;
if (entry.confidence > 0.999999)
entry.num_relevant_present_failures = 0;
}
}
FixedRefreshRatePacer::HistogramStats FixedRefreshRatePacer::get_current_histogram_stats() const
{
if (estimated_present_gap_ns == UINT64_MAX)
return {};
int index = int(estimated_present_gap_ns / GapQuantumNS);
if (index >= NumHistogramEntries)
return {};
return histogram[index];
}
double FixedRefreshRatePacer::get_minimum_confidence_for_promotion() const
{
return minimum_confidence_for_promotion;
}
FixedRefreshRatePacer::HistogramStats FixedRefreshRatePacer::get_candidate_histogram_stats() const
{
if (estimated_present_gap_ns == UINT64_MAX)
return {};
int index = int(estimated_present_gap_ns / GapQuantumNS) - 1;
if (index >= NumHistogramEntries || index < 0)
return {};
return histogram[index];
}
FixedRefreshRatePacer::HistogramStats FixedRefreshRatePacer::get_overall_histogram_stats() const
{
return overall;
}
uint64_t FixedRefreshRatePacer::get_estimated_present_gap_ns() const
{
return estimated_present_gap_ns;
}
uint64_t FixedRefreshRatePacer::get_estimated_cpu_gpu_idle_latency_ns() const
{
return estimated_frame_latency_ns;
}
bool FixedRefreshRatePacer::safe_to_lower_gap_to(uint64_t present_id, uint64_t observed_gap_ns, uint64_t to_ns) const
{
// If we can prove some safety of the target gap, go for it.
int to_index = int(to_ns / GapQuantumNS);
int from_index = int(observed_gap_ns / GapQuantumNS);
if (to_index >= NumHistogramEntries || from_index >= NumHistogramEntries)
return true;
// Don't lower until overall success probability is solid.
uint64_t frames_since_last_observed_failure = present_id - overall.last_failure_present_id;
auto num_failures = std::min<unsigned>(16, overall.num_relevant_present_failures);
if (frames_since_last_observed_failure < (1u << num_failures))
return false;
// We're confident the update will work well.
if (histogram[to_index].confidence >= 0.999)
return true;
// The bar to clear is higher now. If we never saw any failures, go ahead, we need to learn.
if (histogram[to_index].confidence < minimum_confidence_for_promotion &&
histogram[to_index].num_relevant_present_failures != 0)
return false;
// We haven't proved this rate is solid.
if (histogram[from_index].confidence < 0.999)
return false;
// We've never observed a failure at this gap before, should be good to test until we observe failures.
if (histogram[to_index].last_failure_present_id == 0)
return true;
// Check statistics per histogram.
frames_since_last_observed_failure = present_id - histogram[to_index].last_failure_present_id;
// Backoff algorithm which scales the number of safety frames based on failures observed overall.
// Eventually, we have enough failures that we never trust it blindly.
num_failures = std::min<unsigned>(16, histogram[to_index].num_relevant_present_failures);
return frames_since_last_observed_failure >= (60u << num_failures);
}
void FixedRefreshRatePacer::begin_frame_submission(uint64_t current_present_id)
{
if (last_feedback.present_id == 0 || frame_time_ns == 0)
return;
uint64_t estimated_complete_ns =
(current_present_id - last_feedback.present_id) * frame_time_ns +
last_feedback.complete_ns;
if (estimated_present_gap_ns != UINT64_MAX)
{
uint64_t target_gap_ns = estimated_present_gap_ns;
target_gap_ns += estimated_frame_latency_ns;
int64_t sleep_target_ns = int64_t(estimated_complete_ns) - int64_t(target_gap_ns);
Util::sleep_until_nsecs(sleep_target_ns);
}
// Safety clear in case it's never polled.
if (feedbacks.size() > 16)
feedbacks.clear();
feedbacks.push_back({ current_present_id, uint64_t(Util::get_current_time_nsecs()), estimated_complete_ns });
}
@@ -0,0 +1,92 @@
/* Copyright (c) 2017-2026 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "small_vector.hpp"
#include <stdint.h>
// Designed for lowest possible latency on fixed rate displays.
// The GPU workload is expected to be very light and stable (e.g. video decoding).
class FixedRefreshRatePacer
{
public:
FixedRefreshRatePacer();
~FixedRefreshRatePacer();
void set_frame_time_ns(uint64_t frame_time);
void update_feedback(uint64_t present_id, uint64_t queue_done_ns, uint64_t complete_ns);
// Sleeps as needed. Should be called before sampling inputs, etc.
void begin_frame_submission(uint64_t current_present_id);
// This frame will not be valid for purposes of statistics.
void discard_pacing_statistics(uint64_t present_id);
// Can avoid some weird interactions with async compute and such.
void override_gpu_done_time(uint64_t present_id, uint64_t queue_done_ns);
void reset();
struct HistogramStats
{
double confidence = 0.0;
uint64_t last_failure_present_id = 0;
uint64_t num_relevant_present_failures = 0;
};
// Debugging.
HistogramStats get_current_histogram_stats() const;
HistogramStats get_candidate_histogram_stats() const;
HistogramStats get_overall_histogram_stats() const;
uint64_t get_estimated_present_gap_ns() const;
uint64_t get_estimated_cpu_gpu_idle_latency_ns() const;
double get_minimum_confidence_for_promotion() const;
private:
uint64_t frame_time_ns = 0;
uint64_t estimated_present_gap_ns = UINT64_MAX;
uint64_t estimated_frame_latency_ns = UINT64_MAX;
double minimum_confidence_for_promotion = 0.0;
struct Feedback
{
uint64_t present_id;
uint64_t frame_submission_ns;
uint64_t estimated_complete_ns;
uint64_t queue_done_ns;
uint64_t complete_ns;
};
Util::SmallVector<Feedback> feedbacks;
Feedback last_feedback = {};
Feedback find_and_remove_feedback(uint64_t present_id);
// Don't make the quant interval too narrow, or we won't be able capture statistics well.
enum { GapQuantumNS = 250 * 1000, NumHistogramEntries = 64 };
HistogramStats histogram[NumHistogramEntries];
HistogramStats overall;
void register_present_gap(uint64_t present_id, uint64_t gap_ns, bool success);
bool safe_to_lower_gap_to(uint64_t present_id, uint64_t observed_gap_ns, uint64_t to_ns) const;
};