diff --git a/src/libutils/oglapphelpers/CMakeLists.txt b/src/libutils/oglapphelpers/CMakeLists.txt index cef50ede1..207caf849 100644 --- a/src/libutils/oglapphelpers/CMakeLists.txt +++ b/src/libutils/oglapphelpers/CMakeLists.txt @@ -31,6 +31,21 @@ if(APPLE) endif() +if(OCIO_VULKAN_ENABLED) + + find_package(Vulkan REQUIRED) + find_package(glslang REQUIRED) + + list(APPEND SOURCES + vulkanapp.cpp + ) + + list(APPEND INCLUDES + vulkanapp.h + ) + +endif() + add_library(oglapphelpers STATIC ${SOURCES}) set_target_properties(oglapphelpers PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(oglapphelpers PROPERTIES OUTPUT_NAME OpenColorIOoglapphelpers) @@ -111,6 +126,24 @@ if(APPLE) ) endif() +if(OCIO_VULKAN_ENABLED) + target_include_directories(oglapphelpers + PUBLIC + ${Vulkan_INCLUDE_DIRS} + ) + target_link_libraries(oglapphelpers + PUBLIC + Vulkan::Vulkan + glslang::glslang + glslang::glslang-default-resource-limits + glslang::SPIRV + ) + target_compile_definitions(oglapphelpers + PUBLIC + OCIO_VULKAN_ENABLED + ) +endif() + if(${OCIO_EGL_HEADLESS}) target_include_directories(oglapphelpers PRIVATE diff --git a/src/libutils/oglapphelpers/vulkanapp.cpp b/src/libutils/oglapphelpers/vulkanapp.cpp new file mode 100644 index 000000000..0c4363409 --- /dev/null +++ b/src/libutils/oglapphelpers/vulkanapp.cpp @@ -0,0 +1,782 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + +#ifdef OCIO_VULKAN_ENABLED + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "vulkanapp.h" + +namespace OCIO_NAMESPACE +{ + +// +// VulkanApp Implementation +// + +VulkanApp::VulkanApp(int bufWidth, int bufHeight) + : m_bufferWidth(bufWidth) + , m_bufferHeight(bufHeight) +{ + initVulkan(); +} + +VulkanApp::~VulkanApp() +{ + cleanup(); +} + +void VulkanApp::initVulkan() +{ + // Create Vulkan instance + VkApplicationInfo appInfo{}; + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "OCIO GPU Test"; + appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.pEngineName = "OCIO"; + appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.apiVersion = VK_API_VERSION_1_2; + + VkInstanceCreateInfo createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + createInfo.pApplicationInfo = &appInfo; + + // Required extensions for MoltenVK on macOS + std::vector extensions; +#ifdef __APPLE__ + extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + createInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; +#endif + + createInfo.enabledExtensionCount = static_cast(extensions.size()); + createInfo.ppEnabledExtensionNames = extensions.data(); + + if (m_enableValidationLayers) + { + createInfo.enabledLayerCount = static_cast(m_validationLayers.size()); + createInfo.ppEnabledLayerNames = m_validationLayers.data(); + } + else + { + createInfo.enabledLayerCount = 0; + } + + if (vkCreateInstance(&createInfo, nullptr, &m_instance) != VK_SUCCESS) + { + throw std::runtime_error("Failed to create Vulkan instance"); + } + + // Select physical device + uint32_t deviceCount = 0; + vkEnumeratePhysicalDevices(m_instance, &deviceCount, nullptr); + + if (deviceCount == 0) + { + throw std::runtime_error("Failed to find GPUs with Vulkan support"); + } + + std::vector devices(deviceCount); + vkEnumeratePhysicalDevices(m_instance, &deviceCount, devices.data()); + + // Find a device with compute queue support + for (const auto & device : devices) + { + uint32_t queueFamilyCount = 0; + vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); + + std::vector queueFamilies(queueFamilyCount); + vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); + + for (uint32_t i = 0; i < queueFamilyCount; i++) + { + if (queueFamilies[i].queueFlags & VK_QUEUE_COMPUTE_BIT) + { + m_physicalDevice = device; + m_computeQueueFamilyIndex = i; + break; + } + } + + if (m_physicalDevice != VK_NULL_HANDLE) + { + break; + } + } + + if (m_physicalDevice == VK_NULL_HANDLE) + { + throw std::runtime_error("Failed to find a suitable GPU with compute support"); + } + + // Create logical device + float queuePriority = 1.0f; + VkDeviceQueueCreateInfo queueCreateInfo{}; + queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queueCreateInfo.queueFamilyIndex = m_computeQueueFamilyIndex; + queueCreateInfo.queueCount = 1; + queueCreateInfo.pQueuePriorities = &queuePriority; + + VkPhysicalDeviceFeatures deviceFeatures{}; + + VkDeviceCreateInfo deviceCreateInfo{}; + deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo; + deviceCreateInfo.queueCreateInfoCount = 1; + deviceCreateInfo.pEnabledFeatures = &deviceFeatures; + deviceCreateInfo.enabledExtensionCount = 0; + + if (m_enableValidationLayers) + { + deviceCreateInfo.enabledLayerCount = static_cast(m_validationLayers.size()); + deviceCreateInfo.ppEnabledLayerNames = m_validationLayers.data(); + } + else + { + deviceCreateInfo.enabledLayerCount = 0; + } + + if (vkCreateDevice(m_physicalDevice, &deviceCreateInfo, nullptr, &m_device) != VK_SUCCESS) + { + throw std::runtime_error("Failed to create logical device"); + } + + vkGetDeviceQueue(m_device, m_computeQueueFamilyIndex, 0, &m_computeQueue); + + // Create command pool + VkCommandPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + poolInfo.queueFamilyIndex = m_computeQueueFamilyIndex; + poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + + if (vkCreateCommandPool(m_device, &poolInfo, nullptr, &m_commandPool) != VK_SUCCESS) + { + throw std::runtime_error("Failed to create command pool"); + } + + // Allocate command buffer + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.commandPool = m_commandPool; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandBufferCount = 1; + + if (vkAllocateCommandBuffers(m_device, &allocInfo, &m_commandBuffer) != VK_SUCCESS) + { + throw std::runtime_error("Failed to allocate command buffer"); + } + + m_initialized = true; +} + +void VulkanApp::cleanup() +{ + if (m_device != VK_NULL_HANDLE) + { + vkDeviceWaitIdle(m_device); + + // Destroy VulkanBuilder first (it holds shader module references) + m_vulkanBuilder.reset(); + + if (m_computePipeline != VK_NULL_HANDLE) + { + vkDestroyPipeline(m_device, m_computePipeline, nullptr); + } + if (m_pipelineLayout != VK_NULL_HANDLE) + { + vkDestroyPipelineLayout(m_device, m_pipelineLayout, nullptr); + } + if (m_descriptorSetLayout != VK_NULL_HANDLE) + { + vkDestroyDescriptorSetLayout(m_device, m_descriptorSetLayout, nullptr); + } + if (m_descriptorPool != VK_NULL_HANDLE) + { + vkDestroyDescriptorPool(m_device, m_descriptorPool, nullptr); + } + if (m_computeShaderModule != VK_NULL_HANDLE) + { + vkDestroyShaderModule(m_device, m_computeShaderModule, nullptr); + } + + if (m_inputBuffer != VK_NULL_HANDLE) + { + vkDestroyBuffer(m_device, m_inputBuffer, nullptr); + } + if (m_inputBufferMemory != VK_NULL_HANDLE) + { + vkFreeMemory(m_device, m_inputBufferMemory, nullptr); + } + if (m_outputBuffer != VK_NULL_HANDLE) + { + vkDestroyBuffer(m_device, m_outputBuffer, nullptr); + } + if (m_outputBufferMemory != VK_NULL_HANDLE) + { + vkFreeMemory(m_device, m_outputBufferMemory, nullptr); + } + if (m_stagingBuffer != VK_NULL_HANDLE) + { + vkDestroyBuffer(m_device, m_stagingBuffer, nullptr); + } + if (m_stagingBufferMemory != VK_NULL_HANDLE) + { + vkFreeMemory(m_device, m_stagingBufferMemory, nullptr); + } + + if (m_commandPool != VK_NULL_HANDLE) + { + vkDestroyCommandPool(m_device, m_commandPool, nullptr); + } + + vkDestroyDevice(m_device, nullptr); + } + + if (m_instance != VK_NULL_HANDLE) + { + vkDestroyInstance(m_instance, nullptr); + } +} + +uint32_t VulkanApp::findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) +{ + VkPhysicalDeviceMemoryProperties memProperties; + vkGetPhysicalDeviceMemoryProperties(m_physicalDevice, &memProperties); + + for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) + { + if ((typeFilter & (1 << i)) && + (memProperties.memoryTypes[i].propertyFlags & properties) == properties) + { + return i; + } + } + + throw std::runtime_error("Failed to find suitable memory type"); +} + +void VulkanApp::createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, + VkMemoryPropertyFlags properties, VkBuffer & buffer, + VkDeviceMemory & bufferMemory) +{ + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = size; + bufferInfo.usage = usage; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateBuffer(m_device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) + { + throw std::runtime_error("Failed to create buffer"); + } + + VkMemoryRequirements memRequirements; + vkGetBufferMemoryRequirements(m_device, buffer, &memRequirements); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); + + if (vkAllocateMemory(m_device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) + { + throw std::runtime_error("Failed to allocate buffer memory"); + } + + vkBindBufferMemory(m_device, buffer, bufferMemory, 0); +} + +void VulkanApp::copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) +{ + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + + vkBeginCommandBuffer(m_commandBuffer, &beginInfo); + + VkBufferCopy copyRegion{}; + copyRegion.size = size; + vkCmdCopyBuffer(m_commandBuffer, srcBuffer, dstBuffer, 1, ©Region); + + vkEndCommandBuffer(m_commandBuffer); + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &m_commandBuffer; + + vkQueueSubmit(m_computeQueue, 1, &submitInfo, VK_NULL_HANDLE); + vkQueueWaitIdle(m_computeQueue); + + vkResetCommandBuffer(m_commandBuffer, 0); +} + +void VulkanApp::initImage(int imageWidth, int imageHeight, Components comp, const float * imageBuffer) +{ + m_imageWidth = imageWidth; + m_imageHeight = imageHeight; + m_components = comp; + + createBuffers(); + updateImage(imageBuffer); +} + +void VulkanApp::createBuffers() +{ + const int numComponents = (m_components == COMPONENTS_RGB) ? 3 : 4; + const VkDeviceSize bufferSize = m_imageWidth * m_imageHeight * numComponents * sizeof(float); + + // Create staging buffer (CPU accessible) + createBuffer(bufferSize, + VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + m_stagingBuffer, m_stagingBufferMemory); + + // Create input buffer (GPU only) + createBuffer(bufferSize, + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + m_inputBuffer, m_inputBufferMemory); + + // Create output buffer (GPU only) + createBuffer(bufferSize, + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + m_outputBuffer, m_outputBufferMemory); +} + +void VulkanApp::updateImage(const float * imageBuffer) +{ + const int numComponents = (m_components == COMPONENTS_RGB) ? 3 : 4; + const VkDeviceSize bufferSize = m_imageWidth * m_imageHeight * numComponents * sizeof(float); + + // Copy data to staging buffer + void * data; + vkMapMemory(m_device, m_stagingBufferMemory, 0, bufferSize, 0, &data); + memcpy(data, imageBuffer, static_cast(bufferSize)); + vkUnmapMemory(m_device, m_stagingBufferMemory); + + // Copy from staging to input buffer + copyBuffer(m_stagingBuffer, m_inputBuffer, bufferSize); +} + +void VulkanApp::setShader(GpuShaderDescRcPtr & shaderDesc) +{ + if (!m_vulkanBuilder) + { + m_vulkanBuilder = std::make_shared(m_device); + } + + m_vulkanBuilder->buildShader(shaderDesc); + + if (m_printShader) + { + std::cout << "Vulkan Compute Shader:\n" << m_vulkanBuilder->getShaderSource() << std::endl; + } + + createComputePipeline(); +} + +void VulkanApp::createComputePipeline() +{ + // Create descriptor set layout + std::vector bindings = { + // Input buffer binding + {0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + // Output buffer binding + {1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr} + }; + + // Add texture bindings from shader builder + auto textureBindings = m_vulkanBuilder->getDescriptorSetLayoutBindings(); + bindings.insert(bindings.end(), textureBindings.begin(), textureBindings.end()); + + VkDescriptorSetLayoutCreateInfo layoutInfo{}; + layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layoutInfo.bindingCount = static_cast(bindings.size()); + layoutInfo.pBindings = bindings.data(); + + if (vkCreateDescriptorSetLayout(m_device, &layoutInfo, nullptr, &m_descriptorSetLayout) != VK_SUCCESS) + { + throw std::runtime_error("Failed to create descriptor set layout"); + } + + // Create pipeline layout + VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; + pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pipelineLayoutInfo.setLayoutCount = 1; + pipelineLayoutInfo.pSetLayouts = &m_descriptorSetLayout; + + if (vkCreatePipelineLayout(m_device, &pipelineLayoutInfo, nullptr, &m_pipelineLayout) != VK_SUCCESS) + { + throw std::runtime_error("Failed to create pipeline layout"); + } + + // Create compute pipeline + VkComputePipelineCreateInfo pipelineInfo{}; + pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; + pipelineInfo.layout = m_pipelineLayout; + pipelineInfo.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + pipelineInfo.stage.module = m_vulkanBuilder->getShaderModule(); + pipelineInfo.stage.pName = "main"; + + if (vkCreateComputePipelines(m_device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &m_computePipeline) != VK_SUCCESS) + { + throw std::runtime_error("Failed to create compute pipeline"); + } + + // Create descriptor pool + std::vector poolSizes = { + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2} + }; + + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.poolSizeCount = static_cast(poolSizes.size()); + poolInfo.pPoolSizes = poolSizes.data(); + poolInfo.maxSets = 1; + + if (vkCreateDescriptorPool(m_device, &poolInfo, nullptr, &m_descriptorPool) != VK_SUCCESS) + { + throw std::runtime_error("Failed to create descriptor pool"); + } + + // Allocate descriptor set + VkDescriptorSetAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = m_descriptorPool; + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = &m_descriptorSetLayout; + + if (vkAllocateDescriptorSets(m_device, &allocInfo, &m_descriptorSet) != VK_SUCCESS) + { + throw std::runtime_error("Failed to allocate descriptor set"); + } + + // Update descriptor set with buffer bindings + const int numComponents = (m_components == COMPONENTS_RGB) ? 3 : 4; + const VkDeviceSize bufferSize = m_imageWidth * m_imageHeight * numComponents * sizeof(float); + + VkDescriptorBufferInfo inputBufferInfo{}; + inputBufferInfo.buffer = m_inputBuffer; + inputBufferInfo.offset = 0; + inputBufferInfo.range = bufferSize; + + VkDescriptorBufferInfo outputBufferInfo{}; + outputBufferInfo.buffer = m_outputBuffer; + outputBufferInfo.offset = 0; + outputBufferInfo.range = bufferSize; + + std::vector descriptorWrites = { + {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, m_descriptorSet, 0, 0, 1, + VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &inputBufferInfo, nullptr}, + {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, m_descriptorSet, 1, 0, 1, + VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &outputBufferInfo, nullptr} + }; + + vkUpdateDescriptorSets(m_device, static_cast(descriptorWrites.size()), + descriptorWrites.data(), 0, nullptr); + + // Update texture bindings + m_vulkanBuilder->updateDescriptorSet(m_descriptorSet); +} + +void VulkanApp::reshape(int width, int height) +{ + m_bufferWidth = width; + m_bufferHeight = height; +} + +void VulkanApp::redisplay() +{ + // Record command buffer + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + + vkBeginCommandBuffer(m_commandBuffer, &beginInfo); + + vkCmdBindPipeline(m_commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, m_computePipeline); + vkCmdBindDescriptorSets(m_commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, + m_pipelineLayout, 0, 1, &m_descriptorSet, 0, nullptr); + + // Dispatch compute shader + const uint32_t groupCountX = (m_imageWidth + 15) / 16; + const uint32_t groupCountY = (m_imageHeight + 15) / 16; + vkCmdDispatch(m_commandBuffer, groupCountX, groupCountY, 1); + + vkEndCommandBuffer(m_commandBuffer); + + // Submit command buffer + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &m_commandBuffer; + + vkQueueSubmit(m_computeQueue, 1, &submitInfo, VK_NULL_HANDLE); + vkQueueWaitIdle(m_computeQueue); + + vkResetCommandBuffer(m_commandBuffer, 0); +} + +void VulkanApp::readImage(float * imageBuffer) +{ + const int numComponents = (m_components == COMPONENTS_RGB) ? 3 : 4; + const VkDeviceSize bufferSize = m_imageWidth * m_imageHeight * numComponents * sizeof(float); + + // Copy from output buffer to staging buffer + copyBuffer(m_outputBuffer, m_stagingBuffer, bufferSize); + + // Read from staging buffer + void * data; + vkMapMemory(m_device, m_stagingBufferMemory, 0, bufferSize, 0, &data); + memcpy(imageBuffer, data, static_cast(bufferSize)); + vkUnmapMemory(m_device, m_stagingBufferMemory); +} + +void VulkanApp::printVulkanInfo() const noexcept +{ + if (m_physicalDevice == VK_NULL_HANDLE) + { + std::cout << "Vulkan not initialized" << std::endl; + return; + } + + VkPhysicalDeviceProperties properties; + vkGetPhysicalDeviceProperties(m_physicalDevice, &properties); + + std::cout << "Vulkan Device: " << properties.deviceName << std::endl; + std::cout << "Vulkan API Version: " + << VK_VERSION_MAJOR(properties.apiVersion) << "." + << VK_VERSION_MINOR(properties.apiVersion) << "." + << VK_VERSION_PATCH(properties.apiVersion) << std::endl; + std::cout << "Driver Version: " << properties.driverVersion << std::endl; +} + +VulkanAppRcPtr VulkanApp::CreateVulkanApp(int bufWidth, int bufHeight) +{ + return std::make_shared(bufWidth, bufHeight); +} + +// +// VulkanBuilder Implementation +// + +VulkanBuilder::VulkanBuilder(VkDevice device) + : m_device(device) +{ +} + +VulkanBuilder::~VulkanBuilder() +{ + if (m_device != VK_NULL_HANDLE) + { + if (m_shaderModule != VK_NULL_HANDLE) + { + vkDestroyShaderModule(m_device, m_shaderModule, nullptr); + } + + for (auto & tex : m_textures) + { + if (tex.sampler != VK_NULL_HANDLE) + { + vkDestroySampler(m_device, tex.sampler, nullptr); + } + if (tex.imageView != VK_NULL_HANDLE) + { + vkDestroyImageView(m_device, tex.imageView, nullptr); + } + if (tex.image != VK_NULL_HANDLE) + { + vkDestroyImage(m_device, tex.image, nullptr); + } + if (tex.memory != VK_NULL_HANDLE) + { + vkFreeMemory(m_device, tex.memory, nullptr); + } + } + } +} + +void VulkanBuilder::buildShader(GpuShaderDescRcPtr & shaderDesc) +{ + // Generate GLSL compute shader source from OCIO shader description + std::ostringstream shader; + + shader << "#version 460\n"; + shader << "#extension GL_EXT_scalar_block_layout : enable\n"; + shader << "\n"; + shader << "layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;\n"; + shader << "\n"; + shader << "layout(std430, set = 0, binding = 0) readonly buffer InputBuffer {\n"; + shader << " vec4 inputPixels[];\n"; + shader << "};\n"; + shader << "\n"; + shader << "layout(std430, set = 0, binding = 1) writeonly buffer OutputBuffer {\n"; + shader << " vec4 outputPixels[];\n"; + shader << "};\n"; + shader << "\n"; + + // Add OCIO shader helper code (declarations and functions) + const char * shaderText = shaderDesc->getShaderText(); + if (shaderText && strlen(shaderText) > 0) + { + shader << shaderText << "\n"; + } + + shader << "\n"; + shader << "void main() {\n"; + shader << " uvec2 gid = gl_GlobalInvocationID.xy;\n"; + shader << " uint width = 256u;\n"; + shader << " uint height = 256u;\n"; + shader << " \n"; + shader << " // Bounds check to avoid out-of-bounds access\n"; + shader << " if (gid.x >= width || gid.y >= height) return;\n"; + shader << " \n"; + shader << " uint idx = gid.y * width + gid.x;\n"; + shader << " \n"; + shader << " vec4 " << shaderDesc->getPixelName() << " = inputPixels[idx];\n"; + shader << " \n"; + + // Call the OCIO color transformation function + const char * functionName = shaderDesc->getFunctionName(); + if (functionName && strlen(functionName) > 0) + { + shader << " " << shaderDesc->getPixelName() << " = " << functionName + << "(" << shaderDesc->getPixelName() << ");\n"; + } + + shader << " \n"; + shader << " outputPixels[idx] = " << shaderDesc->getPixelName() << ";\n"; + shader << "}\n"; + + m_shaderSource = shader.str(); + + // Compile GLSL to SPIR-V + std::vector spirvCode = compileGLSLToSPIRV(m_shaderSource); + + // Create shader module + VkShaderModuleCreateInfo createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + createInfo.codeSize = spirvCode.size() * sizeof(uint32_t); + createInfo.pCode = spirvCode.data(); + + if (vkCreateShaderModule(m_device, &createInfo, nullptr, &m_shaderModule) != VK_SUCCESS) + { + throw std::runtime_error("Failed to create shader module"); + } +} + +std::vector VulkanBuilder::compileGLSLToSPIRV(const std::string & glslSource) +{ + // Initialize glslang (safe to call multiple times) + static bool glslangInitialized = false; + if (!glslangInitialized) + { + glslang::InitializeProcess(); + glslangInitialized = true; + } + + // Create shader object + glslang::TShader shader(EShLangCompute); + + const char * shaderStrings[1] = { glslSource.c_str() }; + shader.setStrings(shaderStrings, 1); + + // Set up Vulkan 1.2 / SPIR-V 1.5 environment + shader.setEnvInput(glslang::EShSourceGlsl, EShLangCompute, glslang::EShClientVulkan, 460); + shader.setEnvClient(glslang::EShClientVulkan, glslang::EShTargetVulkan_1_2); + shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_5); + + // Get default resource limits + const TBuiltInResource * resources = GetDefaultResources(); + + // Parse the shader + const int defaultVersion = 460; + const bool forwardCompatible = false; + const EShMessages messages = static_cast(EShMsgSpvRules | EShMsgVulkanRules); + + if (!shader.parse(resources, defaultVersion, forwardCompatible, messages)) + { + std::string errorMsg = "GLSL parsing failed:\n"; + errorMsg += shader.getInfoLog(); + errorMsg += "\n"; + errorMsg += shader.getInfoDebugLog(); + throw std::runtime_error(errorMsg); + } + + // Create program and link + glslang::TProgram program; + program.addShader(&shader); + + if (!program.link(messages)) + { + std::string errorMsg = "GLSL linking failed:\n"; + errorMsg += program.getInfoLog(); + errorMsg += "\n"; + errorMsg += program.getInfoDebugLog(); + throw std::runtime_error(errorMsg); + } + + // Convert to SPIR-V + std::vector spirv; + glslang::SpvOptions spvOptions; + spvOptions.generateDebugInfo = false; + spvOptions.stripDebugInfo = true; + spvOptions.disableOptimizer = false; + spvOptions.optimizeSize = false; + + glslang::GlslangToSpv(*program.getIntermediate(EShLangCompute), spirv, &spvOptions); + + if (spirv.empty()) + { + throw std::runtime_error("SPIR-V generation produced empty output"); + } + + return spirv; +} + +void VulkanBuilder::allocateAllTextures(unsigned maxTextureSize) +{ + // TODO: Implement 3D LUT texture allocation for OCIO + // This would create VkImage, VkImageView, and VkSampler for each LUT +} + +std::vector VulkanBuilder::getDescriptorSetLayoutBindings() const +{ + std::vector bindings; + + // Add bindings for 3D LUT textures + // Starting at binding index 2 (0 and 1 are for input/output buffers) + uint32_t bindingIndex = 2; + for (size_t i = 0; i < m_textures.size(); ++i) + { + VkDescriptorSetLayoutBinding binding{}; + binding.binding = bindingIndex++; + binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + binding.pImmutableSamplers = nullptr; + bindings.push_back(binding); + } + + return bindings; +} + +void VulkanBuilder::updateDescriptorSet(VkDescriptorSet descriptorSet) +{ + // TODO: Update descriptor set with texture bindings + // This would write VkDescriptorImageInfo for each LUT texture +} + +} // namespace OCIO_NAMESPACE + +#endif // OCIO_VULKAN_ENABLED diff --git a/src/libutils/oglapphelpers/vulkanapp.h b/src/libutils/oglapphelpers/vulkanapp.h new file mode 100644 index 000000000..a742ff952 --- /dev/null +++ b/src/libutils/oglapphelpers/vulkanapp.h @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + + +#ifndef INCLUDED_OCIO_VULKANAPP_H +#define INCLUDED_OCIO_VULKANAPP_H + +#ifdef OCIO_VULKAN_ENABLED + +#include +#include +#include + +#include + +#include + +namespace OCIO_NAMESPACE +{ + +class VulkanBuilder; +typedef OCIO_SHARED_PTR VulkanBuilderRcPtr; + +class VulkanApp; +typedef OCIO_SHARED_PTR VulkanAppRcPtr; + +// VulkanApp provides headless Vulkan rendering for GPU unit testing. +// This class is designed to process images using OCIO GPU shaders via Vulkan compute pipelines. +class VulkanApp +{ +public: + VulkanApp() = delete; + VulkanApp(const VulkanApp &) = delete; + VulkanApp & operator=(const VulkanApp &) = delete; + + // Initialize the app with given buffer size for headless rendering. + VulkanApp(int bufWidth, int bufHeight); + + virtual ~VulkanApp(); + + enum Components + { + COMPONENTS_RGB = 0, + COMPONENTS_RGBA + }; + + // Initialize the image buffer. + void initImage(int imageWidth, int imageHeight, Components comp, const float * imageBuffer); + + // Update the image if it changes. + void updateImage(const float * imageBuffer); + + // Set the shader code from OCIO GpuShaderDesc. + void setShader(GpuShaderDescRcPtr & shaderDesc); + + // Update the size of the buffer used to process the image. + void reshape(int width, int height); + + // Process the image using the Vulkan compute pipeline. + void redisplay(); + + // Read the processed image from the GPU buffer. + void readImage(float * imageBuffer); + + // Print Vulkan device and instance info. + void printVulkanInfo() const noexcept; + + // Factory method to create a VulkanApp instance. + static VulkanAppRcPtr CreateVulkanApp(int bufWidth, int bufHeight); + + // Shader code will be printed when generated. + void setPrintShader(bool print) { m_printShader = print; } + +protected: + // Initialize Vulkan instance, device, and queues. + void initVulkan(); + + // Create Vulkan compute pipeline for shader processing. + void createComputePipeline(); + + // Create buffers for image data. + void createBuffers(); + + // Clean up Vulkan resources. + void cleanup(); + + // Helper to find a suitable memory type. + uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties); + + // Helper to create a Vulkan buffer. + void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, + VkMemoryPropertyFlags properties, VkBuffer & buffer, + VkDeviceMemory & bufferMemory); + + // Helper to copy buffer data. + void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size); + +private: + // Vulkan core objects + VkInstance m_instance{ VK_NULL_HANDLE }; + VkPhysicalDevice m_physicalDevice{ VK_NULL_HANDLE }; + VkDevice m_device{ VK_NULL_HANDLE }; + VkQueue m_computeQueue{ VK_NULL_HANDLE }; + uint32_t m_computeQueueFamilyIndex{ 0 }; + + // Command pool and buffer + VkCommandPool m_commandPool{ VK_NULL_HANDLE }; + VkCommandBuffer m_commandBuffer{ VK_NULL_HANDLE }; + + // Compute pipeline + VkPipelineLayout m_pipelineLayout{ VK_NULL_HANDLE }; + VkPipeline m_computePipeline{ VK_NULL_HANDLE }; + VkDescriptorSetLayout m_descriptorSetLayout{ VK_NULL_HANDLE }; + VkDescriptorPool m_descriptorPool{ VK_NULL_HANDLE }; + VkDescriptorSet m_descriptorSet{ VK_NULL_HANDLE }; + + // Shader module + VkShaderModule m_computeShaderModule{ VK_NULL_HANDLE }; + + // Image buffers + VkBuffer m_inputBuffer{ VK_NULL_HANDLE }; + VkDeviceMemory m_inputBufferMemory{ VK_NULL_HANDLE }; + VkBuffer m_outputBuffer{ VK_NULL_HANDLE }; + VkDeviceMemory m_outputBufferMemory{ VK_NULL_HANDLE }; + VkBuffer m_stagingBuffer{ VK_NULL_HANDLE }; + VkDeviceMemory m_stagingBufferMemory{ VK_NULL_HANDLE }; + + // Image dimensions + int m_imageWidth{ 0 }; + int m_imageHeight{ 0 }; + int m_bufferWidth{ 0 }; + int m_bufferHeight{ 0 }; + Components m_components{ COMPONENTS_RGBA }; + + // Shader builder + VulkanBuilderRcPtr m_vulkanBuilder; + + // Debug and configuration + bool m_printShader{ false }; + bool m_initialized{ false }; + + // Validation layers (debug builds) +#ifdef NDEBUG + const bool m_enableValidationLayers{ false }; +#else + const bool m_enableValidationLayers{ true }; +#endif + const std::vector m_validationLayers = { + "VK_LAYER_KHRONOS_validation" + }; +}; + +// VulkanBuilder handles OCIO shader compilation for Vulkan. +class VulkanBuilder +{ +public: + VulkanBuilder() = delete; + VulkanBuilder(const VulkanBuilder &) = delete; + VulkanBuilder & operator=(const VulkanBuilder &) = delete; + + explicit VulkanBuilder(VkDevice device); + ~VulkanBuilder(); + + // Build compute shader from OCIO GpuShaderDesc. + void buildShader(GpuShaderDescRcPtr & shaderDesc); + + // Get the compiled shader module. + VkShaderModule getShaderModule() const { return m_shaderModule; } + + // Get the shader source code (for debugging). + const std::string & getShaderSource() const { return m_shaderSource; } + + // Allocate and setup 3D LUT textures. + void allocateAllTextures(unsigned maxTextureSize); + + // Get descriptor set layout bindings for textures. + std::vector getDescriptorSetLayoutBindings() const; + + // Update descriptor set with texture bindings. + void updateDescriptorSet(VkDescriptorSet descriptorSet); + +private: + // Compile GLSL to SPIR-V. + std::vector compileGLSLToSPIRV(const std::string & glslSource); + + VkDevice m_device{ VK_NULL_HANDLE }; + VkShaderModule m_shaderModule{ VK_NULL_HANDLE }; + std::string m_shaderSource; + + // Texture resources for 3D LUTs + struct TextureResource + { + VkImage image{ VK_NULL_HANDLE }; + VkDeviceMemory memory{ VK_NULL_HANDLE }; + VkImageView imageView{ VK_NULL_HANDLE }; + VkSampler sampler{ VK_NULL_HANDLE }; + }; + std::vector m_textures; +}; + +} // namespace OCIO_NAMESPACE + +#endif // OCIO_VULKAN_ENABLED + +#endif // INCLUDED_OCIO_VULKANAPP_H diff --git a/tests/gpu/GPUUnitTest.cpp b/tests/gpu/GPUUnitTest.cpp index 650813121..81b6fd827 100644 --- a/tests/gpu/GPUUnitTest.cpp +++ b/tests/gpu/GPUUnitTest.cpp @@ -19,6 +19,9 @@ #if __APPLE__ #include "metalapp.h" #endif +#ifdef OCIO_VULKAN_ENABLED +#include "vulkanapp.h" +#endif namespace OCIO = OCIO_NAMESPACE; @@ -202,6 +205,16 @@ namespace app->initImage(g_winWidth, g_winHeight, OCIO::OglApp::COMPONENTS_RGBA, &image[0]); } +#ifdef OCIO_VULKAN_ENABLED + void AllocateImageTexture(OCIO::VulkanAppRcPtr & app) + { + const unsigned numEntries = g_winWidth * g_winHeight * g_components; + OCIOGPUTest::CustomValues::Values image(numEntries, 0.0f); + + app->initImage(g_winWidth, g_winHeight, OCIO::VulkanApp::COMPONENTS_RGBA, &image[0]); + } +#endif + void SetTestValue(float * image, float val, unsigned numComponents) { for (unsigned component = 0; component < numComponents; ++component) @@ -328,6 +341,114 @@ namespace app->updateImage(&values.m_inputValues[0]); } +#ifdef OCIO_VULKAN_ENABLED + void UpdateImageTexture(OCIO::VulkanAppRcPtr & app, OCIOGPUTestRcPtr & test) + { + // Note: User-specified custom values are padded out + // to the preferred size (g_winWidth x g_winHeight). + + const unsigned predefinedNumEntries = g_winWidth * g_winHeight * g_components; + + if (test->getCustomValues().m_inputValues.empty()) + { + // It means to generate the input values. + + const bool testNaN = false; + const bool testInfinity = false; + + float min = 0.0f; + float max = 1.0f; + if(test->getTestWideRange()) + { + test->getWideRangeInterval(min, max); + } + const float range = max - min; + + OCIOGPUTest::CustomValues tmp; + tmp.m_originalInputValueSize = predefinedNumEntries; + tmp.m_inputValues = OCIOGPUTest::CustomValues::Values(predefinedNumEntries, min); + + unsigned idx = 0; + unsigned numEntries = predefinedNumEntries; + const unsigned numTests = g_components * g_components; + if (testNaN) + { + const float qnan = std::numeric_limits::quiet_NaN(); + SetTestValue(&tmp.m_inputValues[0], qnan, g_components); + idx += numTests; + numEntries -= numTests; + } + + if (testInfinity) + { + const float posinf = std::numeric_limits::infinity(); + SetTestValue(&tmp.m_inputValues[idx], posinf, g_components); + idx += numTests; + numEntries -= numTests; + + const float neginf = -std::numeric_limits::infinity(); + SetTestValue(&tmp.m_inputValues[idx], neginf, g_components); + idx += numTests; + numEntries -= numTests; + } + + // Compute the value step based on the remaining number of values. + const float step = range / float(numEntries); + + for (unsigned int i=0; i < numEntries; ++i, ++idx) + { + tmp.m_inputValues[idx] = min + step * float(i); + } + + test->setCustomValues(tmp); + } + else + { + // It means to use the custom input values. + + const OCIOGPUTest::CustomValues::Values & existingInputValues + = test->getCustomValues().m_inputValues; + + const size_t numInputValues = existingInputValues.size(); + if (0 != (numInputValues%g_components)) + { + throw OCIO::Exception("Only the RGBA input values are supported"); + } + + test->getCustomValues().m_originalInputValueSize = numInputValues; + + if (numInputValues > predefinedNumEntries) + { + throw OCIO::Exception("Exceed the predefined texture maximum size"); + } + else if (numInputValues < predefinedNumEntries) + { + OCIOGPUTest::CustomValues values; + values.m_originalInputValueSize = existingInputValues.size(); + + // Resize the buffer to fit the expected input image size. + values.m_inputValues.resize(predefinedNumEntries, 0); + + for (size_t idx = 0; idx < numInputValues; ++idx) + { + values.m_inputValues[idx] = existingInputValues[idx]; + } + + test->setCustomValues(values); + } + } + + const OCIOGPUTest::CustomValues & values = test->getCustomValues(); + + if (predefinedNumEntries != values.m_inputValues.size()) + { + throw OCIO::Exception("Missing some expected input values"); + } + + app->updateImage(&values.m_inputValues[0]); + } +#endif + void UpdateOCIOGLState(OCIO::OglAppRcPtr & app, OCIOGPUTestRcPtr & test) { app->setPrintShader(test->isVerbose()); @@ -352,6 +473,32 @@ namespace app->setShader(shaderDesc); } +#ifdef OCIO_VULKAN_ENABLED + void UpdateOCIOVulkanState(OCIO::VulkanAppRcPtr & app, OCIOGPUTestRcPtr & test) + { + app->setPrintShader(test->isVerbose()); + + OCIO::ConstProcessorRcPtr & processor = test->getProcessor(); + OCIO::GpuShaderDescRcPtr & shaderDesc = test->getShaderDesc(); + + OCIO::ConstGPUProcessorRcPtr gpu; + if (test->isLegacyShader()) + { + gpu = processor->getOptimizedLegacyGPUProcessor(OCIO::OPTIMIZATION_DEFAULT, + test->getLegacyShaderLutEdge()); + } + else + { + gpu = processor->getDefaultGPUProcessor(); + } + + // Collect the shader program information for a specific processor. + gpu->extractGpuShaderInfo(shaderDesc); + + app->setShader(shaderDesc); + } +#endif + void DiffComponent(const std::vector & cpuImage, const std::vector & gpuImage, size_t idx, bool relativeTest, float expectMin, @@ -524,6 +671,146 @@ namespace test->updateMaxDiff(diff, idxDiff); } } + +#ifdef OCIO_VULKAN_ENABLED + // Validate the GPU processing against the CPU one for Vulkan. + void ValidateImageTexture(OCIO::VulkanAppRcPtr & app, OCIOGPUTestRcPtr & test) + { + // Each retest is rebuilding a cpu proc. + OCIO::ConstCPUProcessorRcPtr processor = test->getProcessor()->getDefaultCPUProcessor(); + + const float epsilon = test->getErrorThreshold(); + const float expectMinValue = test->getExpectedMinimalValue(); + + // Compute the width & height to avoid testing the padded values. + + const size_t numPixels = test->getCustomValues().m_originalInputValueSize / g_components; + + size_t width, height = 0; + if(numPixels<=g_winWidth) + { + width = numPixels; + height = 1; + } + else + { + width = g_winWidth; + height = numPixels/g_winWidth; + if((numPixels%g_winWidth)>0) height += 1; + } + + if(width==0 || width>g_winWidth || height==0 || height>g_winHeight) + { + throw OCIO::Exception("Mismatch with the expected image size"); + } + + // Step 1: Compute the output using the CPU engine. + + OCIOGPUTest::CustomValues::Values cpuImage = test->getCustomValues().m_inputValues; + OCIO::PackedImageDesc desc(&cpuImage[0], (long)width, (long)height, g_components); + processor->apply(desc); + + // Step 2: Grab the GPU output from the rendering buffer. + + OCIOGPUTest::CustomValues::Values gpuImage(g_winWidth*g_winHeight*g_components, 0.0f); + app->readImage(&gpuImage[0]); + + // Step 3: Compare the two results. + + const OCIOGPUTest::CustomValues::Values & image = test->getCustomValues().m_inputValues; + float diff = 0.0f; + size_t idxDiff = invalidIndex; + size_t idxNan = invalidIndex; + size_t idxInf = invalidIndex; + constexpr float huge = std::numeric_limits::max(); + float minVals[4] = {huge, huge, huge, huge}; + float maxVals[4] = {-huge, -huge, -huge, -huge}; + const bool relativeTest = test->getRelativeComparison(); + for(size_t idx=0; idx<(width*height); ++idx) + { + for(size_t chan=0; chan<4; ++chan) + { + DiffComponent(cpuImage, gpuImage, 4 * idx + chan, relativeTest, expectMinValue, + diff, idxDiff, idxInf, idxNan); + minVals[chan] = std::min(minVals[chan], + std::isinf(gpuImage[4 * idx + chan]) ? huge: gpuImage[4 * idx + chan]); + maxVals[chan] = std::max(maxVals[chan], + std::isinf(gpuImage[4 * idx + chan]) ? -huge: gpuImage[4 * idx + chan]); + } + } + + size_t componentIdx = idxDiff % 4; + size_t pixelIdx = idxDiff / 4; + if (diff > epsilon || idxInf != invalidIndex || idxNan != invalidIndex || test->isPrintMinMax()) + { + std::stringstream err; + err << std::setprecision(10); + err << "\n\nGPU max vals = {" + << maxVals[0] << ", " << maxVals[1] << ", " << maxVals[2] << ", " << maxVals[3] << "}\n" + << "GPU min vals = {" + << minVals[0] << ", " << minVals[1] << ", " << minVals[2] << ", " << minVals[3] << "}\n"; + + err << std::setprecision(10) + << "\nMaximum error: " << diff << " at pixel: " << pixelIdx + << " on component " << componentIdx; + if (diff > epsilon) + { + err << std::setprecision(10) + << " larger than epsilon.\nsrc = {" + << image[4 * pixelIdx + 0] << ", " << image[4 * pixelIdx + 1] << ", " + << image[4 * pixelIdx + 2] << ", " << image[4 * pixelIdx + 3] << "}" + << "\ncpu = {" + << cpuImage[4 * pixelIdx + 0] << ", " << cpuImage[4 * pixelIdx + 1] << ", " + << cpuImage[4 * pixelIdx + 2] << ", " << cpuImage[4 * pixelIdx + 3] << "}" + << "\ngpu = {" + << gpuImage[4 * pixelIdx + 0] << ", " << gpuImage[4 * pixelIdx + 1] << ", " + << gpuImage[4 * pixelIdx + 2] << ", " << gpuImage[4 * pixelIdx + 3] << "}\n" + << (test->getRelativeComparison() ? "relative " : "absolute ") + << "tolerance=" + << epsilon; + } + if (idxInf != invalidIndex) + { + componentIdx = idxInf % 4; + pixelIdx = idxInf / 4; + err << std::setprecision(10) + << "\nLarge number error: " << diff << " at pixel: " << pixelIdx + << " on component " << componentIdx + << ".\nsrc = {" + << image[4 * pixelIdx + 0] << ", " << image[4 * pixelIdx + 1] << ", " + << image[4 * pixelIdx + 2] << ", " << image[4 * pixelIdx + 3] << "}" + << "\ncpu = {" + << cpuImage[4 * pixelIdx + 0] << ", " << cpuImage[4 * pixelIdx + 1] << ", " + << cpuImage[4 * pixelIdx + 2] << ", " << cpuImage[4 * pixelIdx + 3] << "}" + << "\ngpu = {" + << gpuImage[4 * pixelIdx + 0] << ", " << gpuImage[4 * pixelIdx + 1] << ", " + << gpuImage[4 * pixelIdx + 2] << ", " << gpuImage[4 * pixelIdx + 3] << "}\n"; + } + if (idxNan != invalidIndex) + { + componentIdx = idxNan % 4; + pixelIdx = idxNan / 4; + err << std::setprecision(10) + << "\nNAN error: " << diff << " at pixel: " << pixelIdx + << " on component " << componentIdx + << ".\nsrc = {" + << image[4 * pixelIdx + 0] << ", " << image[4 * pixelIdx + 1] << ", " + << image[4 * pixelIdx + 2] << ", " << image[4 * pixelIdx + 3] << "}" + << "\ncpu = {" + << cpuImage[4 * pixelIdx + 0] << ", " << cpuImage[4 * pixelIdx + 1] << ", " + << cpuImage[4 * pixelIdx + 2] << ", " << cpuImage[4 * pixelIdx + 3] << "}" + << "\ngpu = {" + << gpuImage[4 * pixelIdx + 0] << ", " << gpuImage[4 * pixelIdx + 1] << ", " + << gpuImage[4 * pixelIdx + 2] << ", " << gpuImage[4 * pixelIdx + 3] << "}\n"; + } + throw OCIO::Exception(err.str().c_str()); + } + else + { + test->updateMaxDiff(diff, idxDiff); + } + } +#endif }; int main(int argc, const char ** argv) @@ -536,6 +823,7 @@ int main(int argc, const char ** argv) bool printHelp = false; bool useMetalRenderer = false; + bool useVulkanRenderer = false; bool verbose = false; bool stopOnFirstError = false; @@ -546,6 +834,7 @@ int main(int argc, const char ** argv) ap.options("\nCommand line arguments:\n", "--help", &printHelp, "Print help message", "--metal", &useMetalRenderer, "Run the GPU unit test with Metal", + "--vulkan", &useVulkanRenderer, "Run the GPU unit test with Vulkan", "-v", &verbose, "Output the GPU shader program", "--stop_on_error", &stopOnFirstError, "Stop on the first error", "--run_only %s", &filter, "Run only some unit tests\n" @@ -589,6 +878,9 @@ int main(int argc, const char ** argv) // Step 1: Initialize the graphic library engines. OCIO::OglAppRcPtr app; +#ifdef OCIO_VULKAN_ENABLED + OCIO::VulkanAppRcPtr vulkanApp; +#endif try { @@ -599,6 +891,16 @@ int main(int argc, const char ** argv) #else std::cerr << std::endl << "'GPU tests - Metal' is not supported" << std::endl; return 1; +#endif + } + else if(useVulkanRenderer) + { +#ifdef OCIO_VULKAN_ENABLED + vulkanApp = OCIO::VulkanApp::CreateVulkanApp(g_winWidth, g_winHeight); + vulkanApp->printVulkanInfo(); +#else + std::cerr << std::endl << "'GPU tests - Vulkan' is not supported (OCIO_VULKAN_ENABLED not defined)" << std::endl; + return 1; #endif } else @@ -611,16 +913,34 @@ int main(int argc, const char ** argv) std::cerr << std::endl << e.what() << std::endl; return 1; } + catch (const std::exception & e) + { + std::cerr << std::endl << e.what() << std::endl; + return 1; + } - app->printGLInfo(); + if (!useVulkanRenderer) + { + app->printGLInfo(); + } // Step 2: Allocate the texture that holds the image. - AllocateImageTexture(app); +#ifdef OCIO_VULKAN_ENABLED + if (useVulkanRenderer) + { + AllocateImageTexture(vulkanApp); + vulkanApp->reshape(g_winWidth, g_winHeight); + } + else +#endif + { + AllocateImageTexture(app); - // Step 3: Create the frame buffer and render buffer. - app->createGLBuffers(); + // Step 3: Create the frame buffer and render buffer. + app->createGLBuffers(); - app->reshape(g_winWidth, g_winHeight); + app->reshape(g_winWidth, g_winHeight); + } // Step 4: Execute all the unit tests. @@ -661,12 +981,18 @@ int main(int argc, const char ** argv) // Prepare the unit test. test->setVerbose(verbose); - test->setShadingLanguage( + OCIO::GpuLanguage gpuLang = OCIO::GPU_LANGUAGE_GLSL_1_2; #if __APPLE__ - useMetalRenderer ? - OCIO::GPU_LANGUAGE_MSL_2_0 : + if (useMetalRenderer) + { + gpuLang = OCIO::GPU_LANGUAGE_MSL_2_0; + } #endif - OCIO::GPU_LANGUAGE_GLSL_1_2); + if (useVulkanRenderer) + { + gpuLang = OCIO::GPU_LANGUAGE_GLSL_VK_4_6; + } + test->setShadingLanguage(gpuLang); bool enabledTest = true; try @@ -693,28 +1019,59 @@ int main(int argc, const char ** argv) if(test->isValid() && enabledTest) { - // Initialize the texture with the RGBA values to be processed. - UpdateImageTexture(app, test); +#ifdef OCIO_VULKAN_ENABLED + if (useVulkanRenderer) + { + // Initialize the texture with the RGBA values to be processed. + UpdateImageTexture(vulkanApp, test); - // Update the GPU shader program. - UpdateOCIOGLState(app, test); + // Update the GPU shader program. + UpdateOCIOVulkanState(vulkanApp, test); - const size_t numRetest = test->getNumRetests(); - // Need to run once and for each retest. - for (size_t idxRetest = 0; idxRetest <= numRetest; ++idxRetest) - { - if (idxRetest != 0) // Skip first run. + const size_t numRetest = test->getNumRetests(); + // Need to run once and for each retest. + for (size_t idxRetest = 0; idxRetest <= numRetest; ++idxRetest) { - // Call the retest callback. - test->retestSetup(idxRetest - 1); + if (idxRetest != 0) // Skip first run. + { + // Call the retest callback. + test->retestSetup(idxRetest - 1); + } + + // Process the image texture into the rendering buffer. + vulkanApp->redisplay(); + + // Compute the expected values using the CPU and compare + // against the GPU values. + ValidateImageTexture(vulkanApp, test); } + } + else +#endif + { + // Initialize the texture with the RGBA values to be processed. + UpdateImageTexture(app, test); - // Process the image texture into the rendering buffer. - app->redisplay(); + // Update the GPU shader program. + UpdateOCIOGLState(app, test); - // Compute the expected values using the CPU and compare - // against the GPU values. - ValidateImageTexture(app, test); + const size_t numRetest = test->getNumRetests(); + // Need to run once and for each retest. + for (size_t idxRetest = 0; idxRetest <= numRetest; ++idxRetest) + { + if (idxRetest != 0) // Skip first run. + { + // Call the retest callback. + test->retestSetup(idxRetest - 1); + } + + // Process the image texture into the rendering buffer. + app->redisplay(); + + // Compute the expected values using the CPU and compare + // against the GPU values. + ValidateImageTexture(app, test); + } } } }