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:
+922
@@ -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_)
|
||||
{}
|
||||
}
|
||||
}
|
||||
+206
@@ -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();
|
||||
};
|
||||
}
|
||||
+1039
File diff suppressed because it is too large
Load Diff
+277
@@ -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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user