Automated with clang-tidy readability-identifier-naming (config added to .clang-tidy) plus scripted passes, per the updated rules now documented in CONTRIBUTING.md: - types (class/struct/enum/alias/template params): PascalCase - functions, variables, members: snake_case (incl. rational -> Rational) - private/protected members: trailing underscore; static member variables likewise (instance_, available_themes_) - constants and enum values: snake_case (kLinear -> k_linear, F32P -> f32p); ALL_CAPS reserved for macros - macros: OAK_ prefix (OLIVE_ADD_TEST/OLIVE_ASSERT/OLIVE_CONFIG -> OAK_ADD_TEST/OAK_ASSERT/OAK_CONFIG, GL_PREAMBLE -> OAK_GL_PREAMBLE, include guards -> OAK_*) - file names: all lowercase (Current/Plugin/OliveHost/OliveClip/ OlivePluginInstance -> current/plugin/olivehost/oliveclip/ oliveplugininstance) - getters share the member name sans underscore, setters set_foo() - Qt and third-party (OpenFX) virtual overrides and framework callbacks keep their original names (exempt in .clang-tidy) Manual follow-ups required where automation could not reach: - string-based QMetaObject/SIGNAL/SLOT references updated to renamed methods (AddTask, CreatedFile, DeleteSpecificFile, moveSelectionUp, ...) - macro bodies referencing renamed methods (OLIVE_CONFIG, NODE_DEFAULT_DESTRUCTOR, MANAGEDDISPLAYWIDGET_*) - self-shadowing locals renamed where signals/methods became same-named (size_changed, worker_count, selected_items, import param, filters) - third_party OFX member/namespace usages restored (OFX::Host::*, _created, _clipPrefsDirty, createInstance, clearPersistentMessage) - STL protocol aliases restored (const_iterator) with .clang-tidy ignore rules; qHash overloads restored Full build and test suite pass: ctest 4/4, ~1960 gtest cases green.
3007 lines
99 KiB
C++
3007 lines
99 KiB
C++
#include "vulkanrenderer.h"
|
|
|
|
#include <QDebug>
|
|
#include <QFile>
|
|
#include <QRegularExpression>
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
|
|
#include "node/value.h"
|
|
#include "render/job/shaderjob.h"
|
|
|
|
#ifdef OAK_HAS_SHADERC
|
|
#include <shaderc/shaderc.h>
|
|
#endif
|
|
|
|
namespace olive
|
|
{
|
|
|
|
struct VulkanRenderer::VulkanTexture {
|
|
quint64 id = 0;
|
|
VkImage image = VK_NULL_HANDLE;
|
|
VkImageView view = VK_NULL_HANDLE;
|
|
VkDeviceMemory memory = VK_NULL_HANDLE;
|
|
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
|
int width = 0;
|
|
int height = 0;
|
|
int depth = 0;
|
|
PixelFormat format = PixelFormat::invalid;
|
|
int channel_count = 0;
|
|
VkFormat vk_format = VK_FORMAT_UNDEFINED;
|
|
VkImageLayout current_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
};
|
|
|
|
struct VulkanRenderer::UniformInfo {
|
|
QString name;
|
|
QString type;
|
|
VkDeviceSize offset;
|
|
VkDeviceSize size;
|
|
};
|
|
|
|
struct VulkanRenderer::VulkanShader {
|
|
quint64 id = 0;
|
|
VkShaderModule vert_module = VK_NULL_HANDLE;
|
|
VkShaderModule frag_module = VK_NULL_HANDLE;
|
|
VkPipelineLayout pipeline_layout = VK_NULL_HANDLE;
|
|
VkDescriptorSetLayout descriptor_layout = VK_NULL_HANDLE;
|
|
QHash<VkFormat, VkPipeline> pipeline_cache;
|
|
bool pipeline_created = false;
|
|
QVector<UniformInfo> uniforms;
|
|
VkDeviceSize ubo_size = 0;
|
|
int sampler_count = 0;
|
|
// Maps sampler uniform names to the descriptor binding assigned in
|
|
// RewriteShaderWithUbo. Used when updating descriptor sets so textures are
|
|
// bound to the sampler they belong to regardless of job iteration order.
|
|
QHash<QString, int> sampler_bindings;
|
|
};
|
|
|
|
struct VulkanRenderer::StagingBuffer {
|
|
VkBuffer buffer = VK_NULL_HANDLE;
|
|
VkDeviceMemory memory = VK_NULL_HANDLE;
|
|
VkDeviceSize size = 0;
|
|
};
|
|
|
|
static const float k_blit_vertices[] = {
|
|
-1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
|
|
1.0f, 1.0f, 0.0f, 1.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
|
|
-1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
|
|
};
|
|
|
|
// Constructs the renderer; Vulkan resources are created lazily so unavailable
|
|
// Vulkan systems can still instantiate the object and report fallback state.
|
|
VulkanRenderer::VulkanRenderer(QObject *parent)
|
|
: Renderer(parent)
|
|
{
|
|
}
|
|
|
|
// Ensures Vulkan resources are destroyed before the QObject hierarchy goes away.
|
|
VulkanRenderer::~VulkanRenderer()
|
|
{
|
|
destroy();
|
|
post_destroy();
|
|
}
|
|
|
|
// Initializes the Vulkan instance/device path once. Repeated calls are accepted
|
|
// because backend availability probes may call Init() before normal rendering.
|
|
bool VulkanRenderer::init()
|
|
{
|
|
if (instance_ != VK_NULL_HANDLE) {
|
|
return true;
|
|
}
|
|
return create_instance() && create_device() && create_command_pool() &&
|
|
create_descriptor_pool();
|
|
}
|
|
|
|
// Creates reusable draw resources after Init() has a valid logical device.
|
|
void VulkanRenderer::post_init()
|
|
{
|
|
if (vertex_buffer_ != VK_NULL_HANDLE) {
|
|
return;
|
|
}
|
|
create_vertex_buffer();
|
|
create_linear_sampler();
|
|
create_nearest_sampler();
|
|
}
|
|
|
|
// Reserved for Renderer API symmetry; Vulkan teardown is centralized in
|
|
// DestroyInternal() so object destruction and explicit Destroy() share a path.
|
|
void VulkanRenderer::post_destroy()
|
|
{
|
|
}
|
|
|
|
// Destroys all Vulkan objects in dependency order. The device is idled first so
|
|
// cached textures, pipelines, descriptor pools, and command pools are no longer
|
|
// referenced by in-flight work.
|
|
void VulkanRenderer::destroy_internal()
|
|
{
|
|
if (device_ != VK_NULL_HANDLE) {
|
|
vkDeviceWaitIdle(device_);
|
|
}
|
|
|
|
{
|
|
QMutexLocker lock(&mutex_);
|
|
for (auto it = textures_.begin(); it != textures_.end(); ++it) {
|
|
VulkanTexture *tex = it.value();
|
|
if (tex->view != VK_NULL_HANDLE) {
|
|
vkDestroyImageView(device_, tex->view, nullptr);
|
|
}
|
|
if (tex->image != VK_NULL_HANDLE) {
|
|
vkDestroyImage(device_, tex->image, nullptr);
|
|
}
|
|
if (tex->memory != VK_NULL_HANDLE) {
|
|
vkFreeMemory(device_, tex->memory, nullptr);
|
|
}
|
|
delete tex;
|
|
}
|
|
textures_.clear();
|
|
|
|
for (auto it = shaders_.begin(); it != shaders_.end(); ++it) {
|
|
VulkanShader *sh = it.value();
|
|
for (auto pit = sh->pipeline_cache.begin();
|
|
pit != sh->pipeline_cache.end(); ++pit) {
|
|
if (pit.value() != VK_NULL_HANDLE) {
|
|
vkDestroyPipeline(device_, pit.value(), nullptr);
|
|
}
|
|
}
|
|
sh->pipeline_cache.clear();
|
|
if (sh->pipeline_layout != VK_NULL_HANDLE) {
|
|
vkDestroyPipelineLayout(device_, sh->pipeline_layout, nullptr);
|
|
}
|
|
if (sh->descriptor_layout != VK_NULL_HANDLE) {
|
|
vkDestroyDescriptorSetLayout(device_, sh->descriptor_layout,
|
|
nullptr);
|
|
}
|
|
if (sh->vert_module != VK_NULL_HANDLE) {
|
|
vkDestroyShaderModule(device_, sh->vert_module, nullptr);
|
|
}
|
|
if (sh->frag_module != VK_NULL_HANDLE) {
|
|
vkDestroyShaderModule(device_, sh->frag_module, nullptr);
|
|
}
|
|
delete sh;
|
|
}
|
|
shaders_.clear();
|
|
}
|
|
|
|
if (linear_sampler_ != VK_NULL_HANDLE) {
|
|
vkDestroySampler(device_, linear_sampler_, nullptr);
|
|
linear_sampler_ = VK_NULL_HANDLE;
|
|
}
|
|
if (nearest_sampler_ != VK_NULL_HANDLE) {
|
|
vkDestroySampler(device_, nearest_sampler_, nullptr);
|
|
nearest_sampler_ = VK_NULL_HANDLE;
|
|
}
|
|
|
|
if (vertex_buffer_ != VK_NULL_HANDLE) {
|
|
vkDestroyBuffer(device_, vertex_buffer_, nullptr);
|
|
vkFreeMemory(device_, vertex_buffer_memory_, nullptr);
|
|
vertex_buffer_ = VK_NULL_HANDLE;
|
|
vertex_buffer_memory_ = VK_NULL_HANDLE;
|
|
}
|
|
|
|
if (staging_buffer_) {
|
|
if (staging_buffer_->buffer != VK_NULL_HANDLE) {
|
|
vkDestroyBuffer(device_, staging_buffer_->buffer, nullptr);
|
|
}
|
|
if (staging_buffer_->memory != VK_NULL_HANDLE) {
|
|
vkFreeMemory(device_, staging_buffer_->memory, nullptr);
|
|
}
|
|
delete staging_buffer_;
|
|
staging_buffer_ = nullptr;
|
|
}
|
|
|
|
if (reusable_fence_ != VK_NULL_HANDLE) {
|
|
vkDestroyFence(device_, reusable_fence_, nullptr);
|
|
reusable_fence_ = VK_NULL_HANDLE;
|
|
}
|
|
if (reusable_command_buffer_ != VK_NULL_HANDLE) {
|
|
vkFreeCommandBuffers(device_, command_pool_, 1,
|
|
&reusable_command_buffer_);
|
|
reusable_command_buffer_ = VK_NULL_HANDLE;
|
|
}
|
|
|
|
for (auto it = render_pass_cache_.begin(); it != render_pass_cache_.end();
|
|
++it) {
|
|
if (it.value() != VK_NULL_HANDLE) {
|
|
vkDestroyRenderPass(device_, it.value(), nullptr);
|
|
}
|
|
}
|
|
render_pass_cache_.clear();
|
|
|
|
if (descriptor_pool_ != VK_NULL_HANDLE) {
|
|
vkDestroyDescriptorPool(device_, descriptor_pool_, nullptr);
|
|
descriptor_pool_ = VK_NULL_HANDLE;
|
|
}
|
|
descriptor_sets_since_reset_ = 0;
|
|
|
|
if (command_pool_ != VK_NULL_HANDLE) {
|
|
vkDestroyCommandPool(device_, command_pool_, nullptr);
|
|
command_pool_ = VK_NULL_HANDLE;
|
|
}
|
|
|
|
if (device_ != VK_NULL_HANDLE) {
|
|
vkDestroyDevice(device_, nullptr);
|
|
device_ = VK_NULL_HANDLE;
|
|
}
|
|
device_lost_ = false;
|
|
|
|
if (instance_ != VK_NULL_HANDLE) {
|
|
destroy_debug_messenger();
|
|
vkDestroyInstance(instance_, nullptr);
|
|
instance_ = VK_NULL_HANDLE;
|
|
}
|
|
}
|
|
|
|
// Creates the minimal Vulkan instance needed for offscreen rendering.
|
|
bool VulkanRenderer::create_instance()
|
|
{
|
|
VkApplicationInfo app_info = {};
|
|
app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
|
|
app_info.pApplicationName = "Oak Video Editor";
|
|
app_info.applicationVersion = VK_MAKE_VERSION(0, 3, 0);
|
|
app_info.pEngineName = "Oak";
|
|
app_info.engineVersion = VK_MAKE_VERSION(0, 3, 0);
|
|
app_info.apiVersion = VK_API_VERSION_1_2;
|
|
|
|
const bool enable_validation =
|
|
qEnvironmentVariableIsSet("OAK_VULKAN_VALIDATION");
|
|
const char *validation_layer = "VK_LAYER_KHRONOS_validation";
|
|
const char *debug_extension = VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
|
|
bool has_validation = false;
|
|
bool has_debug_extension = false;
|
|
|
|
if (enable_validation) {
|
|
uint32_t layer_count = 0;
|
|
vkEnumerateInstanceLayerProperties(&layer_count, nullptr);
|
|
QVector<VkLayerProperties> layers(layer_count);
|
|
vkEnumerateInstanceLayerProperties(&layer_count, layers.data());
|
|
for (const VkLayerProperties &layer : layers) {
|
|
if (strcmp(layer.layerName, validation_layer) == 0) {
|
|
has_validation = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
uint32_t extension_count = 0;
|
|
vkEnumerateInstanceExtensionProperties(nullptr, &extension_count,
|
|
nullptr);
|
|
QVector<VkExtensionProperties> extensions(extension_count);
|
|
vkEnumerateInstanceExtensionProperties(nullptr, &extension_count,
|
|
extensions.data());
|
|
for (const VkExtensionProperties &ext : extensions) {
|
|
if (strcmp(ext.extensionName, debug_extension) == 0) {
|
|
has_debug_extension = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
VkInstanceCreateInfo create_info = {};
|
|
create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
|
|
create_info.pApplicationInfo = &app_info;
|
|
if (has_validation) {
|
|
create_info.enabledLayerCount = 1;
|
|
create_info.ppEnabledLayerNames = &validation_layer;
|
|
}
|
|
if (has_debug_extension) {
|
|
create_info.enabledExtensionCount = 1;
|
|
create_info.ppEnabledExtensionNames = &debug_extension;
|
|
}
|
|
|
|
VkResult result = vkCreateInstance(&create_info, nullptr, &instance_);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to create Vulkan instance:" << result;
|
|
return false;
|
|
}
|
|
|
|
if (has_validation && has_debug_extension) {
|
|
create_debug_messenger();
|
|
}
|
|
|
|
qDebug() << "Vulkan instance created successfully";
|
|
return true;
|
|
}
|
|
|
|
// Logs validation errors/warnings from the Vulkan validation layers. These are
|
|
// the first signal of missing barriers or invalid usage that would otherwise
|
|
// become a GPU hang.
|
|
VKAPI_ATTR VkBool32 VKAPI_CALL VulkanRenderer::debug_callback(
|
|
VkDebugUtilsMessageSeverityFlagBitsEXT message_severity,
|
|
VkDebugUtilsMessageTypeFlagsEXT message_type,
|
|
const VkDebugUtilsMessengerCallbackDataEXT *p_callback_data, void *p_user_data)
|
|
{
|
|
Q_UNUSED(message_type)
|
|
Q_UNUSED(p_user_data)
|
|
|
|
if (!p_callback_data || !p_callback_data->pMessage) {
|
|
return VK_FALSE;
|
|
}
|
|
|
|
// Only emit errors/warnings. Verbose validation messages are useful during
|
|
// bring-up but flood the log and degrade playback performance.
|
|
if (message_severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) {
|
|
qWarning() << "Vulkan validation error:" << p_callback_data->pMessage;
|
|
} else if (message_severity &
|
|
VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
|
|
qWarning() << "Vulkan validation warning:" << p_callback_data->pMessage;
|
|
}
|
|
|
|
return VK_FALSE;
|
|
}
|
|
|
|
bool VulkanRenderer::create_debug_messenger()
|
|
{
|
|
auto create_fn = reinterpret_cast<PFN_vkCreateDebugUtilsMessengerEXT>(
|
|
vkGetInstanceProcAddr(instance_, "vkCreateDebugUtilsMessengerEXT"));
|
|
if (!create_fn) {
|
|
qWarning() << "Failed to load vkCreateDebugUtilsMessengerEXT";
|
|
return false;
|
|
}
|
|
|
|
VkDebugUtilsMessengerCreateInfoEXT create_info = {};
|
|
create_info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
|
|
create_info.messageSeverity =
|
|
VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
|
|
VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
|
|
create_info.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
|
|
VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT;
|
|
create_info.pfnUserCallback = debug_callback;
|
|
|
|
VkResult result =
|
|
create_fn(instance_, &create_info, nullptr, &debug_messenger_);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to create Vulkan debug messenger:" << result;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void VulkanRenderer::destroy_debug_messenger()
|
|
{
|
|
if (debug_messenger_ == VK_NULL_HANDLE || instance_ == VK_NULL_HANDLE) {
|
|
return;
|
|
}
|
|
auto destroy_fn = reinterpret_cast<PFN_vkDestroyDebugUtilsMessengerEXT>(
|
|
vkGetInstanceProcAddr(instance_, "vkDestroyDebugUtilsMessengerEXT"));
|
|
if (destroy_fn) {
|
|
destroy_fn(instance_, debug_messenger_, nullptr);
|
|
}
|
|
debug_messenger_ = VK_NULL_HANDLE;
|
|
}
|
|
|
|
// Selects the first physical device with a graphics queue and creates a logical
|
|
// device without swapchain extensions because viewer output is CPU readback.
|
|
bool VulkanRenderer::create_device()
|
|
{
|
|
VkResult result =
|
|
vkEnumeratePhysicalDevices(instance_, &physical_device_count_, nullptr);
|
|
if (result != VK_SUCCESS || physical_device_count_ == 0) {
|
|
qWarning() << "No Vulkan-capable physical devices found";
|
|
return false;
|
|
}
|
|
|
|
QVector<VkPhysicalDevice> devices(physical_device_count_);
|
|
result = vkEnumeratePhysicalDevices(instance_, &physical_device_count_,
|
|
devices.data());
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to enumerate Vulkan physical devices:" << result;
|
|
return false;
|
|
}
|
|
|
|
// Pick the first device that has a graphics queue family. In the future we
|
|
// should score devices (discrete > integrated > CPU) and check feature support.
|
|
for (VkPhysicalDevice device : devices) {
|
|
vkGetPhysicalDeviceProperties(device, &device_properties_);
|
|
vkGetPhysicalDeviceMemoryProperties(device, &mem_properties_);
|
|
|
|
uint32_t queue_family_count = 0;
|
|
vkGetPhysicalDeviceQueueFamilyProperties(device, &queue_family_count,
|
|
nullptr);
|
|
QVector<VkQueueFamilyProperties> queue_families(queue_family_count);
|
|
vkGetPhysicalDeviceQueueFamilyProperties(device, &queue_family_count,
|
|
queue_families.data());
|
|
|
|
for (uint32_t i = 0; i < queue_family_count; i++) {
|
|
if (queue_families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
|
|
physical_device_ = device;
|
|
graphics_queue_family_ = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (physical_device_ != VK_NULL_HANDLE) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (physical_device_ == VK_NULL_HANDLE ||
|
|
graphics_queue_family_ == UINT32_MAX) {
|
|
qWarning() << "No Vulkan physical device with a graphics queue found";
|
|
return false;
|
|
}
|
|
|
|
float queue_priority = 1.0f;
|
|
VkDeviceQueueCreateInfo queue_create_info = {};
|
|
queue_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
|
|
queue_create_info.queueFamilyIndex = graphics_queue_family_;
|
|
queue_create_info.queueCount = 1;
|
|
queue_create_info.pQueuePriorities = &queue_priority;
|
|
|
|
VkPhysicalDeviceFeatures device_features = {};
|
|
|
|
// No device extensions are required for offscreen rendering. Requesting
|
|
// VK_KHR_swapchain caused failures on headless/CI setups and is unused.
|
|
VkDeviceCreateInfo device_create_info = {};
|
|
device_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
|
device_create_info.pQueueCreateInfos = &queue_create_info;
|
|
device_create_info.queueCreateInfoCount = 1;
|
|
device_create_info.pEnabledFeatures = &device_features;
|
|
device_create_info.enabledExtensionCount = 0;
|
|
device_create_info.ppEnabledExtensionNames = nullptr;
|
|
|
|
result = vkCreateDevice(physical_device_, &device_create_info, nullptr,
|
|
&device_);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to create Vulkan logical device:" << result;
|
|
return false;
|
|
}
|
|
|
|
qDebug() << "Vulkan device created successfully on"
|
|
<< QString::fromUtf8(device_properties_.deviceName);
|
|
vkGetDeviceQueue(device_, graphics_queue_family_, 0, &graphics_queue_);
|
|
return true;
|
|
}
|
|
|
|
// Creates the command pool used for short-lived transfer and draw command
|
|
// buffers.
|
|
bool VulkanRenderer::create_command_pool()
|
|
{
|
|
VkCommandPoolCreateInfo pool_info = {};
|
|
pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
|
|
pool_info.queueFamilyIndex = graphics_queue_family_;
|
|
pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
|
|
|
VkResult result =
|
|
vkCreateCommandPool(device_, &pool_info, nullptr, &command_pool_);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to create Vulkan command pool:" << result;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Creates a pool large enough for transient per-blit descriptor sets. Descriptor
|
|
// sets are freed after each pass, so this is capacity rather than lifetime count.
|
|
bool VulkanRenderer::create_descriptor_pool()
|
|
{
|
|
VkDescriptorPoolSize pool_sizes[2] = {};
|
|
pool_sizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
pool_sizes[0].descriptorCount = k_max_descriptor_sets;
|
|
pool_sizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
pool_sizes[1].descriptorCount = k_max_descriptor_sets * 8;
|
|
|
|
VkDescriptorPoolCreateInfo pool_info = {};
|
|
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
|
pool_info.poolSizeCount = 2;
|
|
pool_info.pPoolSizes = pool_sizes;
|
|
pool_info.maxSets = k_max_descriptor_sets;
|
|
pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
|
|
|
VkResult result =
|
|
vkCreateDescriptorPool(device_, &pool_info, nullptr, &descriptor_pool_);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to create Vulkan descriptor pool:" << result;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Returns a cached render pass keyed by color format and load operation.
|
|
VkRenderPass VulkanRenderer::get_or_create_render_pass(VkFormat format, bool clear)
|
|
{
|
|
const quint64 key = (static_cast<quint64>(format) << 1) |
|
|
(clear ? 1ULL : 0ULL);
|
|
auto it = render_pass_cache_.find(key);
|
|
if (it != render_pass_cache_.end()) {
|
|
return it.value();
|
|
}
|
|
|
|
VkAttachmentDescription color_attachment = {};
|
|
color_attachment.format = format;
|
|
color_attachment.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
color_attachment.loadOp = clear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
|
|
VK_ATTACHMENT_LOAD_OP_LOAD;
|
|
color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
color_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
color_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
color_attachment.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
color_attachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
|
|
VkAttachmentReference color_ref = {};
|
|
color_ref.attachment = 0;
|
|
color_ref.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
|
|
VkSubpassDescription subpass = {};
|
|
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
|
subpass.colorAttachmentCount = 1;
|
|
subpass.pColorAttachments = &color_ref;
|
|
|
|
// Explicit dependencies so layout transitions and read-after-write are
|
|
// correctly synchronized. The destination image is brought in by the
|
|
// pipeline barrier before the render pass; here we synchronize the render
|
|
// pass output with whatever stage reads it next.
|
|
//
|
|
// Two dependencies are required:
|
|
// 1) EXTERNAL -> 0: whatever produced the image before the render pass must
|
|
// finish before the color attachment output stage starts.
|
|
// 2) 0 -> EXTERNAL: the render pass write must complete before the image is
|
|
// read again by shaders or transfer commands.
|
|
// Without (1), drivers may start the subpass before prior transfer/shader
|
|
// writes finish, causing GPU hangs.
|
|
VkSubpassDependency dependencies[2] = {};
|
|
|
|
// Use conservative ALL_COMMANDS / MEMORY_READ|WRITE masks. The render pass
|
|
// is used after many different prior operations (transfers, shader reads,
|
|
// layout transitions, etc.) and an overly narrow dependency is the most
|
|
// common cause of VK_ERROR_DEVICE_LOST on the first draw.
|
|
dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
|
|
dependencies[0].dstSubpass = 0;
|
|
dependencies[0].srcStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
|
|
dependencies[0].dstStageMask =
|
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
dependencies[0].srcAccessMask = VK_ACCESS_MEMORY_READ_BIT |
|
|
VK_ACCESS_MEMORY_WRITE_BIT;
|
|
dependencies[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
|
|
dependencies[1].srcSubpass = 0;
|
|
dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
|
|
dependencies[1].srcStageMask =
|
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
dependencies[1].dstStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
|
|
dependencies[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
dependencies[1].dstAccessMask = VK_ACCESS_MEMORY_READ_BIT |
|
|
VK_ACCESS_MEMORY_WRITE_BIT;
|
|
|
|
VkRenderPassCreateInfo render_pass_info = {};
|
|
render_pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
|
|
render_pass_info.attachmentCount = 1;
|
|
render_pass_info.pAttachments = &color_attachment;
|
|
render_pass_info.subpassCount = 1;
|
|
render_pass_info.pSubpasses = &subpass;
|
|
render_pass_info.dependencyCount = 2;
|
|
render_pass_info.pDependencies = dependencies;
|
|
|
|
VkRenderPass render_pass = VK_NULL_HANDLE;
|
|
VkResult result =
|
|
vkCreateRenderPass(device_, &render_pass_info, nullptr, &render_pass);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to create Vulkan render pass:" << result;
|
|
return VK_NULL_HANDLE;
|
|
}
|
|
|
|
render_pass_cache_.insert(key, render_pass);
|
|
return render_pass;
|
|
}
|
|
|
|
// Uploads a fullscreen quad to device-local memory through a staging buffer.
|
|
bool VulkanRenderer::create_vertex_buffer()
|
|
{
|
|
VkDeviceSize buffer_size = sizeof(k_blit_vertices);
|
|
|
|
VkBufferCreateInfo buffer_info = {};
|
|
buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
|
buffer_info.size = buffer_size;
|
|
buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
|
buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
|
|
VkBuffer staging_buffer;
|
|
VkResult result =
|
|
vkCreateBuffer(device_, &buffer_info, nullptr, &staging_buffer);
|
|
if (result != VK_SUCCESS) {
|
|
return false;
|
|
}
|
|
|
|
VkMemoryRequirements mem_req;
|
|
vkGetBufferMemoryRequirements(device_, staging_buffer, &mem_req);
|
|
|
|
VkMemoryAllocateInfo alloc_info = {};
|
|
alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
|
alloc_info.allocationSize = mem_req.size;
|
|
alloc_info.memoryTypeIndex = find_memory_type(
|
|
mem_req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
|
|
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
|
if (alloc_info.memoryTypeIndex == UINT32_MAX) {
|
|
vkDestroyBuffer(device_, staging_buffer, nullptr);
|
|
return false;
|
|
}
|
|
|
|
VkDeviceMemory staging_memory;
|
|
result = vkAllocateMemory(device_, &alloc_info, nullptr, &staging_memory);
|
|
if (result != VK_SUCCESS) {
|
|
vkDestroyBuffer(device_, staging_buffer, nullptr);
|
|
return false;
|
|
}
|
|
|
|
result = vkBindBufferMemory(device_, staging_buffer, staging_memory, 0);
|
|
if (result != VK_SUCCESS) {
|
|
vkFreeMemory(device_, staging_memory, nullptr);
|
|
vkDestroyBuffer(device_, staging_buffer, nullptr);
|
|
return false;
|
|
}
|
|
|
|
void *data;
|
|
vkMapMemory(device_, staging_memory, 0, buffer_size, 0, &data);
|
|
memcpy(data, k_blit_vertices, (size_t)buffer_size);
|
|
vkUnmapMemory(device_, staging_memory);
|
|
|
|
buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT |
|
|
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
|
|
result = vkCreateBuffer(device_, &buffer_info, nullptr, &vertex_buffer_);
|
|
if (result != VK_SUCCESS) {
|
|
vkFreeMemory(device_, staging_memory, nullptr);
|
|
vkDestroyBuffer(device_, staging_buffer, nullptr);
|
|
return false;
|
|
}
|
|
|
|
vkGetBufferMemoryRequirements(device_, vertex_buffer_, &mem_req);
|
|
|
|
alloc_info.allocationSize = mem_req.size;
|
|
alloc_info.memoryTypeIndex = find_memory_type(
|
|
mem_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
|
if (alloc_info.memoryTypeIndex == UINT32_MAX) {
|
|
vkDestroyBuffer(device_, vertex_buffer_, nullptr);
|
|
vkFreeMemory(device_, staging_memory, nullptr);
|
|
vkDestroyBuffer(device_, staging_buffer, nullptr);
|
|
return false;
|
|
}
|
|
|
|
result =
|
|
vkAllocateMemory(device_, &alloc_info, nullptr, &vertex_buffer_memory_);
|
|
if (result != VK_SUCCESS) {
|
|
vkDestroyBuffer(device_, vertex_buffer_, nullptr);
|
|
vkFreeMemory(device_, staging_memory, nullptr);
|
|
vkDestroyBuffer(device_, staging_buffer, nullptr);
|
|
return false;
|
|
}
|
|
|
|
result =
|
|
vkBindBufferMemory(device_, vertex_buffer_, vertex_buffer_memory_, 0);
|
|
if (result != VK_SUCCESS) {
|
|
vkFreeMemory(device_, vertex_buffer_memory_, nullptr);
|
|
vkDestroyBuffer(device_, vertex_buffer_, nullptr);
|
|
vkFreeMemory(device_, staging_memory, nullptr);
|
|
vkDestroyBuffer(device_, staging_buffer, nullptr);
|
|
return false;
|
|
}
|
|
|
|
// Copy from staging to device local
|
|
VkCommandBuffer cmd = begin_one_time_commands();
|
|
if (cmd == VK_NULL_HANDLE) {
|
|
return false;
|
|
}
|
|
VkBufferCopy copy_region = {};
|
|
copy_region.size = buffer_size;
|
|
vkCmdCopyBuffer(cmd, staging_buffer, vertex_buffer_, 1, ©_region);
|
|
end_one_time_commands(cmd);
|
|
|
|
vkFreeMemory(device_, staging_memory, nullptr);
|
|
vkDestroyBuffer(device_, staging_buffer, nullptr);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Creates the persistent linear sampler shared by all texture bindings.
|
|
bool VulkanRenderer::create_linear_sampler()
|
|
{
|
|
VkSamplerCreateInfo sampler_info = {};
|
|
sampler_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
|
sampler_info.magFilter = VK_FILTER_LINEAR;
|
|
sampler_info.minFilter = VK_FILTER_LINEAR;
|
|
sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
sampler_info.anisotropyEnable = VK_FALSE;
|
|
sampler_info.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
|
sampler_info.unnormalizedCoordinates = VK_FALSE;
|
|
sampler_info.compareEnable = VK_FALSE;
|
|
sampler_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
|
sampler_info.mipLodBias = 0.0f;
|
|
sampler_info.minLod = 0.0f;
|
|
sampler_info.maxLod = 0.0f;
|
|
|
|
VkResult result =
|
|
vkCreateSampler(device_, &sampler_info, nullptr, &linear_sampler_);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to create Vulkan linear sampler:" << result;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Creates the persistent nearest sampler shared by all texture bindings.
|
|
bool VulkanRenderer::create_nearest_sampler()
|
|
{
|
|
VkSamplerCreateInfo sampler_info = {};
|
|
sampler_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
|
sampler_info.magFilter = VK_FILTER_NEAREST;
|
|
sampler_info.minFilter = VK_FILTER_NEAREST;
|
|
sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
sampler_info.anisotropyEnable = VK_FALSE;
|
|
sampler_info.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
|
sampler_info.unnormalizedCoordinates = VK_FALSE;
|
|
sampler_info.compareEnable = VK_FALSE;
|
|
sampler_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
|
sampler_info.mipLodBias = 0.0f;
|
|
sampler_info.minLod = 0.0f;
|
|
sampler_info.maxLod = 0.0f;
|
|
|
|
VkResult result =
|
|
vkCreateSampler(device_, &sampler_info, nullptr, &nearest_sampler_);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to create Vulkan nearest sampler:" << result;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Maps Oak interpolation settings to persistent Vulkan sampler objects.
|
|
VkSampler VulkanRenderer::get_sampler(Texture::Interpolation interpolation) const
|
|
{
|
|
switch (interpolation) {
|
|
case Texture::k_nearest:
|
|
return nearest_sampler_ != VK_NULL_HANDLE ? nearest_sampler_ :
|
|
linear_sampler_;
|
|
case Texture::k_linear:
|
|
case Texture::k_mipmapped_linear:
|
|
default:
|
|
return linear_sampler_;
|
|
}
|
|
}
|
|
|
|
// Returns a renderer-owned host-visible buffer for upload/download transfers.
|
|
// Vulkan allocations are expensive and some drivers fragment host-visible heaps
|
|
// under repeated 4K/F32 readback. Reusing one submit-and-wait staging buffer
|
|
// keeps peak allocation count low while the renderer mutex serializes callers.
|
|
bool VulkanRenderer::create_staging_buffer(VkDeviceSize size,
|
|
VkBuffer *out_buffer,
|
|
VkDeviceMemory *out_memory)
|
|
{
|
|
if (size == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (staging_buffer_ && staging_buffer_->size >= size) {
|
|
*out_buffer = staging_buffer_->buffer;
|
|
*out_memory = staging_buffer_->memory;
|
|
return true;
|
|
}
|
|
|
|
if (staging_buffer_) {
|
|
vkDeviceWaitIdle(device_);
|
|
if (staging_buffer_->buffer != VK_NULL_HANDLE) {
|
|
vkDestroyBuffer(device_, staging_buffer_->buffer, nullptr);
|
|
}
|
|
if (staging_buffer_->memory != VK_NULL_HANDLE) {
|
|
vkFreeMemory(device_, staging_buffer_->memory, nullptr);
|
|
}
|
|
delete staging_buffer_;
|
|
staging_buffer_ = nullptr;
|
|
}
|
|
|
|
VkBufferCreateInfo buffer_info = {};
|
|
buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
|
buffer_info.size = size;
|
|
buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
|
|
VK_BUFFER_USAGE_TRANSFER_DST_BIT |
|
|
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
|
|
buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
|
|
VkBuffer buffer = VK_NULL_HANDLE;
|
|
VkResult result = vkCreateBuffer(device_, &buffer_info, nullptr, &buffer);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to create Vulkan staging buffer:" << result
|
|
<< "size=" << qulonglong(size);
|
|
return false;
|
|
}
|
|
|
|
VkMemoryRequirements mem_req;
|
|
vkGetBufferMemoryRequirements(device_, buffer, &mem_req);
|
|
|
|
VkMemoryAllocateInfo alloc_info = {};
|
|
alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
|
alloc_info.allocationSize = mem_req.size;
|
|
alloc_info.memoryTypeIndex = find_memory_type(
|
|
mem_req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
|
|
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
|
if (alloc_info.memoryTypeIndex == UINT32_MAX) {
|
|
qWarning()
|
|
<< "Failed to find host-visible memory type for Vulkan staging buffer";
|
|
vkDestroyBuffer(device_, buffer, nullptr);
|
|
return false;
|
|
}
|
|
|
|
VkDeviceMemory memory = VK_NULL_HANDLE;
|
|
result = vkAllocateMemory(device_, &alloc_info, nullptr, &memory);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning()
|
|
<< "Failed to allocate Vulkan staging buffer memory:" << result
|
|
<< "size=" << qulonglong(size)
|
|
<< "allocation=" << qulonglong(mem_req.size);
|
|
vkDestroyBuffer(device_, buffer, nullptr);
|
|
return false;
|
|
}
|
|
|
|
result = vkBindBufferMemory(device_, buffer, memory, 0);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to bind Vulkan staging buffer memory:" << result;
|
|
vkFreeMemory(device_, memory, nullptr);
|
|
vkDestroyBuffer(device_, buffer, nullptr);
|
|
return false;
|
|
}
|
|
|
|
staging_buffer_ = new StagingBuffer();
|
|
staging_buffer_->buffer = buffer;
|
|
staging_buffer_->memory = memory;
|
|
staging_buffer_->size = mem_req.size;
|
|
*out_buffer = buffer;
|
|
*out_memory = memory;
|
|
return true;
|
|
}
|
|
|
|
// Kept for existing call sites; runtime staging buffers are renderer-owned and
|
|
// released in DestroyInternal() or when a larger staging allocation is required.
|
|
void VulkanRenderer::destroy_staging_buffer(VkBuffer buffer,
|
|
VkDeviceMemory memory)
|
|
{
|
|
if (staging_buffer_ && buffer == staging_buffer_->buffer &&
|
|
memory == staging_buffer_->memory) {
|
|
return;
|
|
}
|
|
if (buffer != VK_NULL_HANDLE) {
|
|
vkDestroyBuffer(device_, buffer, nullptr);
|
|
}
|
|
if (memory != VK_NULL_HANDLE) {
|
|
vkFreeMemory(device_, memory, nullptr);
|
|
}
|
|
}
|
|
|
|
// Starts a primary command buffer intended for immediate submit-and-wait use.
|
|
VkCommandBuffer VulkanRenderer::begin_one_time_commands()
|
|
{
|
|
if (reusable_command_buffer_ == VK_NULL_HANDLE) {
|
|
VkCommandBufferAllocateInfo alloc_info = {};
|
|
alloc_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
|
alloc_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
|
alloc_info.commandPool = command_pool_;
|
|
alloc_info.commandBufferCount = 1;
|
|
|
|
VkResult result = vkAllocateCommandBuffers(device_, &alloc_info,
|
|
&reusable_command_buffer_);
|
|
if (result != VK_SUCCESS ||
|
|
reusable_command_buffer_ == VK_NULL_HANDLE) {
|
|
qWarning() << "Failed to allocate Vulkan command buffer:" << result;
|
|
return VK_NULL_HANDLE;
|
|
}
|
|
} else {
|
|
vkResetCommandBuffer(reusable_command_buffer_, 0);
|
|
}
|
|
|
|
VkCommandBufferBeginInfo begin_info = {};
|
|
begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
|
begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
|
|
|
VkResult result =
|
|
vkBeginCommandBuffer(reusable_command_buffer_, &begin_info);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to begin Vulkan command buffer:" << result;
|
|
return VK_NULL_HANDLE;
|
|
}
|
|
return reusable_command_buffer_;
|
|
}
|
|
|
|
// Submits a one-time command buffer and waits with a timeout. Using a fence
|
|
// instead of vkQueueWaitIdle prevents the CPU thread from blocking forever if
|
|
// a bad barrier/shader causes the GPU to hang.
|
|
void VulkanRenderer::end_one_time_commands(VkCommandBuffer cmd)
|
|
{
|
|
if (cmd == VK_NULL_HANDLE) {
|
|
return;
|
|
}
|
|
|
|
if (device_lost_) {
|
|
return;
|
|
}
|
|
|
|
vkEndCommandBuffer(cmd);
|
|
|
|
if (reusable_fence_ == VK_NULL_HANDLE) {
|
|
VkFenceCreateInfo fence_info = {};
|
|
fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
|
|
|
VkResult result =
|
|
vkCreateFence(device_, &fence_info, nullptr, &reusable_fence_);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to create Vulkan fence:" << result;
|
|
return;
|
|
}
|
|
} else {
|
|
vkResetFences(device_, 1, &reusable_fence_);
|
|
}
|
|
|
|
VkSubmitInfo submit_info = {};
|
|
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
|
submit_info.commandBufferCount = 1;
|
|
submit_info.pCommandBuffers = &cmd;
|
|
|
|
VkResult result =
|
|
vkQueueSubmit(graphics_queue_, 1, &submit_info, reusable_fence_);
|
|
if (result != VK_SUCCESS) {
|
|
if (result == VK_ERROR_DEVICE_LOST) {
|
|
if (!device_lost_) {
|
|
device_lost_ = true;
|
|
qCritical()
|
|
<< "Vulkan device lost during vkQueueSubmit; stopping "
|
|
"further GPU submissions";
|
|
}
|
|
} else {
|
|
qWarning() << "vkQueueSubmit failed:" << result;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 10 second timeout. If the GPU is hung, the process can report it instead
|
|
// of blocking forever. Note: a true GPU hang may still freeze the display
|
|
// before this timeout is reached, but the CPU-side wait will not deadlock.
|
|
constexpr uint64_t k_timeout_ns = 10ULL * 1000ULL * 1000ULL * 1000ULL;
|
|
result = vkWaitForFences(device_, 1, &reusable_fence_, VK_TRUE, k_timeout_ns);
|
|
if (result == VK_TIMEOUT) {
|
|
qCritical() << "Vulkan GPU wait timed out; the GPU may be hung";
|
|
} else if (result != VK_SUCCESS) {
|
|
qWarning() << "vkWaitForFences failed:" << result;
|
|
}
|
|
}
|
|
|
|
// Emits a conservative barrier for the image layout transitions used by this
|
|
// renderer: upload, shader read, color attachment, clear, and readback.
|
|
void VulkanRenderer::transition_image_layout(VkCommandBuffer cmd, VkImage image,
|
|
VkImageLayout old_layout,
|
|
VkImageLayout new_layout)
|
|
{
|
|
VkImageMemoryBarrier barrier = {};
|
|
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
barrier.oldLayout = old_layout;
|
|
barrier.newLayout = new_layout;
|
|
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
barrier.image = image;
|
|
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
barrier.subresourceRange.baseMipLevel = 0;
|
|
barrier.subresourceRange.levelCount = 1;
|
|
barrier.subresourceRange.baseArrayLayer = 0;
|
|
barrier.subresourceRange.layerCount = 1;
|
|
|
|
VkPipelineStageFlags source_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
|
VkPipelineStageFlags destination_stage =
|
|
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
|
|
bool handled = false;
|
|
|
|
auto set_transfer = [&]() {
|
|
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
|
source_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
|
handled = true;
|
|
};
|
|
auto set_shader_read = [&]() {
|
|
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
source_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
|
handled = true;
|
|
};
|
|
auto set_color_attachment = [&]() {
|
|
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
source_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
handled = true;
|
|
};
|
|
|
|
if (old_layout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
|
barrier.srcAccessMask = 0;
|
|
source_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
|
if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
|
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
|
handled = true;
|
|
} else if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
|
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
|
handled = true;
|
|
} else if (new_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
|
|
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
|
handled = true;
|
|
} else if (new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
|
|
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
handled = true;
|
|
}
|
|
} else if (old_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
|
|
if (new_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
|
|
set_shader_read();
|
|
} else if (new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
|
|
set_color_attachment();
|
|
} else if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
|
|
set_transfer();
|
|
}
|
|
} else if (old_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
|
|
barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
|
source_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
|
if (new_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
|
|
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
|
handled = true;
|
|
} else if (new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
|
|
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
handled = true;
|
|
} else if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
|
|
set_transfer();
|
|
}
|
|
} else if (old_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
|
|
barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
source_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
|
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
|
handled = true;
|
|
} else if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
|
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
|
handled = true;
|
|
} else if (new_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
|
|
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
|
handled = true;
|
|
}
|
|
} else if (old_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
|
|
barrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
source_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
|
if (new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
|
|
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
handled = true;
|
|
} else if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
|
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
|
handled = true;
|
|
} else if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
|
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
|
handled = true;
|
|
}
|
|
}
|
|
|
|
if (!handled) {
|
|
qWarning()
|
|
<< "Unhandled Vulkan layout transition from" << old_layout << "to"
|
|
<< new_layout << "- using conservative ALL_COMMANDS barrier";
|
|
barrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT |
|
|
VK_ACCESS_MEMORY_WRITE_BIT;
|
|
barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT |
|
|
VK_ACCESS_MEMORY_WRITE_BIT;
|
|
source_stage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
|
|
destination_stage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
|
|
}
|
|
|
|
vkCmdPipelineBarrier(cmd, source_stage, destination_stage, 0, 0, nullptr, 0,
|
|
nullptr, 1, &barrier);
|
|
}
|
|
|
|
// Records a buffer-to-image copy for tightly packed texture uploads.
|
|
void VulkanRenderer::copy_buffer_to_image(VkCommandBuffer cmd, VkBuffer buffer,
|
|
VkImage image, uint32_t width,
|
|
uint32_t height, uint32_t depth)
|
|
{
|
|
VkBufferImageCopy region = {};
|
|
region.bufferOffset = 0;
|
|
region.bufferRowLength = 0;
|
|
region.bufferImageHeight = 0;
|
|
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
region.imageSubresource.mipLevel = 0;
|
|
region.imageSubresource.baseArrayLayer = 0;
|
|
region.imageSubresource.layerCount = 1;
|
|
region.imageOffset = { 0, 0, 0 };
|
|
region.imageExtent = { width, height, depth };
|
|
|
|
vkCmdCopyBufferToImage(cmd, buffer, image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion);
|
|
}
|
|
|
|
// Records an image-to-buffer copy for full texture downloads or one-pixel reads.
|
|
void VulkanRenderer::copy_image_to_buffer(VkCommandBuffer cmd, VkImage image,
|
|
VkBuffer buffer, uint32_t width,
|
|
uint32_t height, uint32_t offset_x,
|
|
uint32_t offset_y)
|
|
{
|
|
VkBufferImageCopy region = {};
|
|
region.bufferOffset = 0;
|
|
region.bufferRowLength = 0;
|
|
region.bufferImageHeight = 0;
|
|
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
region.imageSubresource.mipLevel = 0;
|
|
region.imageSubresource.baseArrayLayer = 0;
|
|
region.imageSubresource.layerCount = 1;
|
|
region.imageOffset = { static_cast<int32_t>(offset_x),
|
|
static_cast<int32_t>(offset_y), 0 };
|
|
region.imageExtent = { width, height, 1 };
|
|
|
|
vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
buffer, 1, ®ion);
|
|
}
|
|
|
|
// Converts Oak's pixel format/channel count pair to the closest Vulkan format.
|
|
VkFormat VulkanRenderer::pixel_format_to_vk_format(PixelFormat format,
|
|
int channel_count) const
|
|
{
|
|
switch (format) {
|
|
case PixelFormat::u8:
|
|
switch (channel_count) {
|
|
case 1:
|
|
return VK_FORMAT_R8_UNORM;
|
|
case 2:
|
|
return VK_FORMAT_R8G8_UNORM;
|
|
case 3:
|
|
return VK_FORMAT_R8G8B8_UNORM;
|
|
case 4:
|
|
return VK_FORMAT_R8G8B8A8_UNORM;
|
|
}
|
|
break;
|
|
case PixelFormat::u10:
|
|
if (channel_count == 4) {
|
|
return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
|
}
|
|
break;
|
|
case PixelFormat::u16:
|
|
switch (channel_count) {
|
|
case 1:
|
|
return VK_FORMAT_R16_UNORM;
|
|
case 2:
|
|
return VK_FORMAT_R16G16_UNORM;
|
|
case 3:
|
|
return VK_FORMAT_R16G16B16_UNORM;
|
|
case 4:
|
|
return VK_FORMAT_R16G16B16A16_UNORM;
|
|
}
|
|
break;
|
|
case PixelFormat::f16:
|
|
switch (channel_count) {
|
|
case 1:
|
|
return VK_FORMAT_R16_SFLOAT;
|
|
case 2:
|
|
return VK_FORMAT_R16G16_SFLOAT;
|
|
case 3:
|
|
return VK_FORMAT_R16G16B16_SFLOAT;
|
|
case 4:
|
|
return VK_FORMAT_R16G16B16A16_SFLOAT;
|
|
}
|
|
break;
|
|
case PixelFormat::f32:
|
|
switch (channel_count) {
|
|
case 1:
|
|
return VK_FORMAT_R32_SFLOAT;
|
|
case 2:
|
|
return VK_FORMAT_R32G32_SFLOAT;
|
|
case 3:
|
|
return VK_FORMAT_R32G32B32_SFLOAT;
|
|
case 4:
|
|
return VK_FORMAT_R32G32B32A32_SFLOAT;
|
|
}
|
|
break;
|
|
case PixelFormat::invalid:
|
|
case PixelFormat::count:
|
|
break;
|
|
}
|
|
return VK_FORMAT_UNDEFINED;
|
|
}
|
|
|
|
// Checks color-attachment support before selecting renderable image formats.
|
|
bool VulkanRenderer::is_color_attachment_supported(VkFormat format) const
|
|
{
|
|
VkFormatProperties props;
|
|
vkGetPhysicalDeviceFormatProperties(physical_device_, format, &props);
|
|
return (props.optimalTilingFeatures &
|
|
VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
|
|
}
|
|
|
|
// Chooses a renderable Vulkan format and falls back from RGB to RGBA when a
|
|
// driver does not expose 3-channel color attachment support.
|
|
VkFormat VulkanRenderer::pick_renderable_format(PixelFormat format,
|
|
int channel_count) const
|
|
{
|
|
VkFormat candidate = pixel_format_to_vk_format(format, channel_count);
|
|
if (candidate != VK_FORMAT_UNDEFINED &&
|
|
is_color_attachment_supported(candidate)) {
|
|
return candidate;
|
|
}
|
|
|
|
// 3-channel formats are often unsupported as color attachments; fallback
|
|
// to the 4-channel equivalent.
|
|
if (channel_count == 3) {
|
|
VkFormat rgba = pixel_format_to_vk_format(format, 4);
|
|
if (rgba != VK_FORMAT_UNDEFINED && is_color_attachment_supported(rgba)) {
|
|
return rgba;
|
|
}
|
|
}
|
|
|
|
return VK_FORMAT_UNDEFINED;
|
|
}
|
|
|
|
// Returns the packed texel size for the VkFormat values generated above.
|
|
int VulkanRenderer::get_vk_format_bytes_per_pixel(VkFormat format) const
|
|
{
|
|
switch (format) {
|
|
case VK_FORMAT_R8_UNORM:
|
|
case VK_FORMAT_R8_UINT:
|
|
case VK_FORMAT_R8_SINT:
|
|
return 1;
|
|
case VK_FORMAT_R8G8_UNORM:
|
|
return 2;
|
|
case VK_FORMAT_R8G8B8_UNORM:
|
|
return 3;
|
|
case VK_FORMAT_R8G8B8A8_UNORM:
|
|
return 4;
|
|
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
|
|
return 4;
|
|
case VK_FORMAT_R16_UNORM:
|
|
case VK_FORMAT_R16_SFLOAT:
|
|
return 2;
|
|
case VK_FORMAT_R16G16_UNORM:
|
|
case VK_FORMAT_R16G16_SFLOAT:
|
|
return 4;
|
|
case VK_FORMAT_R16G16B16_UNORM:
|
|
case VK_FORMAT_R16G16B16_SFLOAT:
|
|
return 6;
|
|
case VK_FORMAT_R16G16B16A16_UNORM:
|
|
case VK_FORMAT_R16G16B16A16_SFLOAT:
|
|
return 8;
|
|
case VK_FORMAT_R32_SFLOAT:
|
|
return 4;
|
|
case VK_FORMAT_R32G32_SFLOAT:
|
|
return 8;
|
|
case VK_FORMAT_R32G32B32_SFLOAT:
|
|
return 12;
|
|
case VK_FORMAT_R32G32B32A32_SFLOAT:
|
|
return 16;
|
|
default:
|
|
// For packed or compressed formats, return 0 and let callers fall back
|
|
// to the requested channel count.
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Returns the alpha fill value used when expanding formats without alpha.
|
|
float VulkanRenderer::get_format_max_alpha(PixelFormat format) const
|
|
{
|
|
if (format == PixelFormat::u8) {
|
|
return 255.0f;
|
|
} else if (format == PixelFormat::u16) {
|
|
return 65535.0f;
|
|
}
|
|
return 1.0f;
|
|
}
|
|
|
|
// Copies tightly-packed pixels while changing channel count. This handles the
|
|
// common Vulkan fallback where requested RGB data is stored as RGBA on the GPU.
|
|
void VulkanRenderer::copy_pixels_with_channel_conversion(
|
|
const void *src, void *dst, int width, int height, int depth,
|
|
int src_channels, int dst_channels, PixelFormat format) const
|
|
{
|
|
int src_bpc = VideoParams::get_bytes_per_channel(format);
|
|
int dst_bpc = src_bpc;
|
|
float alpha = get_format_max_alpha(format);
|
|
|
|
int plane_pixels = width * height;
|
|
int total_pixels = plane_pixels * depth;
|
|
|
|
const char *src_ptr = static_cast<const char *>(src);
|
|
char *dst_ptr = static_cast<char *>(dst);
|
|
|
|
for (int i = 0; i < total_pixels; ++i) {
|
|
for (int c = 0; c < dst_channels; ++c) {
|
|
if (c < src_channels) {
|
|
memcpy(dst_ptr + (i * dst_channels + c) * dst_bpc,
|
|
src_ptr + (i * src_channels + c) * src_bpc, dst_bpc);
|
|
} else {
|
|
// Fill missing channels with 0 (color) or max alpha.
|
|
if (c == 3) {
|
|
if (format == PixelFormat::u8) {
|
|
*reinterpret_cast<uint8_t *>(
|
|
dst_ptr + (i * dst_channels + c) * dst_bpc) =
|
|
static_cast<uint8_t>(alpha);
|
|
} else if (format == PixelFormat::u16) {
|
|
*reinterpret_cast<uint16_t *>(
|
|
dst_ptr + (i * dst_channels + c) * dst_bpc) =
|
|
static_cast<uint16_t>(alpha);
|
|
} else if (format == PixelFormat::f16) {
|
|
// Half-float 1.0: 0x3C00
|
|
*reinterpret_cast<uint16_t *>(
|
|
dst_ptr + (i * dst_channels + c) * dst_bpc) =
|
|
0x3C00;
|
|
} else {
|
|
*reinterpret_cast<float *>(
|
|
dst_ptr + (i * dst_channels + c) * dst_bpc) = alpha;
|
|
}
|
|
} else {
|
|
memset(dst_ptr + (i * dst_channels + c) * dst_bpc, 0,
|
|
dst_bpc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rounds size up to the next multiple of alignment.
|
|
VkDeviceSize VulkanRenderer::align_size(VkDeviceSize size,
|
|
VkDeviceSize alignment) const
|
|
{
|
|
return (size + alignment - 1) & ~(alignment - 1);
|
|
}
|
|
|
|
// Finds a compatible memory type satisfying Vulkan's bitmask and property flags.
|
|
uint32_t VulkanRenderer::find_memory_type(uint32_t type_filter,
|
|
VkMemoryPropertyFlags properties) const
|
|
{
|
|
for (uint32_t i = 0; i < mem_properties_.memoryTypeCount; i++) {
|
|
if ((type_filter & (1 << i)) &&
|
|
(mem_properties_.memoryTypes[i].propertyFlags & properties) ==
|
|
properties) {
|
|
return i;
|
|
}
|
|
}
|
|
return UINT32_MAX;
|
|
}
|
|
|
|
// Creates a Vulkan image, memory allocation, and image view for an Oak texture.
|
|
QVariant VulkanRenderer::create_native_texture(int width, int height, int depth,
|
|
PixelFormat format,
|
|
int channel_count,
|
|
const void *data, int linesize)
|
|
{
|
|
QMutexLocker lock(&mutex_);
|
|
|
|
VkFormat vk_format = pick_renderable_format(format, channel_count);
|
|
if (vk_format == VK_FORMAT_UNDEFINED) {
|
|
qWarning() << "Unsupported pixel format for Vulkan texture";
|
|
return QVariant();
|
|
}
|
|
|
|
VulkanTexture *tex = new VulkanTexture();
|
|
tex->id = next_texture_id_++;
|
|
tex->width = width;
|
|
tex->height = height;
|
|
tex->depth = depth;
|
|
tex->format = format;
|
|
tex->channel_count = channel_count;
|
|
tex->vk_format = vk_format;
|
|
|
|
VkImageCreateInfo image_info = {};
|
|
image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
|
image_info.imageType = depth > 1 ? VK_IMAGE_TYPE_3D : VK_IMAGE_TYPE_2D;
|
|
image_info.extent.width = static_cast<uint32_t>(width);
|
|
image_info.extent.height = static_cast<uint32_t>(height);
|
|
// Vulkan requires extent.depth >= 1 for all image types; for 2D images it
|
|
// must be exactly 1. Some callers pass 0 for 2D textures, which would
|
|
// otherwise produce validation errors and device lost.
|
|
image_info.extent.depth = static_cast<uint32_t>(depth > 0 ? depth : 1);
|
|
image_info.mipLevels = 1;
|
|
image_info.arrayLayers = 1;
|
|
image_info.format = vk_format;
|
|
image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
image_info.usage =
|
|
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
|
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
|
image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
|
|
VkResult result = vkCreateImage(device_, &image_info, nullptr, &tex->image);
|
|
if (result != VK_SUCCESS) {
|
|
delete tex;
|
|
return QVariant();
|
|
}
|
|
|
|
VkMemoryRequirements mem_req;
|
|
vkGetImageMemoryRequirements(device_, tex->image, &mem_req);
|
|
|
|
VkMemoryAllocateInfo alloc_info = {};
|
|
alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
|
alloc_info.allocationSize = mem_req.size;
|
|
alloc_info.memoryTypeIndex = find_memory_type(
|
|
mem_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
|
if (alloc_info.memoryTypeIndex == UINT32_MAX) {
|
|
qWarning()
|
|
<< "Failed to find device-local memory type for Vulkan image";
|
|
vkDestroyImage(device_, tex->image, nullptr);
|
|
delete tex;
|
|
return QVariant();
|
|
}
|
|
|
|
result = vkAllocateMemory(device_, &alloc_info, nullptr, &tex->memory);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning()
|
|
<< "Failed to allocate device memory for Vulkan image:" << result;
|
|
vkDestroyImage(device_, tex->image, nullptr);
|
|
delete tex;
|
|
return QVariant();
|
|
}
|
|
|
|
result = vkBindImageMemory(device_, tex->image, tex->memory, 0);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to bind Vulkan image memory:" << result;
|
|
vkFreeMemory(device_, tex->memory, nullptr);
|
|
vkDestroyImage(device_, tex->image, nullptr);
|
|
delete tex;
|
|
return QVariant();
|
|
}
|
|
|
|
VkImageViewCreateInfo view_info = {};
|
|
view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
|
view_info.image = tex->image;
|
|
view_info.viewType = depth > 1 ? VK_IMAGE_VIEW_TYPE_3D :
|
|
VK_IMAGE_VIEW_TYPE_2D;
|
|
view_info.format = vk_format;
|
|
view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
view_info.subresourceRange.baseMipLevel = 0;
|
|
view_info.subresourceRange.levelCount = 1;
|
|
view_info.subresourceRange.baseArrayLayer = 0;
|
|
view_info.subresourceRange.layerCount = 1;
|
|
|
|
// Single-channel textures are typically intended as grayscale. Replicate the
|
|
// red channel to RGB and force alpha to 1, matching OpenGL's swizzle behavior.
|
|
if (channel_count == 1) {
|
|
view_info.components.r = VK_COMPONENT_SWIZZLE_R;
|
|
view_info.components.g = VK_COMPONENT_SWIZZLE_R;
|
|
view_info.components.b = VK_COMPONENT_SWIZZLE_R;
|
|
view_info.components.a = VK_COMPONENT_SWIZZLE_ONE;
|
|
}
|
|
|
|
result = vkCreateImageView(device_, &view_info, nullptr, &tex->view);
|
|
if (result != VK_SUCCESS) {
|
|
vkFreeMemory(device_, tex->memory, nullptr);
|
|
vkDestroyImage(device_, tex->image, nullptr);
|
|
delete tex;
|
|
return QVariant();
|
|
}
|
|
|
|
// Upload initial data if provided
|
|
if (data) {
|
|
int cpu_bytes_per_pixel =
|
|
VideoParams::get_bytes_per_pixel(format, channel_count);
|
|
int gpu_bytes_per_pixel = get_vk_format_bytes_per_pixel(vk_format);
|
|
if (gpu_bytes_per_pixel == 0) {
|
|
gpu_bytes_per_pixel = cpu_bytes_per_pixel;
|
|
}
|
|
VkDeviceSize image_size = static_cast<VkDeviceSize>(width) * height *
|
|
depth * gpu_bytes_per_pixel;
|
|
if (linesize == 0) {
|
|
linesize = width;
|
|
}
|
|
const int row_stride_bytes = linesize * cpu_bytes_per_pixel;
|
|
|
|
VkBuffer staging_buffer;
|
|
VkDeviceMemory staging_memory;
|
|
if (create_staging_buffer(image_size, &staging_buffer, &staging_memory)) {
|
|
void *mapped;
|
|
vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped);
|
|
if (cpu_bytes_per_pixel == gpu_bytes_per_pixel) {
|
|
if (linesize == width) {
|
|
memcpy(mapped, data, static_cast<size_t>(image_size));
|
|
} else {
|
|
char *dst = static_cast<char *>(mapped);
|
|
const char *src = static_cast<const char *>(data);
|
|
for (int row = 0; row < height * depth; row++) {
|
|
memcpy(
|
|
dst + row * width * cpu_bytes_per_pixel,
|
|
src + row * row_stride_bytes,
|
|
static_cast<size_t>(width * cpu_bytes_per_pixel));
|
|
}
|
|
}
|
|
} else {
|
|
// The GPU format has a different channel count than the CPU data
|
|
// (e.g. 3-channel RGB fallback to 4-channel RGBA). Repack the data
|
|
// in the staging buffer so the copy uses the GPU texel layout.
|
|
QByteArray tmp(width * height * depth * cpu_bytes_per_pixel,
|
|
Qt::Uninitialized);
|
|
if (linesize == width) {
|
|
memcpy(tmp.data(), data, static_cast<size_t>(tmp.size()));
|
|
} else {
|
|
char *dst = tmp.data();
|
|
const char *src = static_cast<const char *>(data);
|
|
for (int row = 0; row < height * depth; row++) {
|
|
memcpy(
|
|
dst + row * width * cpu_bytes_per_pixel,
|
|
src + row * row_stride_bytes,
|
|
static_cast<size_t>(width * cpu_bytes_per_pixel));
|
|
}
|
|
}
|
|
int gpu_channels = gpu_bytes_per_pixel /
|
|
VideoParams::get_bytes_per_channel(format);
|
|
copy_pixels_with_channel_conversion(tmp.constData(), mapped, width,
|
|
height, depth, channel_count,
|
|
gpu_channels, format);
|
|
}
|
|
vkUnmapMemory(device_, staging_memory);
|
|
|
|
VkCommandBuffer cmd = begin_one_time_commands();
|
|
|
|
if (cmd == VK_NULL_HANDLE) {
|
|
return QVariant();
|
|
}
|
|
transition_image_layout(cmd, tex->image, VK_IMAGE_LAYOUT_UNDEFINED,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
|
copy_buffer_to_image(cmd, staging_buffer, tex->image,
|
|
static_cast<uint32_t>(width),
|
|
static_cast<uint32_t>(height),
|
|
static_cast<uint32_t>(depth));
|
|
transition_image_layout(cmd, tex->image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
|
end_one_time_commands(cmd);
|
|
|
|
destroy_staging_buffer(staging_buffer, staging_memory);
|
|
tex->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
}
|
|
} else {
|
|
VkCommandBuffer cmd = begin_one_time_commands();
|
|
if (cmd == VK_NULL_HANDLE) {
|
|
return QVariant();
|
|
}
|
|
transition_image_layout(cmd, tex->image, VK_IMAGE_LAYOUT_UNDEFINED,
|
|
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
|
end_one_time_commands(cmd);
|
|
tex->current_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
}
|
|
|
|
textures_.insert(tex->id, tex);
|
|
return QVariant::fromValue(tex->id);
|
|
}
|
|
|
|
// Destroys a texture handle and all Vulkan objects owned by that texture.
|
|
void VulkanRenderer::destroy_native_texture(QVariant texture)
|
|
{
|
|
QMutexLocker lock(&mutex_);
|
|
quint64 id = texture.value<quint64>();
|
|
VulkanTexture *tex = textures_.take(id);
|
|
if (!tex) {
|
|
return;
|
|
}
|
|
if (tex->framebuffer != VK_NULL_HANDLE) {
|
|
vkDestroyFramebuffer(device_, tex->framebuffer, nullptr);
|
|
}
|
|
if (tex->view != VK_NULL_HANDLE) {
|
|
vkDestroyImageView(device_, tex->view, nullptr);
|
|
}
|
|
if (tex->image != VK_NULL_HANDLE) {
|
|
vkDestroyImage(device_, tex->image, nullptr);
|
|
}
|
|
if (tex->memory != VK_NULL_HANDLE) {
|
|
vkFreeMemory(device_, tex->memory, nullptr);
|
|
}
|
|
delete tex;
|
|
}
|
|
|
|
// Uploads CPU pixels to an existing image. The staging layout is based on the
|
|
// selected GPU VkFormat, then CPU data is repacked when channel counts differ.
|
|
void VulkanRenderer::upload_to_texture(const QVariant &handle,
|
|
const VideoParams ¶ms,
|
|
const void *data, int linesize)
|
|
{
|
|
QMutexLocker lock(&mutex_);
|
|
quint64 id = handle.value<quint64>();
|
|
VulkanTexture *tex = textures_.value(id);
|
|
if (!tex || !data) {
|
|
return;
|
|
}
|
|
|
|
int width = params.effective_width();
|
|
int height = params.effective_height();
|
|
int depth = params.effective_depth();
|
|
int cpu_bytes_per_pixel =
|
|
VideoParams::get_bytes_per_pixel(params.format(), params.channel_count());
|
|
int gpu_bytes_per_pixel = get_vk_format_bytes_per_pixel(tex->vk_format);
|
|
if (gpu_bytes_per_pixel == 0) {
|
|
gpu_bytes_per_pixel = cpu_bytes_per_pixel;
|
|
}
|
|
VkDeviceSize image_size =
|
|
static_cast<VkDeviceSize>(width) * height * depth * gpu_bytes_per_pixel;
|
|
if (linesize == 0) {
|
|
linesize = width;
|
|
}
|
|
const int row_stride_bytes = linesize * cpu_bytes_per_pixel;
|
|
|
|
VkBuffer staging_buffer;
|
|
VkDeviceMemory staging_memory;
|
|
if (!create_staging_buffer(image_size, &staging_buffer, &staging_memory)) {
|
|
return;
|
|
}
|
|
|
|
void *mapped;
|
|
vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped);
|
|
if (cpu_bytes_per_pixel == gpu_bytes_per_pixel) {
|
|
if (linesize == width) {
|
|
memcpy(mapped, data, static_cast<size_t>(image_size));
|
|
} else {
|
|
char *dst = static_cast<char *>(mapped);
|
|
const char *src = static_cast<const char *>(data);
|
|
for (int row = 0; row < height * depth; row++) {
|
|
memcpy(dst + row * width * cpu_bytes_per_pixel,
|
|
src + row * row_stride_bytes,
|
|
static_cast<size_t>(width * cpu_bytes_per_pixel));
|
|
}
|
|
}
|
|
} else {
|
|
QByteArray tmp(width * height * depth * cpu_bytes_per_pixel,
|
|
Qt::Uninitialized);
|
|
if (linesize == width) {
|
|
memcpy(tmp.data(), data, static_cast<size_t>(tmp.size()));
|
|
} else {
|
|
char *dst = tmp.data();
|
|
const char *src = static_cast<const char *>(data);
|
|
for (int row = 0; row < height * depth; row++) {
|
|
memcpy(dst + row * width * cpu_bytes_per_pixel,
|
|
src + row * row_stride_bytes,
|
|
static_cast<size_t>(width * cpu_bytes_per_pixel));
|
|
}
|
|
}
|
|
int gpu_channels = gpu_bytes_per_pixel /
|
|
VideoParams::get_bytes_per_channel(params.format());
|
|
copy_pixels_with_channel_conversion(tmp.constData(), mapped, width, height,
|
|
depth, params.channel_count(),
|
|
gpu_channels, params.format());
|
|
}
|
|
vkUnmapMemory(device_, staging_memory);
|
|
|
|
VkCommandBuffer cmd = begin_one_time_commands();
|
|
|
|
if (cmd == VK_NULL_HANDLE) {
|
|
return;
|
|
}
|
|
if (tex->current_layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
|
|
transition_image_layout(cmd, tex->image, tex->current_layout,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
|
}
|
|
copy_buffer_to_image(cmd, staging_buffer, tex->image,
|
|
static_cast<uint32_t>(width),
|
|
static_cast<uint32_t>(height),
|
|
static_cast<uint32_t>(depth));
|
|
transition_image_layout(cmd, tex->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
|
end_one_time_commands(cmd);
|
|
|
|
destroy_staging_buffer(staging_buffer, staging_memory);
|
|
tex->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
}
|
|
|
|
// Downloads an image to CPU memory. When the GPU format is wider than the
|
|
// requested CPU format, the staging data is compacted back to the caller layout.
|
|
void VulkanRenderer::download_from_texture(const QVariant &handle,
|
|
const VideoParams ¶ms, void *data,
|
|
int linesize)
|
|
{
|
|
QMutexLocker lock(&mutex_);
|
|
quint64 id = handle.value<quint64>();
|
|
VulkanTexture *tex = textures_.value(id);
|
|
if (!tex || !data) {
|
|
return;
|
|
}
|
|
|
|
int width = params.effective_width();
|
|
int height = params.effective_height();
|
|
int cpu_bytes_per_pixel =
|
|
VideoParams::get_bytes_per_pixel(params.format(), params.channel_count());
|
|
int gpu_bytes_per_pixel = get_vk_format_bytes_per_pixel(tex->vk_format);
|
|
if (gpu_bytes_per_pixel == 0) {
|
|
gpu_bytes_per_pixel = cpu_bytes_per_pixel;
|
|
}
|
|
if (linesize == 0) {
|
|
linesize = width;
|
|
}
|
|
const int row_stride_bytes = linesize * cpu_bytes_per_pixel;
|
|
VkDeviceSize image_size =
|
|
static_cast<VkDeviceSize>(width) * height * gpu_bytes_per_pixel;
|
|
|
|
VkBuffer staging_buffer;
|
|
VkDeviceMemory staging_memory;
|
|
if (!create_staging_buffer(image_size, &staging_buffer, &staging_memory)) {
|
|
return;
|
|
}
|
|
|
|
VkCommandBuffer cmd = begin_one_time_commands();
|
|
|
|
if (cmd == VK_NULL_HANDLE) {
|
|
return;
|
|
}
|
|
if (tex->current_layout != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
|
|
transition_image_layout(cmd, tex->image, tex->current_layout,
|
|
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
|
}
|
|
copy_image_to_buffer(cmd, tex->image, staging_buffer,
|
|
static_cast<uint32_t>(width),
|
|
static_cast<uint32_t>(height));
|
|
end_one_time_commands(cmd);
|
|
|
|
void *mapped;
|
|
vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped);
|
|
if (cpu_bytes_per_pixel == gpu_bytes_per_pixel) {
|
|
if (linesize == width) {
|
|
memcpy(data, mapped, static_cast<size_t>(image_size));
|
|
} else {
|
|
char *dst = static_cast<char *>(data);
|
|
const char *src = static_cast<const char *>(mapped);
|
|
for (int row = 0; row < height; row++) {
|
|
memcpy(dst + row * row_stride_bytes,
|
|
src + row * width * cpu_bytes_per_pixel,
|
|
static_cast<size_t>(width * cpu_bytes_per_pixel));
|
|
}
|
|
}
|
|
} else {
|
|
int gpu_channels = gpu_bytes_per_pixel /
|
|
VideoParams::get_bytes_per_channel(params.format());
|
|
QByteArray tmp(width * height * gpu_bytes_per_pixel, Qt::Uninitialized);
|
|
memcpy(tmp.data(), mapped, static_cast<size_t>(tmp.size()));
|
|
copy_pixels_with_channel_conversion(tmp.constData(), data, width, height, 1,
|
|
gpu_channels, params.channel_count(),
|
|
params.format());
|
|
if (linesize != width) {
|
|
// Repack from tight CPU layout to caller's stride in-place.
|
|
QByteArray tight(static_cast<const char *>(data),
|
|
width * height * cpu_bytes_per_pixel);
|
|
char *dst = static_cast<char *>(data);
|
|
for (int row = 0; row < height; row++) {
|
|
memcpy(dst + row * row_stride_bytes,
|
|
tight.constData() + row * width * cpu_bytes_per_pixel,
|
|
static_cast<size_t>(width * cpu_bytes_per_pixel));
|
|
}
|
|
}
|
|
}
|
|
vkUnmapMemory(device_, staging_memory);
|
|
|
|
destroy_staging_buffer(staging_buffer, staging_memory);
|
|
tex->current_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
|
}
|
|
|
|
// Blocks until the device is idle so later CPU readback or teardown is safe.
|
|
void VulkanRenderer::flush()
|
|
{
|
|
if (device_ != VK_NULL_HANDLE) {
|
|
vkDeviceWaitIdle(device_);
|
|
}
|
|
}
|
|
|
|
// Clears a texture with vkCmdClearColorImage; null destinations are ignored
|
|
// because this backend has no implicit swapchain framebuffer.
|
|
void VulkanRenderer::clear_destination(olive::Texture *texture, double r,
|
|
double g, double b, double a)
|
|
{
|
|
QMutexLocker lock(&mutex_);
|
|
|
|
VkCommandBuffer cmd = begin_one_time_commands();
|
|
|
|
if (cmd == VK_NULL_HANDLE) {
|
|
return;
|
|
}
|
|
if (texture) {
|
|
quint64 id = texture->id().value<quint64>();
|
|
VulkanTexture *tex = textures_.value(id);
|
|
if (!tex) {
|
|
end_one_time_commands(cmd);
|
|
return;
|
|
}
|
|
if (tex->current_layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
|
|
transition_image_layout(cmd, tex->image, tex->current_layout,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
|
}
|
|
VkClearColorValue clear_color = {};
|
|
clear_color.float32[0] = static_cast<float>(r);
|
|
clear_color.float32[1] = static_cast<float>(g);
|
|
clear_color.float32[2] = static_cast<float>(b);
|
|
clear_color.float32[3] = static_cast<float>(a);
|
|
VkImageSubresourceRange range = {};
|
|
range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
range.baseMipLevel = 0;
|
|
range.levelCount = 1;
|
|
range.baseArrayLayer = 0;
|
|
range.layerCount = 1;
|
|
vkCmdClearColorImage(cmd, tex->image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear_color,
|
|
1, &range);
|
|
transition_image_layout(cmd, tex->image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
|
tex->current_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
}
|
|
end_one_time_commands(cmd);
|
|
}
|
|
|
|
// Reads one pixel by copying a 1x1 image region into a staging buffer.
|
|
Color VulkanRenderer::get_pixel_from_texture(olive::Texture *texture,
|
|
const QPointF &pt)
|
|
{
|
|
if (!texture) {
|
|
return Color();
|
|
}
|
|
int cpu_bytes_per_pixel = VideoParams::get_bytes_per_pixel(
|
|
texture->format(), texture->channel_count());
|
|
QByteArray data(cpu_bytes_per_pixel, Qt::Uninitialized);
|
|
|
|
quint64 id = texture->id().value<quint64>();
|
|
QMutexLocker lock(&mutex_);
|
|
VulkanTexture *tex = textures_.value(id);
|
|
if (!tex) {
|
|
return Color();
|
|
}
|
|
|
|
uint32_t px =
|
|
static_cast<uint32_t>(qBound(0.0, pt.x(), double(tex->width - 1)));
|
|
uint32_t py =
|
|
static_cast<uint32_t>(qBound(0.0, pt.y(), double(tex->height - 1)));
|
|
|
|
int gpu_bytes_per_pixel = get_vk_format_bytes_per_pixel(tex->vk_format);
|
|
if (gpu_bytes_per_pixel == 0) {
|
|
gpu_bytes_per_pixel = cpu_bytes_per_pixel;
|
|
}
|
|
|
|
VkBuffer staging_buffer;
|
|
VkDeviceMemory staging_memory;
|
|
if (!create_staging_buffer(gpu_bytes_per_pixel, &staging_buffer,
|
|
&staging_memory)) {
|
|
return Color();
|
|
}
|
|
|
|
VkCommandBuffer cmd = begin_one_time_commands();
|
|
|
|
if (cmd == VK_NULL_HANDLE) {
|
|
return Color();
|
|
}
|
|
if (tex->current_layout != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
|
|
transition_image_layout(cmd, tex->image, tex->current_layout,
|
|
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
|
}
|
|
copy_image_to_buffer(cmd, tex->image, staging_buffer, 1, 1, px, py);
|
|
end_one_time_commands(cmd);
|
|
|
|
void *mapped;
|
|
vkMapMemory(device_, staging_memory, 0, gpu_bytes_per_pixel, 0, &mapped);
|
|
if (cpu_bytes_per_pixel == gpu_bytes_per_pixel) {
|
|
memcpy(data.data(), mapped, static_cast<size_t>(cpu_bytes_per_pixel));
|
|
} else {
|
|
int gpu_channels = gpu_bytes_per_pixel /
|
|
VideoParams::get_bytes_per_channel(texture->format());
|
|
QByteArray gpu_pixel(gpu_bytes_per_pixel, Qt::Uninitialized);
|
|
memcpy(gpu_pixel.data(), mapped,
|
|
static_cast<size_t>(gpu_bytes_per_pixel));
|
|
copy_pixels_with_channel_conversion(gpu_pixel.constData(), data.data(), 1,
|
|
1, 1, gpu_channels,
|
|
texture->channel_count(),
|
|
texture->format());
|
|
}
|
|
vkUnmapMemory(device_, staging_memory);
|
|
|
|
destroy_staging_buffer(staging_buffer, staging_memory);
|
|
tex->current_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
|
|
|
return Color::from_data(data.data(), texture->format(),
|
|
texture->channel_count());
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Shader compilation (GLSL -> SPIR-V via shaderc)
|
|
// ------------------------------------------------------------------
|
|
|
|
// Returns true for GLSL sampler uniforms that must become explicit descriptors.
|
|
static bool is_sampler_type(const QString &type)
|
|
{
|
|
static const QRegularExpression sampler_re(
|
|
QStringLiteral(R"(sampler\d*D|samplerCube|sampler2DArray|sampler3D)"));
|
|
return sampler_re.match(type).hasMatch();
|
|
}
|
|
|
|
// Ensures GLSL has a Vulkan-compatible version directive before shaderc compiles
|
|
// it as GLSL 450.
|
|
QString VulkanRenderer::ensure_glsl_version450(const QString &glsl) const
|
|
{
|
|
QString result = glsl.trimmed();
|
|
if (result.startsWith(QStringLiteral("#version"))) {
|
|
int end = result.indexOf(QChar('\n'));
|
|
QString rest = (end >= 0) ? result.mid(end + 1) : QString();
|
|
return QStringLiteral("#version 450 core\n") + rest;
|
|
}
|
|
return QStringLiteral("#version 450 core\n") + result;
|
|
}
|
|
|
|
// Converts legacy Oak/OpenGL GLSL into Vulkan GLSL. The conversion keeps shader
|
|
// semantics but replaces implicit attributes/varyings and texture sampling with
|
|
// explicit layouts that Vulkan requires.
|
|
QString VulkanRenderer::convert_glsl_to_vulkan(const QString &glsl,
|
|
VkShaderStageFlagBits stage)
|
|
{
|
|
QString result = glsl;
|
|
|
|
// Olive uses the legacy texture2D/texture3D names in some shaders.
|
|
result.replace(QStringLiteral("texture2D("), QStringLiteral("texture("));
|
|
result.replace(QStringLiteral("texture3D("), QStringLiteral("texture("));
|
|
|
|
// Ensure fragment shader output has layout
|
|
if (stage == VK_SHADER_STAGE_FRAGMENT_BIT) {
|
|
result.replace(
|
|
QStringLiteral("out vec4 frag_color;"),
|
|
QStringLiteral("layout(location = 0) out vec4 frag_color;"));
|
|
result.replace(
|
|
QStringLiteral("in vec2 ove_texcoord;"),
|
|
QStringLiteral("layout(location = 0) in vec2 ove_texcoord;"));
|
|
}
|
|
|
|
// Add layout to vertex attributes and varyings
|
|
if (stage == VK_SHADER_STAGE_VERTEX_BIT) {
|
|
result.replace(
|
|
QStringLiteral("in vec4 a_position;"),
|
|
QStringLiteral("layout(location = 0) in vec4 a_position;"));
|
|
result.replace(
|
|
QStringLiteral("in vec2 a_texcoord;"),
|
|
QStringLiteral("layout(location = 1) in vec2 a_texcoord;"));
|
|
result.replace(
|
|
QStringLiteral("out vec2 ove_texcoord;"),
|
|
QStringLiteral("layout(location = 0) out vec2 ove_texcoord;"));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Returns the std140 storage size for scalar, vector, color, and matrix values.
|
|
VkDeviceSize VulkanRenderer::get_std140_size(const QString &type) const
|
|
{
|
|
if (type == QStringLiteral("float"))
|
|
return 4;
|
|
if (type == QStringLiteral("vec2"))
|
|
return 8;
|
|
if (type == QStringLiteral("vec3"))
|
|
return 12;
|
|
if (type == QStringLiteral("vec4"))
|
|
return 16;
|
|
if (type == QStringLiteral("mat4"))
|
|
return 64;
|
|
if (type == QStringLiteral("int") || type == QStringLiteral("bool"))
|
|
return 4;
|
|
return 4;
|
|
}
|
|
|
|
// Returns std140 base alignment so generated UBO offsets match GPU layout rules.
|
|
VkDeviceSize VulkanRenderer::get_std140_alignment(const QString &type) const
|
|
{
|
|
if (type == QStringLiteral("float"))
|
|
return 4;
|
|
if (type == QStringLiteral("vec2"))
|
|
return 8;
|
|
if (type == QStringLiteral("vec3"))
|
|
return 16;
|
|
if (type == QStringLiteral("vec4"))
|
|
return 16;
|
|
if (type == QStringLiteral("mat4"))
|
|
return 16;
|
|
if (type == QStringLiteral("int") || type == QStringLiteral("bool"))
|
|
return 4;
|
|
return 4;
|
|
}
|
|
|
|
// Scans GLSL uniform declarations and splits them into samplers and values. This
|
|
// is intentionally narrow and targets the shader style generated by Oak nodes.
|
|
void VulkanRenderer::extract_uniforms(const QString &glsl,
|
|
QVector<UniformInfo> *out_uniforms,
|
|
QVector<QString> *out_samplers) const
|
|
{
|
|
static const QRegularExpression re(
|
|
QStringLiteral(R"(^\s*uniform\s+(\w+)\s+(\w+)\s*;)"),
|
|
QRegularExpression::MultilineOption);
|
|
|
|
QRegularExpressionMatchIterator it = re.globalMatch(glsl);
|
|
while (it.hasNext()) {
|
|
QRegularExpressionMatch m = it.next();
|
|
QString type = m.captured(1);
|
|
QString name = m.captured(2);
|
|
|
|
if (is_sampler_type(type)) {
|
|
if (out_samplers && !out_samplers->contains(name)) {
|
|
out_samplers->append(name);
|
|
}
|
|
} else if (out_uniforms) {
|
|
bool exists = false;
|
|
for (const UniformInfo &u : *out_uniforms) {
|
|
if (u.name == name) {
|
|
exists = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!exists) {
|
|
UniformInfo info;
|
|
info.name = name;
|
|
info.type = type;
|
|
info.offset = 0;
|
|
info.size = 0;
|
|
out_uniforms->append(info);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Computes std140 offsets in declaration order and records the total UBO size.
|
|
void VulkanRenderer::compute_uniform_layout(QVector<UniformInfo> *uniforms) const
|
|
{
|
|
VkDeviceSize offset = 0;
|
|
for (UniformInfo &info : *uniforms) {
|
|
VkDeviceSize align = get_std140_alignment(info.type);
|
|
offset = align_size(offset, align);
|
|
info.offset = offset;
|
|
info.size = get_std140_size(info.type);
|
|
offset += info.size;
|
|
}
|
|
}
|
|
|
|
// Generates the uniform block source inserted into rewritten shaders.
|
|
QString
|
|
VulkanRenderer::build_ubo_block(const QVector<UniformInfo> &uniforms) const
|
|
{
|
|
if (uniforms.isEmpty()) {
|
|
return QString();
|
|
}
|
|
|
|
QString ubo = QStringLiteral(
|
|
"\nlayout(set = 0, binding = 0) uniform UniformBuffer {\n");
|
|
for (const UniformInfo &info : uniforms) {
|
|
ubo += QStringLiteral(" %1 %2;\n").arg(info.type, info.name);
|
|
}
|
|
ubo += QStringLiteral("} ubo;\n\n");
|
|
return ubo;
|
|
}
|
|
|
|
// Rewrites GLSL so non-sampler uniforms live in set=0,binding=0 and sampler
|
|
// uniforms get deterministic explicit bindings after the UBO.
|
|
QString VulkanRenderer::rewrite_shader_with_ubo(
|
|
const QString &glsl, const QVector<UniformInfo> &all_uniforms,
|
|
const QHash<QString, int> &sampler_bindings) const
|
|
{
|
|
QString result = glsl;
|
|
|
|
static const QRegularExpression re(
|
|
QStringLiteral(R"(^\s*uniform\s+(\w+)\s+(\w+)\s*;)"),
|
|
QRegularExpression::MultilineOption);
|
|
|
|
// First pass: replace sampler declarations with explicit bindings and remove
|
|
// plain uniform declarations. Process in reverse so indices stay valid.
|
|
QRegularExpressionMatchIterator it = re.globalMatch(result);
|
|
QVector<QRegularExpressionMatch> matches;
|
|
while (it.hasNext()) {
|
|
matches.append(it.next());
|
|
}
|
|
for (int i = matches.size() - 1; i >= 0; --i) {
|
|
const QRegularExpressionMatch &m = matches[i];
|
|
QString type = m.captured(1);
|
|
QString name = m.captured(2);
|
|
|
|
if (is_sampler_type(type)) {
|
|
int binding = sampler_bindings.value(name, -1);
|
|
if (binding >= 0) {
|
|
QString new_decl =
|
|
QStringLiteral(
|
|
"layout(set = 0, binding = %1) uniform %2 %3;")
|
|
.arg(binding)
|
|
.arg(type, name);
|
|
result.replace(m.capturedStart(), m.capturedLength(), new_decl);
|
|
}
|
|
} else {
|
|
result.remove(m.capturedStart(), m.capturedLength());
|
|
}
|
|
}
|
|
|
|
// Second pass: rewrite bare uniform names to ubo.name. The UBO block has not
|
|
// been inserted yet, so we cannot accidentally rewrite names inside it.
|
|
for (const UniformInfo &info : all_uniforms) {
|
|
QString old_name = QStringLiteral("\\b%1\\b").arg(info.name);
|
|
QString new_name = QStringLiteral("ubo.%1").arg(info.name);
|
|
result.replace(QRegularExpression(old_name), new_name);
|
|
}
|
|
|
|
// Third pass: insert the shared UBO block after the #version line.
|
|
if (!all_uniforms.isEmpty()) {
|
|
QString ubo = build_ubo_block(all_uniforms);
|
|
int version_end = result.indexOf(QChar('\n'));
|
|
if (version_end >= 0 && result.startsWith(QStringLiteral("#version"))) {
|
|
result.insert(version_end + 1, ubo);
|
|
} else {
|
|
result.prepend(ubo);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Compiles Vulkan GLSL into SPIR-V using shaderc. Without shaderc this backend
|
|
// can initialize but cannot create shaders.
|
|
bool VulkanRenderer::compile_glsl_to_spv(const QString &glsl,
|
|
VkShaderStageFlagBits stage,
|
|
QByteArray *out_spv)
|
|
{
|
|
#ifdef OAK_HAS_SHADERC
|
|
shaderc_compiler_t compiler = shaderc_compiler_initialize();
|
|
if (!compiler) {
|
|
qWarning() << "Failed to initialize shaderc compiler";
|
|
return false;
|
|
}
|
|
|
|
shaderc_compile_options_t options = shaderc_compile_options_initialize();
|
|
// Bindings are set explicitly by RewriteShaderWithUbo; do not let shaderc
|
|
// reassign them.
|
|
shaderc_compile_options_set_optimization_level(
|
|
options, shaderc_optimization_level_performance);
|
|
|
|
shaderc_shader_kind kind;
|
|
switch (stage) {
|
|
case VK_SHADER_STAGE_VERTEX_BIT:
|
|
kind = shaderc_glsl_vertex_shader;
|
|
break;
|
|
case VK_SHADER_STAGE_FRAGMENT_BIT:
|
|
kind = shaderc_glsl_fragment_shader;
|
|
break;
|
|
default:
|
|
shaderc_compile_options_release(options);
|
|
shaderc_compiler_release(compiler);
|
|
return false;
|
|
}
|
|
|
|
QString converted = convert_glsl_to_vulkan(glsl, stage);
|
|
QByteArray source_utf8 = converted.toUtf8();
|
|
|
|
shaderc_compilation_result_t compile_result = shaderc_compile_into_spv(
|
|
compiler, source_utf8.constData(), source_utf8.size(), kind,
|
|
"shader.glsl", "main", options);
|
|
|
|
shaderc_compile_options_release(options);
|
|
|
|
if (shaderc_result_get_compilation_status(compile_result) !=
|
|
shaderc_compilation_status_success) {
|
|
fprintf(stderr, "shaderc compilation failed: %s\n",
|
|
shaderc_result_get_error_message(compile_result));
|
|
shaderc_result_release(compile_result);
|
|
shaderc_compiler_release(compiler);
|
|
return false;
|
|
}
|
|
|
|
size_t spv_size = shaderc_result_get_length(compile_result);
|
|
const char *spv_data = shaderc_result_get_bytes(compile_result);
|
|
out_spv->resize(static_cast<int>(spv_size));
|
|
memcpy(out_spv->data(), spv_data, spv_size);
|
|
|
|
shaderc_result_release(compile_result);
|
|
shaderc_compiler_release(compiler);
|
|
return true;
|
|
#else
|
|
Q_UNUSED(glsl)
|
|
Q_UNUSED(stage)
|
|
Q_UNUSED(out_spv)
|
|
qWarning() << "shaderc not available, cannot compile GLSL to SPIR-V";
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
// Converts, compiles, and stores a shader pair plus descriptor metadata.
|
|
QVariant VulkanRenderer::create_native_shader(olive::ShaderCode code)
|
|
{
|
|
QMutexLocker lock(&mutex_);
|
|
|
|
QByteArray vert_spv;
|
|
QByteArray frag_spv;
|
|
|
|
QString vert_code = code.vert_code();
|
|
QString frag_code = code.frag_code();
|
|
|
|
// Use default shaders if empty
|
|
if (vert_code.isEmpty()) {
|
|
vert_code = FileFunctions::read_file_as_string(
|
|
QStringLiteral(":/shaders/default.vert"));
|
|
}
|
|
if (frag_code.isEmpty()) {
|
|
frag_code = FileFunctions::read_file_as_string(
|
|
QStringLiteral(":/shaders/default.frag"));
|
|
}
|
|
|
|
// Make sure both stages declare a Vulkan-compatible version.
|
|
vert_code = ensure_glsl_version450(vert_code);
|
|
frag_code = ensure_glsl_version450(frag_code);
|
|
|
|
// Extract uniforms and samplers from both stages. We build a single shared
|
|
// UBO layout and a single sampler binding table so both vertex and fragment
|
|
// shaders see the same descriptor set.
|
|
QVector<UniformInfo> vert_uniforms;
|
|
QVector<UniformInfo> frag_uniforms;
|
|
QVector<QString> vert_samplers;
|
|
QVector<QString> frag_samplers;
|
|
extract_uniforms(vert_code, &vert_uniforms, &vert_samplers);
|
|
extract_uniforms(frag_code, &frag_uniforms, &frag_samplers);
|
|
|
|
QVector<UniformInfo> all_uniforms = vert_uniforms;
|
|
for (const UniformInfo &fu : frag_uniforms) {
|
|
bool exists = false;
|
|
for (const UniformInfo &u : all_uniforms) {
|
|
if (u.name == fu.name) {
|
|
exists = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!exists) {
|
|
all_uniforms.append(fu);
|
|
}
|
|
}
|
|
compute_uniform_layout(&all_uniforms);
|
|
|
|
QVector<QString> all_samplers = vert_samplers;
|
|
for (const QString &name : frag_samplers) {
|
|
if (!all_samplers.contains(name)) {
|
|
all_samplers.append(name);
|
|
}
|
|
}
|
|
QHash<QString, int> sampler_bindings;
|
|
for (int i = 0; i < all_samplers.size(); ++i) {
|
|
sampler_bindings[all_samplers[i]] = 1 + i;
|
|
}
|
|
|
|
QString converted_vert =
|
|
rewrite_shader_with_ubo(vert_code, all_uniforms, sampler_bindings);
|
|
QString converted_frag =
|
|
rewrite_shader_with_ubo(frag_code, all_uniforms, sampler_bindings);
|
|
|
|
converted_vert =
|
|
convert_glsl_to_vulkan(converted_vert, VK_SHADER_STAGE_VERTEX_BIT);
|
|
converted_frag =
|
|
convert_glsl_to_vulkan(converted_frag, VK_SHADER_STAGE_FRAGMENT_BIT);
|
|
|
|
if (!compile_glsl_to_spv(converted_vert, VK_SHADER_STAGE_VERTEX_BIT,
|
|
&vert_spv)) {
|
|
fprintf(stderr, "Failed to compile Vulkan vertex shader:\n%s\n",
|
|
converted_vert.toUtf8().constData());
|
|
return QVariant();
|
|
}
|
|
if (!compile_glsl_to_spv(converted_frag, VK_SHADER_STAGE_FRAGMENT_BIT,
|
|
&frag_spv)) {
|
|
fprintf(stderr, "Failed to compile Vulkan fragment shader:\n%s\n",
|
|
converted_frag.toUtf8().constData());
|
|
return QVariant();
|
|
}
|
|
|
|
VulkanShader *sh = new VulkanShader();
|
|
sh->id = next_shader_id_++;
|
|
sh->uniforms = all_uniforms;
|
|
sh->sampler_count = all_samplers.size();
|
|
sh->sampler_bindings = sampler_bindings;
|
|
sh->ubo_size = 0;
|
|
for (const UniformInfo &u : all_uniforms) {
|
|
sh->ubo_size = qMax(sh->ubo_size, u.offset + u.size);
|
|
}
|
|
|
|
VkShaderModuleCreateInfo vert_info = {};
|
|
vert_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
|
|
vert_info.codeSize = static_cast<size_t>(vert_spv.size());
|
|
vert_info.pCode = reinterpret_cast<const uint32_t *>(vert_spv.constData());
|
|
|
|
VkResult result =
|
|
vkCreateShaderModule(device_, &vert_info, nullptr, &sh->vert_module);
|
|
if (result != VK_SUCCESS) {
|
|
delete sh;
|
|
return QVariant();
|
|
}
|
|
|
|
VkShaderModuleCreateInfo frag_info = {};
|
|
frag_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
|
|
frag_info.codeSize = static_cast<size_t>(frag_spv.size());
|
|
frag_info.pCode = reinterpret_cast<const uint32_t *>(frag_spv.constData());
|
|
|
|
result =
|
|
vkCreateShaderModule(device_, &frag_info, nullptr, &sh->frag_module);
|
|
if (result != VK_SUCCESS) {
|
|
vkDestroyShaderModule(device_, sh->vert_module, nullptr);
|
|
delete sh;
|
|
return QVariant();
|
|
}
|
|
|
|
// Create descriptor set layout: binding 0 = shared UBO, binding 1..N = samplers
|
|
QVector<VkDescriptorSetLayoutBinding> bindings;
|
|
bindings.reserve(1 + all_samplers.size());
|
|
|
|
if (!all_uniforms.isEmpty()) {
|
|
VkDescriptorSetLayoutBinding ubo_binding = {};
|
|
ubo_binding.binding = 0;
|
|
ubo_binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
ubo_binding.descriptorCount = 1;
|
|
ubo_binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT |
|
|
VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
bindings.append(ubo_binding);
|
|
}
|
|
|
|
for (int i = 0; i < all_samplers.size(); ++i) {
|
|
VkDescriptorSetLayoutBinding sampler_binding = {};
|
|
sampler_binding.binding = 1 + i;
|
|
sampler_binding.descriptorType =
|
|
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
sampler_binding.descriptorCount = 1;
|
|
sampler_binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT |
|
|
VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
bindings.append(sampler_binding);
|
|
}
|
|
|
|
VkDescriptorSetLayoutCreateInfo ds_layout_info = {};
|
|
ds_layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
|
|
ds_layout_info.bindingCount = static_cast<uint32_t>(bindings.size());
|
|
ds_layout_info.pBindings = bindings.constData();
|
|
|
|
result = vkCreateDescriptorSetLayout(device_, &ds_layout_info, nullptr,
|
|
&sh->descriptor_layout);
|
|
if (result != VK_SUCCESS) {
|
|
vkDestroyShaderModule(device_, sh->frag_module, nullptr);
|
|
vkDestroyShaderModule(device_, sh->vert_module, nullptr);
|
|
delete sh;
|
|
return QVariant();
|
|
}
|
|
|
|
VkPipelineLayoutCreateInfo layout_info = {};
|
|
layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
|
layout_info.setLayoutCount = 1;
|
|
layout_info.pSetLayouts = &sh->descriptor_layout;
|
|
|
|
result = vkCreatePipelineLayout(device_, &layout_info, nullptr,
|
|
&sh->pipeline_layout);
|
|
if (result != VK_SUCCESS) {
|
|
vkDestroyDescriptorSetLayout(device_, sh->descriptor_layout, nullptr);
|
|
vkDestroyShaderModule(device_, sh->frag_module, nullptr);
|
|
vkDestroyShaderModule(device_, sh->vert_module, nullptr);
|
|
delete sh;
|
|
return QVariant();
|
|
}
|
|
|
|
shaders_.insert(sh->id, sh);
|
|
return QVariant::fromValue(sh->id);
|
|
}
|
|
|
|
// Releases shader modules, descriptor layout, pipeline layout, and pipelines.
|
|
void VulkanRenderer::destroy_native_shader(QVariant shader)
|
|
{
|
|
QMutexLocker lock(&mutex_);
|
|
quint64 id = shader.value<quint64>();
|
|
VulkanShader *sh = shaders_.take(id);
|
|
if (!sh) {
|
|
return;
|
|
}
|
|
for (auto it = sh->pipeline_cache.begin(); it != sh->pipeline_cache.end();
|
|
++it) {
|
|
if (it.value() != VK_NULL_HANDLE) {
|
|
vkDestroyPipeline(device_, it.value(), nullptr);
|
|
}
|
|
}
|
|
sh->pipeline_cache.clear();
|
|
if (sh->pipeline_layout != VK_NULL_HANDLE) {
|
|
vkDestroyPipelineLayout(device_, sh->pipeline_layout, nullptr);
|
|
}
|
|
if (sh->descriptor_layout != VK_NULL_HANDLE) {
|
|
vkDestroyDescriptorSetLayout(device_, sh->descriptor_layout, nullptr);
|
|
}
|
|
if (sh->vert_module != VK_NULL_HANDLE) {
|
|
vkDestroyShaderModule(device_, sh->vert_module, nullptr);
|
|
}
|
|
if (sh->frag_module != VK_NULL_HANDLE) {
|
|
vkDestroyShaderModule(device_, sh->frag_module, nullptr);
|
|
}
|
|
delete sh;
|
|
}
|
|
|
|
// Creates a graphics pipeline for the destination render format. Viewport and
|
|
// scissor are dynamic so one pipeline can handle multiple target sizes.
|
|
bool VulkanRenderer::create_pipeline_for_shader(VulkanShader *shader,
|
|
const VideoParams &dest_params,
|
|
|
|
VkFormat render_pass_format)
|
|
{
|
|
if (shader->pipeline_cache.contains(render_pass_format)) {
|
|
return true;
|
|
}
|
|
|
|
VkPipelineShaderStageCreateInfo vert_stage = {};
|
|
vert_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
|
vert_stage.stage = VK_SHADER_STAGE_VERTEX_BIT;
|
|
vert_stage.module = shader->vert_module;
|
|
vert_stage.pName = "main";
|
|
|
|
VkPipelineShaderStageCreateInfo frag_stage = {};
|
|
frag_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
|
frag_stage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
frag_stage.module = shader->frag_module;
|
|
frag_stage.pName = "main";
|
|
|
|
VkPipelineShaderStageCreateInfo stages[] = { vert_stage, frag_stage };
|
|
|
|
VkPipelineVertexInputStateCreateInfo vertex_input = {};
|
|
vertex_input.sType =
|
|
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
|
|
|
|
VkVertexInputBindingDescription binding_desc = {};
|
|
binding_desc.binding = 0;
|
|
binding_desc.stride = 5 * sizeof(float);
|
|
binding_desc.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
|
|
|
VkVertexInputAttributeDescription attrs[2] = {};
|
|
attrs[0].binding = 0;
|
|
attrs[0].location = 0;
|
|
attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT;
|
|
attrs[0].offset = 0;
|
|
attrs[1].binding = 0;
|
|
attrs[1].location = 1;
|
|
attrs[1].format = VK_FORMAT_R32G32_SFLOAT;
|
|
attrs[1].offset = 3 * sizeof(float);
|
|
|
|
vertex_input.vertexBindingDescriptionCount = 1;
|
|
vertex_input.pVertexBindingDescriptions = &binding_desc;
|
|
vertex_input.vertexAttributeDescriptionCount = 2;
|
|
vertex_input.pVertexAttributeDescriptions = attrs;
|
|
|
|
VkPipelineInputAssemblyStateCreateInfo input_assembly = {};
|
|
input_assembly.sType =
|
|
VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
|
|
input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
|
|
|
VkViewport viewport = {};
|
|
viewport.width = static_cast<float>(dest_params.effective_width());
|
|
viewport.height = static_cast<float>(dest_params.effective_height());
|
|
viewport.minDepth = 0.0f;
|
|
viewport.maxDepth = 1.0f;
|
|
|
|
VkRect2D scissor = {};
|
|
scissor.extent.width = static_cast<uint32_t>(dest_params.effective_width());
|
|
scissor.extent.height =
|
|
static_cast<uint32_t>(dest_params.effective_height());
|
|
|
|
VkPipelineViewportStateCreateInfo viewport_state = {};
|
|
viewport_state.sType =
|
|
VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
|
|
viewport_state.viewportCount = 1;
|
|
viewport_state.pViewports = &viewport;
|
|
viewport_state.scissorCount = 1;
|
|
viewport_state.pScissors = &scissor;
|
|
|
|
VkPipelineRasterizationStateCreateInfo rasterizer = {};
|
|
rasterizer.sType =
|
|
VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
|
|
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
|
|
rasterizer.cullMode = VK_CULL_MODE_NONE;
|
|
rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
|
rasterizer.lineWidth = 1.0f;
|
|
|
|
VkPipelineMultisampleStateCreateInfo multisampling = {};
|
|
multisampling.sType =
|
|
VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
|
|
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
|
|
|
VkPipelineColorBlendAttachmentState color_blend = {};
|
|
color_blend.colorWriteMask =
|
|
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
|
|
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
|
|
color_blend.blendEnable = VK_FALSE;
|
|
|
|
VkPipelineColorBlendStateCreateInfo color_blending = {};
|
|
color_blending.sType =
|
|
VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
|
|
color_blending.attachmentCount = 1;
|
|
color_blending.pAttachments = &color_blend;
|
|
|
|
VkDynamicState dynamic_states[] = { VK_DYNAMIC_STATE_VIEWPORT,
|
|
VK_DYNAMIC_STATE_SCISSOR };
|
|
VkPipelineDynamicStateCreateInfo dynamic_state = {};
|
|
dynamic_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
|
|
dynamic_state.dynamicStateCount = 2;
|
|
dynamic_state.pDynamicStates = dynamic_states;
|
|
|
|
VkGraphicsPipelineCreateInfo pipeline_info = {};
|
|
pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
|
|
pipeline_info.stageCount = 2;
|
|
pipeline_info.pStages = stages;
|
|
pipeline_info.pVertexInputState = &vertex_input;
|
|
pipeline_info.pInputAssemblyState = &input_assembly;
|
|
pipeline_info.pViewportState = &viewport_state;
|
|
pipeline_info.pRasterizationState = &rasterizer;
|
|
pipeline_info.pMultisampleState = &multisampling;
|
|
pipeline_info.pColorBlendState = &color_blending;
|
|
pipeline_info.pDynamicState = &dynamic_state;
|
|
pipeline_info.layout = shader->pipeline_layout;
|
|
pipeline_info.renderPass = get_or_create_render_pass(render_pass_format, false);
|
|
pipeline_info.subpass = 0;
|
|
|
|
VkPipeline new_pipeline = VK_NULL_HANDLE;
|
|
VkResult result = vkCreateGraphicsPipelines(
|
|
device_, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &new_pipeline);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to create Vulkan graphics pipeline:" << result;
|
|
return false;
|
|
}
|
|
|
|
shader->pipeline_cache.insert(render_pass_format, new_pipeline);
|
|
return true;
|
|
}
|
|
|
|
// Executes one fullscreen draw pass. Texture descriptors and a transient UBO are
|
|
// allocated per pass so iterative shaders can update bindings cheaply.
|
|
void VulkanRenderer::blit_pass(VulkanShader *shader, VulkanTexture *dest_tex,
|
|
const QVector<TextureBinding> &bindings,
|
|
const QByteArray &ubo_data,
|
|
const VideoParams &destination_params,
|
|
bool clear_destination, int iteration)
|
|
{
|
|
(void)iteration;
|
|
|
|
if (!dest_tex) {
|
|
return;
|
|
}
|
|
|
|
VkFormat render_pass_format = dest_tex->vk_format;
|
|
VkRenderPass render_pass =
|
|
get_or_create_render_pass(render_pass_format, clear_destination);
|
|
if (render_pass == VK_NULL_HANDLE) {
|
|
return;
|
|
}
|
|
|
|
if (!create_pipeline_for_shader(shader, destination_params,
|
|
render_pass_format)) {
|
|
return;
|
|
}
|
|
|
|
VkPipeline pipeline = shader->pipeline_cache.value(render_pass_format);
|
|
|
|
// Lazily create a per-texture framebuffer. The framebuffer is compatible
|
|
// with any render pass that uses the same format and sample count, so we
|
|
// build it once with the non-clear variant and reuse it.
|
|
if (dest_tex->framebuffer == VK_NULL_HANDLE) {
|
|
VkFramebufferCreateInfo fb_info = {};
|
|
fb_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
|
fb_info.renderPass = get_or_create_render_pass(render_pass_format, false);
|
|
fb_info.attachmentCount = 1;
|
|
fb_info.pAttachments = &dest_tex->view;
|
|
fb_info.width = static_cast<uint32_t>(dest_tex->width);
|
|
fb_info.height = static_cast<uint32_t>(dest_tex->height);
|
|
fb_info.layers = 1;
|
|
VkResult fb_result = vkCreateFramebuffer(device_, &fb_info, nullptr,
|
|
&dest_tex->framebuffer);
|
|
if (fb_result != VK_SUCCESS) {
|
|
qWarning() << "Failed to create Vulkan framebuffer:" << fb_result;
|
|
return;
|
|
}
|
|
}
|
|
VkFramebuffer framebuffer = dest_tex->framebuffer;
|
|
|
|
// Create UBO buffer if needed
|
|
VkBuffer ubo_buffer = VK_NULL_HANDLE;
|
|
VkDeviceMemory ubo_memory = VK_NULL_HANDLE;
|
|
if (shader->ubo_size > 0 && !ubo_data.isEmpty()) {
|
|
if (create_staging_buffer(shader->ubo_size, &ubo_buffer, &ubo_memory)) {
|
|
void *mapped;
|
|
vkMapMemory(device_, ubo_memory, 0, shader->ubo_size, 0, &mapped);
|
|
memcpy(mapped, ubo_data.constData(),
|
|
static_cast<size_t>(shader->ubo_size));
|
|
vkUnmapMemory(device_, ubo_memory);
|
|
}
|
|
}
|
|
|
|
// Allocate and update descriptors before recording commands so we can bail
|
|
// out cleanly if descriptor allocation fails.
|
|
VkDescriptorSet descriptor_set = VK_NULL_HANDLE;
|
|
VkDescriptorBufferInfo buffer_info = {};
|
|
QVector<VkDescriptorImageInfo> image_infos;
|
|
bool descriptors_needed = (shader->ubo_size > 0 || !bindings.isEmpty());
|
|
if (descriptors_needed) {
|
|
if (descriptor_sets_since_reset_ >= k_max_descriptor_sets - 16) {
|
|
vkResetDescriptorPool(device_, descriptor_pool_, 0);
|
|
descriptor_sets_since_reset_ = 0;
|
|
}
|
|
|
|
VkDescriptorSetAllocateInfo ds_alloc = {};
|
|
ds_alloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
|
ds_alloc.descriptorPool = descriptor_pool_;
|
|
ds_alloc.descriptorSetCount = 1;
|
|
ds_alloc.pSetLayouts = &shader->descriptor_layout;
|
|
|
|
VkResult result =
|
|
vkAllocateDescriptorSets(device_, &ds_alloc, &descriptor_set);
|
|
if (result != VK_SUCCESS) {
|
|
qWarning() << "Failed to allocate Vulkan descriptor set:" << result;
|
|
if (ubo_buffer != VK_NULL_HANDLE) {
|
|
destroy_staging_buffer(ubo_buffer, ubo_memory);
|
|
}
|
|
return;
|
|
}
|
|
descriptor_sets_since_reset_++;
|
|
|
|
QVector<VkWriteDescriptorSet> writes;
|
|
|
|
// UBO binding
|
|
if (ubo_buffer != VK_NULL_HANDLE) {
|
|
buffer_info.buffer = ubo_buffer;
|
|
buffer_info.offset = 0;
|
|
buffer_info.range = shader->ubo_size;
|
|
|
|
VkWriteDescriptorSet write = {};
|
|
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
write.dstSet = descriptor_set;
|
|
write.dstBinding = 0;
|
|
write.dstArrayElement = 0;
|
|
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
write.descriptorCount = 1;
|
|
write.pBufferInfo = &buffer_info;
|
|
writes.append(write);
|
|
}
|
|
|
|
// Texture samplers binding
|
|
if (!bindings.isEmpty()) {
|
|
image_infos.reserve(qMin(bindings.size(), 16));
|
|
for (int i = 0; i < bindings.size() && i < 16; i++) {
|
|
const TextureBinding &tb = bindings.at(i);
|
|
VkDescriptorImageInfo img_info = {};
|
|
img_info.sampler = get_sampler(tb.interp);
|
|
img_info.imageView = tb.tex ? tb.tex->view : VK_NULL_HANDLE;
|
|
img_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
image_infos.append(img_info);
|
|
|
|
// Use the binding assigned to this sampler name when the shader
|
|
// was compiled. This keeps descriptor writes in sync with the
|
|
// rewritten layout() bindings even when job value iteration
|
|
// orders the samplers differently.
|
|
int binding = shader->sampler_bindings.value(tb.name, -1);
|
|
if (binding < 0) {
|
|
binding = 1 + i;
|
|
}
|
|
|
|
VkWriteDescriptorSet write = {};
|
|
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
write.dstSet = descriptor_set;
|
|
write.dstBinding = static_cast<uint32_t>(binding);
|
|
write.dstArrayElement = 0;
|
|
write.descriptorType =
|
|
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
write.descriptorCount = 1;
|
|
write.pImageInfo = &image_infos.last();
|
|
writes.append(write);
|
|
}
|
|
}
|
|
|
|
if (!writes.isEmpty()) {
|
|
vkUpdateDescriptorSets(device_, writes.size(), writes.constData(),
|
|
0, nullptr);
|
|
}
|
|
}
|
|
|
|
VkCommandBuffer cmd = begin_one_time_commands();
|
|
|
|
if (cmd == VK_NULL_HANDLE) {
|
|
return;
|
|
}
|
|
|
|
if (dest_tex->current_layout != VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
|
|
transition_image_layout(cmd, dest_tex->image, dest_tex->current_layout,
|
|
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
|
dest_tex->current_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
}
|
|
|
|
for (const TextureBinding &tb : bindings) {
|
|
if (tb.tex && tb.tex->current_layout !=
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
|
|
transition_image_layout(cmd, tb.tex->image, tb.tex->current_layout,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
|
tb.tex->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
}
|
|
}
|
|
|
|
VkRenderPassBeginInfo rp_begin = {};
|
|
rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
|
rp_begin.renderPass = render_pass;
|
|
rp_begin.framebuffer = framebuffer;
|
|
rp_begin.renderArea.extent.width =
|
|
static_cast<uint32_t>(destination_params.effective_width());
|
|
rp_begin.renderArea.extent.height =
|
|
static_cast<uint32_t>(destination_params.effective_height());
|
|
|
|
if (clear_destination) {
|
|
VkClearValue clear_val = {};
|
|
clear_val.color = { { 0.0f, 0.0f, 0.0f, 0.0f } };
|
|
rp_begin.clearValueCount = 1;
|
|
rp_begin.pClearValues = &clear_val;
|
|
}
|
|
|
|
vkCmdBeginRenderPass(cmd, &rp_begin, VK_SUBPASS_CONTENTS_INLINE);
|
|
|
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
|
|
|
// Set viewport
|
|
VkViewport viewport = {};
|
|
viewport.width = static_cast<float>(destination_params.effective_width());
|
|
viewport.height = static_cast<float>(destination_params.effective_height());
|
|
viewport.minDepth = 0.0f;
|
|
viewport.maxDepth = 1.0f;
|
|
vkCmdSetViewport(cmd, 0, 1, &viewport);
|
|
|
|
// Set scissor
|
|
VkRect2D scissor = {};
|
|
scissor.extent.width =
|
|
static_cast<uint32_t>(destination_params.effective_width());
|
|
scissor.extent.height =
|
|
static_cast<uint32_t>(destination_params.effective_height());
|
|
vkCmdSetScissor(cmd, 0, 1, &scissor);
|
|
|
|
// Bind vertex buffer
|
|
VkDeviceSize offsets[] = { 0 };
|
|
vkCmdBindVertexBuffers(cmd, 0, 1, &vertex_buffer_, offsets);
|
|
|
|
// Bind descriptor set
|
|
if (descriptor_set != VK_NULL_HANDLE) {
|
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
|
shader->pipeline_layout, 0, 1, &descriptor_set,
|
|
0, nullptr);
|
|
}
|
|
|
|
// Draw
|
|
vkCmdDraw(cmd, 6, 1, 0, 0);
|
|
|
|
vkCmdEndRenderPass(cmd);
|
|
|
|
// Leave the destination in a shader-readable state so it can be sampled or
|
|
// downloaded without an extra layout transition on the caller side.
|
|
transition_image_layout(cmd, dest_tex->image,
|
|
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
|
dest_tex->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
end_one_time_commands(cmd);
|
|
|
|
if (ubo_buffer != VK_NULL_HANDLE) {
|
|
destroy_staging_buffer(ubo_buffer, ubo_memory);
|
|
}
|
|
}
|
|
|
|
// Runs a shader job. Multi-iteration jobs ping-pong between temporary textures
|
|
// and replace the configured iterative input with the previous pass output.
|
|
void VulkanRenderer::blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
|
|
olive::Texture *destination,
|
|
VideoParams destination_params,
|
|
bool clear_destination)
|
|
{
|
|
ShaderJob *s_job = dynamic_cast<ShaderJob *>(&a_job);
|
|
if (!s_job) {
|
|
return;
|
|
}
|
|
ShaderJob job(*s_job);
|
|
|
|
quint64 shader_id = shader_variant.value<quint64>();
|
|
|
|
// Iterative shaders require ping-pong textures. Create them before locking
|
|
// the renderer mutex because CreateTexture also locks it.
|
|
int real_iteration_count = 1;
|
|
if (job.get_iteration_count() > 1 && !job.get_iterative_input().isEmpty()) {
|
|
real_iteration_count = job.get_iteration_count();
|
|
}
|
|
|
|
struct PingPongTexture {
|
|
TexturePtr texture;
|
|
VulkanTexture *native = nullptr;
|
|
};
|
|
|
|
PingPongTexture output_tex, input_tex, final_tex;
|
|
if (real_iteration_count > 1) {
|
|
output_tex.texture = create_texture(destination_params);
|
|
if (real_iteration_count > 2) {
|
|
input_tex.texture = create_texture(destination_params);
|
|
}
|
|
}
|
|
|
|
if (!destination) {
|
|
final_tex.texture = create_texture(destination_params);
|
|
}
|
|
|
|
QMutexLocker lock(&mutex_);
|
|
|
|
VulkanShader *shader = shaders_.value(shader_id);
|
|
if (!shader) {
|
|
return;
|
|
}
|
|
|
|
VulkanTexture *dest_tex = nullptr;
|
|
if (destination) {
|
|
quint64 dest_id = destination->id().value<quint64>();
|
|
dest_tex = textures_.value(dest_id);
|
|
if (!dest_tex) {
|
|
return;
|
|
}
|
|
} else {
|
|
quint64 final_id = final_tex.texture->id().value<quint64>();
|
|
final_tex.native = textures_.value(final_id);
|
|
if (!final_tex.native) {
|
|
qWarning()
|
|
<< "VulkanRenderer::Blit failed to resolve temporary destination texture";
|
|
return;
|
|
}
|
|
dest_tex = final_tex.native;
|
|
}
|
|
|
|
if (output_tex.texture) {
|
|
quint64 id = output_tex.texture->id().value<quint64>();
|
|
output_tex.native = textures_.value(id);
|
|
}
|
|
if (input_tex.texture) {
|
|
quint64 id = input_tex.texture->id().value<quint64>();
|
|
input_tex.native = textures_.value(id);
|
|
}
|
|
|
|
// Collect textures to bind and build base UBO data
|
|
QVector<TextureBinding> base_bindings;
|
|
QByteArray base_ubo_data;
|
|
if (shader->ubo_size > 0) {
|
|
base_ubo_data.resize(static_cast<int>(shader->ubo_size));
|
|
base_ubo_data.fill(0);
|
|
}
|
|
|
|
for (auto it = job.get_values().constBegin();
|
|
it != job.get_values().constEnd(); ++it) {
|
|
const NodeValue &value = it.value();
|
|
if (value.type() == NodeValue::k_texture) {
|
|
TexturePtr texture = value.to_texture();
|
|
VulkanTexture *vtex = nullptr;
|
|
if (texture) {
|
|
quint64 tid = texture->id().value<quint64>();
|
|
vtex = textures_.value(tid);
|
|
}
|
|
base_bindings.append(
|
|
{ it.key(), vtex, job.get_interpolation(it.key()) });
|
|
} else if (!shader->uniforms.isEmpty() && shader->ubo_size > 0) {
|
|
// Find matching uniform
|
|
for (const UniformInfo &u : shader->uniforms) {
|
|
if (u.name != it.key())
|
|
continue;
|
|
char *dst = base_ubo_data.data() + static_cast<int>(u.offset);
|
|
switch (value.type()) {
|
|
case NodeValue::k_float:
|
|
*reinterpret_cast<float *>(dst) =
|
|
static_cast<float>(value.to_double());
|
|
break;
|
|
case NodeValue::k_int:
|
|
*reinterpret_cast<int *>(dst) =
|
|
static_cast<int>(value.to_int());
|
|
break;
|
|
case NodeValue::k_boolean:
|
|
*reinterpret_cast<int *>(dst) = value.to_bool() ? 1 : 0;
|
|
break;
|
|
case NodeValue::k_vec2: {
|
|
QVector2D v = value.to_vec2();
|
|
memcpy(dst, &v, sizeof(float) * 2);
|
|
break;
|
|
}
|
|
case NodeValue::k_vec3: {
|
|
QVector3D v = value.to_vec3();
|
|
memcpy(dst, &v, sizeof(float) * 3);
|
|
break;
|
|
}
|
|
case NodeValue::k_vec4: {
|
|
QVector4D v = value.to_vec4();
|
|
memcpy(dst, &v, sizeof(float) * 4);
|
|
break;
|
|
}
|
|
case NodeValue::k_matrix: {
|
|
QMatrix4x4 m = value.to_matrix();
|
|
memcpy(dst, m.constData(), sizeof(float) * 16);
|
|
break;
|
|
}
|
|
case NodeValue::k_color: {
|
|
Color c = value.to_color();
|
|
float col[4] = { static_cast<float>(c.red()),
|
|
static_cast<float>(c.green()),
|
|
static_cast<float>(c.blue()),
|
|
static_cast<float>(c.alpha()) };
|
|
memcpy(dst, col, sizeof(float) * 4);
|
|
break;
|
|
}
|
|
case NodeValue::k_combo:
|
|
*reinterpret_cast<int *>(dst) = value.to_int();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle special uniforms that may not be in job values
|
|
if (shader->ubo_size > 0) {
|
|
for (const UniformInfo &u : shader->uniforms) {
|
|
char *dst = base_ubo_data.data() + static_cast<int>(u.offset);
|
|
if (u.name == QStringLiteral("ove_mvpmat")) {
|
|
QMatrix4x4 m = job.get(QStringLiteral("ove_mvpmat")).to_matrix();
|
|
memcpy(dst, m.constData(), sizeof(float) * 16);
|
|
} else if (u.name == QStringLiteral("ove_cropmatrix")) {
|
|
QMatrix4x4 m =
|
|
job.get(QStringLiteral("ove_cropmatrix")).to_matrix();
|
|
memcpy(dst, m.constData(), sizeof(float) * 16);
|
|
} else if (u.name == QStringLiteral("ove_maintex_alpha")) {
|
|
*reinterpret_cast<int *>(dst) =
|
|
job.get(QStringLiteral("ove_maintex_alpha")).to_int();
|
|
} else if (u.name == QStringLiteral("ove_force_opaque")) {
|
|
*reinterpret_cast<int *>(dst) =
|
|
job.get(QStringLiteral("ove_force_opaque")).to_bool() ? 1 :
|
|
0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set texture-enable flags for shaders that declare uniform bool NAME_enabled.
|
|
if (shader->ubo_size > 0) {
|
|
for (const TextureBinding &tb : base_bindings) {
|
|
QString enabled_name = tb.name + QStringLiteral("_enabled");
|
|
for (const UniformInfo &u : shader->uniforms) {
|
|
if (u.name == enabled_name && u.size == sizeof(int)) {
|
|
char *dst =
|
|
base_ubo_data.data() + static_cast<int>(u.offset);
|
|
*reinterpret_cast<int *>(dst) = tb.tex ? 1 : 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int iteration = 0; iteration < real_iteration_count; ++iteration) {
|
|
QVector<TextureBinding> pass_bindings = base_bindings;
|
|
QByteArray pass_ubo_data = base_ubo_data;
|
|
|
|
// Set iteration number
|
|
if (shader->ubo_size > 0) {
|
|
for (const UniformInfo &u : shader->uniforms) {
|
|
if (u.name == QStringLiteral("ove_iteration")) {
|
|
char *dst =
|
|
pass_ubo_data.data() + static_cast<int>(u.offset);
|
|
*reinterpret_cast<int *>(dst) = iteration;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Replace iterative input
|
|
VulkanTexture *pass_dest = dest_tex;
|
|
bool pass_clear = clear_destination;
|
|
if (iteration != real_iteration_count - 1) {
|
|
pass_dest = output_tex.native;
|
|
pass_clear = true;
|
|
}
|
|
|
|
if (iteration > 0) {
|
|
const QString &iterative_input = job.get_iterative_input();
|
|
for (TextureBinding &tb : pass_bindings) {
|
|
if (tb.name == iterative_input) {
|
|
tb.tex = input_tex.native;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
blit_pass(shader, pass_dest, pass_bindings, pass_ubo_data,
|
|
destination_params, pass_clear, iteration);
|
|
|
|
if (iteration != real_iteration_count - 1) {
|
|
std::swap(output_tex, input_tex);
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace olive
|