#define PVR_REGS_FOR_RENDERER // see hw/pvr/pvr_regs.h /* Created on: Oct 2, 2019 Copyright 2019 flyinghead This file is part of Flycast. Flycast is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. Flycast is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ #include "vulkan_context.h" #include "rend/hdr/hdr_output.h" #include "shaders.h" #include "commandpool.h" #include "rend/ssaa_pattern.h" #include #include "hw/pvr/Renderer_if.h" VulkanContext *VulkanContext::contextInstance; static const char *PipelineCacheFileName = "vulkan_pipeline.cache"; const VkApplicationInfo* VkGetApplicationInfo() { // apiVersion is the maximum vulkan version supported by the app static vk::ApplicationInfo applicationInfo("Flycast", 1, "Flycast", 1, VK_API_VERSION_1_1); return &(VkApplicationInfo&)applicationInfo; } static bool createDevice(retro_vulkan_context* context, VkInstance instance, VkPhysicalDevice gpu, VkSurfaceKHR surface, PFN_vkGetInstanceProcAddr get_instance_proc_addr, const char** required_device_extensions, unsigned num_required_device_extensions, const char** required_device_layers, unsigned num_required_device_layers, const VkPhysicalDeviceFeatures* required_features, retro_vulkan_create_device_wrapper_t create_device_wrapper, void *opaque) { vulkan_symbol_wrapper_init(get_instance_proc_addr); vulkan_symbol_wrapper_load_global_symbols(); vulkan_symbol_wrapper_load_core_symbols(instance); VULKAN_SYMBOL_WRAPPER_LOAD_INSTANCE_EXTENSION_SYMBOL(instance, vkGetPhysicalDeviceSurfaceSupportKHR); vk::PhysicalDevice physicalDevice(gpu); if (gpu == VK_NULL_HANDLE) { // Choose a discrete gpu if there's one, otherwise just pick the first one vk::Instance vkinstance(instance); const auto devices = vkinstance.enumeratePhysicalDevices(); for (const auto& phyDev : devices) { vk::PhysicalDeviceProperties props; phyDev.getProperties(&props); if (props.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) { physicalDevice = phyDev; break; } } if (!physicalDevice) physicalDevice = vkinstance.enumeratePhysicalDevices().front(); } context->gpu = (VkPhysicalDevice)physicalDevice; std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); // get the first index into queueFamilyProperties which supports graphics and compute context->queue_family_index = (u32)std::distance(queueFamilyProperties.begin(), std::find_if(queueFamilyProperties.begin(), queueFamilyProperties.end(), [](vk::QueueFamilyProperties const& qfp) { return (qfp.queueFlags & (vk::QueueFlagBits::eGraphics | vk::QueueFlagBits::eCompute)) == (vk::QueueFlagBits::eGraphics | vk::QueueFlagBits::eCompute); })); verify(context->queue_family_index < queueFamilyProperties.size()); if (surface != VK_NULL_HANDLE) { // determine a queue family index that supports present // first check if the queue_family_index is good enough vk::SurfaceKHR vksurface(surface); context->presentation_queue_family_index = physicalDevice.getSurfaceSupportKHR(context->queue_family_index, vksurface) ? context->queue_family_index : queueFamilyProperties.size(); if (context->presentation_queue_family_index == queueFamilyProperties.size()) { // the queue_family_index doesn't support present -> look for an other family index that supports both graphics, compute and present for (size_t i = 0; i < queueFamilyProperties.size(); i++) { if ((queueFamilyProperties[i].queueFlags & (vk::QueueFlagBits::eGraphics | vk::QueueFlagBits::eCompute)) == (vk::QueueFlagBits::eGraphics | vk::QueueFlagBits::eCompute) && physicalDevice.getSurfaceSupportKHR((u32)i, vksurface)) { context->queue_family_index = (u32)i; context->presentation_queue_family_index = (u32)i; break; } } if (context->presentation_queue_family_index == queueFamilyProperties.size()) { // there's nothing like a single family index that supports both graphics/compute and present -> look for an other family index that supports present DEBUG_LOG(RENDERER, "Using separate Graphics and Present queue families"); for (size_t i = 0; i < queueFamilyProperties.size(); i++) { if (physicalDevice.getSurfaceSupportKHR((u32)i, vksurface)) { context->presentation_queue_family_index = (u32)i; break; } } } } if (context->queue_family_index == queueFamilyProperties.size() || context->presentation_queue_family_index == queueFamilyProperties.size()) { ERROR_LOG(RENDERER, "Could not find a queue for graphics or present"); return false; } if (context->queue_family_index == context->presentation_queue_family_index) DEBUG_LOG(RENDERER, "Using Graphics+Present queue family"); else DEBUG_LOG(RENDERER, "Using distinct Graphics and Present queue families"); } vk::PhysicalDeviceFeatures supportedFeatures; physicalDevice.getFeatures(&supportedFeatures); bool fragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics; VulkanContext::Instance()->samplerAnisotropy = supportedFeatures.samplerAnisotropy; VulkanContext::Instance()->storageImageExtendedFormats = supportedFeatures.shaderStorageImageExtendedFormats; VulkanContext::Instance()->multiDrawIndirect = supportedFeatures.multiDrawIndirect; VulkanContext::Instance()->drawIndirectFirstInstance = supportedFeatures.drawIndirectFirstInstance; VulkanContext::Instance()->drawIndirectCount = false; VulkanContext::Instance()->postDepthCoverage = false; // (SSAA's shading per sample: see VulkanContext::SsaaSamples()) VulkanContext::Instance()->sampleRateShading = supportedFeatures.sampleRateShading; // (the compute renderer's edge arithmetic, and its array of textures: vulkan/compute_drawer.h) VulkanContext::Instance()->shaderInt64 = supportedFeatures.shaderInt64; VulkanContext::Instance()->sampledImageArrayIndexing = supportedFeatures.shaderSampledImageArrayDynamicIndexing; // Enable VK_KHR_dedicated_allocation if available bool getMemReq2Supported = false; bool hasEds = false; bool hasEds3 = false; bool hasCacheControl = false; bool hasDescriptorIndexing = false; VulkanContext::Instance()->dedicatedAllocationSupported = false; std::vector deviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; for (int i = 0; i < num_required_device_extensions; i++) deviceExtensions.push_back(required_device_extensions[i]); for (const auto& property : physicalDevice.enumerateDeviceExtensionProperties()) { if (!strcmp(property.extensionName, VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME)) { deviceExtensions.push_back(VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME); getMemReq2Supported = true; } else if (!strcmp(property.extensionName, VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME)) { deviceExtensions.push_back(VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME); VulkanContext::Instance()->dedicatedAllocationSupported = true; } else if (!strcmp(property.extensionName, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME)) { // (GPU-side TA processing's translucent draws: see TaDecoder) deviceExtensions.push_back(VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME); VulkanContext::Instance()->drawIndirectCount = true; } else if (!strcmp(property.extensionName, VK_EXT_POST_DEPTH_COVERAGE_EXTENSION_NAME)) { // (SSAA: the per-pixel renderer's lists take the samples the depth test passed) deviceExtensions.push_back(VK_EXT_POST_DEPTH_COVERAGE_EXTENSION_NAME); VulkanContext::Instance()->postDepthCoverage = true; } else if (!strcmp(property.extensionName, "VK_EXT_extended_dynamic_state")) hasEds = true; else if (!strcmp(property.extensionName, "VK_EXT_extended_dynamic_state3")) hasEds3 = true; else if (!strcmp(property.extensionName, "VK_EXT_pipeline_creation_cache_control")) hasCacheControl = true; else if (!strcmp(property.extensionName, VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME)) hasDescriptorIndexing = true; } VulkanContext::Instance()->dedicatedAllocationSupported &= getMemReq2Supported; /* What the general pipelines are drawn with (pso.h): the depth, * stencil, cull and topology states, and the blend ones, set with each * draw; and pipeline caches the core keeps to one thread each, which * the driver then takes no lock for. Asked of the device's features * first: an extension can be there with the feature off. */ pso_ext::EdsFeatures eds = {}; pso_ext::Eds3Features eds3 = {}; pso_ext::CacheControlFeatures cacheControl = {}; eds.sType = pso_ext::STRUCTURE_TYPE_EDS_FEATURES; eds3.sType = pso_ext::STRUCTURE_TYPE_EDS3_FEATURES; cacheControl.sType = pso_ext::STRUCTURE_TYPE_CACHE_CONTROL_FEATURES; VulkanContext::Instance()->extendedDynamicState = false; VulkanContext::Instance()->extendedDynamicState3 = false; VulkanContext::Instance()->pipelineCacheControl = false; /* The compute renderer's array of textures indexed by a slot that * differs between the lanes of a subgroup (vulkan/compute_drawer.h) */ VkPhysicalDeviceDescriptorIndexingFeaturesEXT descriptorIndexing = {}; descriptorIndexing.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT; VulkanContext::Instance()->nonUniformTextureIndexing = false; vk::PhysicalDeviceProperties deviceProperties; physicalDevice.getProperties(&deviceProperties); void *featuresChain = nullptr; if ((hasEds || hasEds3 || hasCacheControl || hasDescriptorIndexing) && ::vkGetPhysicalDeviceFeatures2 != nullptr && VK_VERSION_MINOR(deviceProperties.apiVersion) >= 1) { eds.pNext = &eds3; eds3.pNext = &cacheControl; VkPhysicalDeviceFeatures2 features2 = {}; features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; features2.pNext = &eds; // (structures of extensions the device does not have are not to be in the chain) void **link = &features2.pNext; if (!hasEds) *link = eds.pNext; else link = &eds.pNext; if (!hasEds3) *link = eds3.pNext; else link = &eds3.pNext; if (!hasCacheControl) *link = cacheControl.pNext; if (hasDescriptorIndexing) { descriptorIndexing.pNext = features2.pNext; features2.pNext = &descriptorIndexing; } ::vkGetPhysicalDeviceFeatures2((VkPhysicalDevice)physicalDevice, &features2); const bool useNonUniform = hasDescriptorIndexing && descriptorIndexing.shaderSampledImageArrayNonUniformIndexing; const bool useEds = hasEds && eds.extendedDynamicState; const bool useEds3 = hasEds3 && eds3.colorBlendEnable && eds3.colorBlendEquation && eds3.colorWriteMask; const bool useCacheControl = hasCacheControl && cacheControl.pipelineCreationCacheControl; // the chain again, of what is enabled, with nothing else on void *chain = nullptr; eds3 = {}; eds3.sType = pso_ext::STRUCTURE_TYPE_EDS3_FEATURES; if (useNonUniform) { descriptorIndexing = {}; descriptorIndexing.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT; descriptorIndexing.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; descriptorIndexing.pNext = chain; chain = &descriptorIndexing; deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); } if (useCacheControl) { cacheControl.pNext = chain; chain = &cacheControl; deviceExtensions.push_back("VK_EXT_pipeline_creation_cache_control"); } if (useEds3) { eds3.colorBlendEnable = eds3.colorBlendEquation = eds3.colorWriteMask = VK_TRUE; eds3.pNext = chain; chain = &eds3; deviceExtensions.push_back("VK_EXT_extended_dynamic_state3"); } if (useEds) { eds.pNext = chain; chain = &eds; deviceExtensions.push_back("VK_EXT_extended_dynamic_state"); } featuresChain = chain; VulkanContext::Instance()->extendedDynamicState = useEds; VulkanContext::Instance()->extendedDynamicState3 = useEds3; VulkanContext::Instance()->pipelineCacheControl = useCacheControl; VulkanContext::Instance()->nonUniformTextureIndexing = useNonUniform; } /* ...and where it cannot be, its lanes taking their textures in turns: * Vulkan 1.1's subgroup ballot, in compute shaders */ VulkanContext::Instance()->subgroupBallotCompute = false; if (VK_VERSION_MINOR(deviceProperties.apiVersion) >= 1 && ::vkGetPhysicalDeviceProperties2 != nullptr) { VkPhysicalDeviceSubgroupProperties subgroup = {}; subgroup.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES; VkPhysicalDeviceProperties2 properties2 = {}; properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; properties2.pNext = &subgroup; ::vkGetPhysicalDeviceProperties2((VkPhysicalDevice)physicalDevice, &properties2); VulkanContext::Instance()->subgroupBallotCompute = (subgroup.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 && (subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BALLOT_BIT) != 0; } // create a Device float queuePriority = 1.0f; vk::DeviceQueueCreateInfo deviceQueueCreateInfos[] = { vk::DeviceQueueCreateInfo(vk::DeviceQueueCreateFlags(), context->queue_family_index, 1, &queuePriority), vk::DeviceQueueCreateInfo(vk::DeviceQueueCreateFlags(), context->presentation_queue_family_index, 1, &queuePriority), }; vk::PhysicalDeviceFeatures features; if (required_features != nullptr) features = *required_features; if (fragmentStoresAndAtomics) features.fragmentStoresAndAtomics = true; if (VulkanContext::Instance()->samplerAnisotropy) features.samplerAnisotropy = true; if (VulkanContext::Instance()->storageImageExtendedFormats) features.shaderStorageImageExtendedFormats = true; if (VulkanContext::Instance()->multiDrawIndirect) features.multiDrawIndirect = true; if (VulkanContext::Instance()->drawIndirectFirstInstance) features.drawIndirectFirstInstance = true; if (VulkanContext::Instance()->sampleRateShading) features.sampleRateShading = true; if (VulkanContext::Instance()->shaderInt64) features.shaderInt64 = true; if (VulkanContext::Instance()->sampledImageArrayIndexing) features.shaderSampledImageArrayDynamicIndexing = true; vk::DeviceCreateInfo deviceCreateInfo(vk::DeviceCreateFlags(), context->queue_family_index == context->presentation_queue_family_index ? 1 : 2, deviceQueueCreateInfos, num_required_device_layers, required_device_layers, deviceExtensions.size(), &deviceExtensions[0], &features); deviceCreateInfo.pNext = featuresChain; vk::Device device; if (create_device_wrapper != nullptr) { // the frontend's vkCreateDevice, which adds what it needs device = vk::Device(create_device_wrapper((VkPhysicalDevice)physicalDevice, opaque, &static_cast(deviceCreateInfo))); if (!device) { ERROR_LOG(RENDERER, "The frontend could not create the Vulkan device"); return false; } } else device = physicalDevice.createDevice(deviceCreateInfo); context->device = (VkDevice)device; vulkan_symbol_wrapper_load_core_device_symbols(context->device); // Queues context->queue = (VkQueue)device.getQueue(context->queue_family_index, 0); context->presentation_queue = (VkQueue)device.getQueue(context->presentation_queue_family_index, 0); return true; } bool VkCreateDevice(retro_vulkan_context* context, VkInstance instance, VkPhysicalDevice gpu, VkSurfaceKHR surface, PFN_vkGetInstanceProcAddr get_instance_proc_addr, const char** required_device_extensions, unsigned num_required_device_extensions, const char** required_device_layers, unsigned num_required_device_layers, const VkPhysicalDeviceFeatures* required_features) { return createDevice(context, instance, gpu, surface, get_instance_proc_addr, required_device_extensions, num_required_device_extensions, required_device_layers, num_required_device_layers, required_features, nullptr, nullptr); } // The same device, made through the frontend's wrapper (negotiation v2), // which brings its own extensions and features. bool VkCreateDevice2(retro_vulkan_context* context, VkInstance instance, VkPhysicalDevice gpu, VkSurfaceKHR surface, PFN_vkGetInstanceProcAddr get_instance_proc_addr, retro_vulkan_create_device_wrapper_t create_device_wrapper, void *opaque) { return createDevice(context, instance, gpu, surface, get_instance_proc_addr, nullptr, 0, nullptr, 0, nullptr, create_device_wrapper, opaque); } bool VulkanContext::Init(retro_hw_render_interface_vulkan *retro_render_if) { if (retro_render_if->interface_type != RETRO_HW_RENDER_INTERFACE_VULKAN || retro_render_if->interface_version != RETRO_HW_RENDER_INTERFACE_VULKAN_VERSION) return false; this->retro_render_if = retro_render_if; swapChainSize = 1; for (u32 m = retro_render_if->get_sync_index_mask(retro_render_if->handle); m >>= 1; ) swapChainSize++; instance = vk::Instance(retro_render_if->instance); physicalDevice = vk::PhysicalDevice(retro_render_if->gpu); device = vk::Device(retro_render_if->device); queue = vk::Queue(retro_render_if->queue); vk::PhysicalDeviceProperties *properties; static vk::PhysicalDeviceProperties props; physicalDevice.getProperties(&props); NOTICE_LOG(RENDERER, "GPU Supports Vulkan API: %u.%u.%u", VK_VERSION_MAJOR(props.apiVersion), VK_VERSION_MINOR(props.apiVersion), VK_VERSION_PATCH(props.apiVersion)); if (VK_VERSION_MINOR(props.apiVersion) >= 1 && ::vkGetPhysicalDeviceFormatProperties2 != nullptr) { NOTICE_LOG(RENDERER, "GPU Supports vkGetPhysicalDeviceProperties2"); static vk::PhysicalDeviceProperties2 properties2; vk::PhysicalDeviceMaintenance3Properties properties3; properties2.pNext = &properties3; physicalDevice.getProperties2(&properties2); properties = &properties2.properties; maxMemoryAllocationSize = properties3.maxMemoryAllocationSize; } else { properties = &props; maxMemoryAllocationSize = 0xFFFFFFFFu; } uniformBufferAlignment = properties->limits.minUniformBufferOffsetAlignment; storageBufferAlignment = properties->limits.minStorageBufferOffsetAlignment; maxStorageBufferRange = properties->limits.maxStorageBufferRange; maxSamplerAnisotropy = properties->limits.maxSamplerAnisotropy; vendorID = properties->vendorID; apiVersion = props.apiVersion; maxDrawIndirectCount = properties->limits.maxDrawIndirectCount; cmdDrawIndexedIndirectCount = drawIndirectCount ? (PFN_vkCmdDrawIndexedIndirectCountKHR)device.getProcAddr("vkCmdDrawIndexedIndirectCountKHR") : nullptr; // the general pipelines' dynamic states, as the device was made with them (pso.h) dynamicState = {}; if (extendedDynamicState) { pso_ext::Functions& f = dynamicState; f.CmdSetCullMode = (decltype(f.CmdSetCullMode))device.getProcAddr("vkCmdSetCullModeEXT"); f.CmdSetPrimitiveTopology = (decltype(f.CmdSetPrimitiveTopology))device.getProcAddr("vkCmdSetPrimitiveTopologyEXT"); f.CmdSetDepthTestEnable = (decltype(f.CmdSetDepthTestEnable))device.getProcAddr("vkCmdSetDepthTestEnableEXT"); f.CmdSetDepthWriteEnable = (decltype(f.CmdSetDepthWriteEnable))device.getProcAddr("vkCmdSetDepthWriteEnableEXT"); f.CmdSetDepthCompareOp = (decltype(f.CmdSetDepthCompareOp))device.getProcAddr("vkCmdSetDepthCompareOpEXT"); f.CmdSetStencilTestEnable = (decltype(f.CmdSetStencilTestEnable))device.getProcAddr("vkCmdSetStencilTestEnableEXT"); f.CmdSetStencilOp = (decltype(f.CmdSetStencilOp))device.getProcAddr("vkCmdSetStencilOpEXT"); f.depthStencil = f.CmdSetCullMode && f.CmdSetPrimitiveTopology && f.CmdSetDepthTestEnable && f.CmdSetDepthWriteEnable && f.CmdSetDepthCompareOp && f.CmdSetStencilTestEnable && f.CmdSetStencilOp; } if (extendedDynamicState3) { pso_ext::Functions& f = dynamicState; f.CmdSetColorBlendEnable = (decltype(f.CmdSetColorBlendEnable))device.getProcAddr("vkCmdSetColorBlendEnableEXT"); f.CmdSetColorBlendEquation = (decltype(f.CmdSetColorBlendEquation))device.getProcAddr("vkCmdSetColorBlendEquationEXT"); f.CmdSetColorWriteMask = (decltype(f.CmdSetColorWriteMask))device.getProcAddr("vkCmdSetColorWriteMaskEXT"); f.blend = f.CmdSetColorBlendEnable && f.CmdSetColorBlendEquation && f.CmdSetColorWriteMask; } #ifdef VK_PSO_TEST_NO_EDS dynamicState.depthStencil = false; #endif #ifdef VK_PSO_TEST_NO_EDS3 dynamicState.blend = false; #endif NOTICE_LOG(RENDERER, "Vulkan: general pipelines drawn with %s depth, stencil, cull and topology, and %s blend", dynamicState.depthStencil ? "dynamic" : "fixed", dynamicState.blend ? "dynamic" : "fixed"); QueryVramBudget(); // what SSAA can have: the sample counts colour, depth and stencil attachments can all have, and memory that is only there if used ssaaSampleCounts = (u32)(properties->limits.framebufferColorSampleCounts & properties->limits.framebufferDepthSampleCounts & properties->limits.framebufferStencilSampleCounts); lazyMemory = false; { const vk::PhysicalDeviceMemoryProperties memory = physicalDevice.getMemoryProperties(); for (u32 i = 0; i < memory.memoryTypeCount; i++) if (memory.memoryTypes[i].propertyFlags & vk::MemoryPropertyFlagBits::eLazilyAllocated) lazyMemory = true; } vk::FormatProperties formatProperties = physicalDevice.getFormatProperties(vk::Format::eR5G5B5A1UnormPack16); if ((formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage) && (formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eBlitDst) && (formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eBlitSrc)) optimalTilingSupported1555 = true; else NOTICE_LOG(RENDERER, "eR5G5B5A1UnormPack16 not supported for optimal tiling"); formatProperties = physicalDevice.getFormatProperties(vk::Format::eR5G6B5UnormPack16); if ((formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage) && (formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eBlitDst) && (formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eBlitSrc)) optimalTilingSupported565 = true; else NOTICE_LOG(RENDERER, "eR5G6B5UnormPack16 not supported for optimal tiling"); formatProperties = physicalDevice.getFormatProperties(vk::Format::eR4G4B4A4UnormPack16); if ((formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage) && (formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eBlitDst) && (formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eBlitSrc)) optimalTilingSupported4444 = true; else NOTICE_LOG(RENDERER, "eR4G4B4A4UnormPack16 not supported for optimal tiling"); // Descriptor pool vk::DescriptorPoolSize pool_sizes[] = { { vk::DescriptorType::eSampler, 2 }, { vk::DescriptorType::eCombinedImageSampler, 4000 }, { vk::DescriptorType::eSampledImage, 2 }, { vk::DescriptorType::eStorageImage, 24 }, { vk::DescriptorType::eUniformTexelBuffer, 2 }, { vk::DescriptorType::eStorageTexelBuffer, 2 }, { vk::DescriptorType::eUniformBuffer, 36 }, { vk::DescriptorType::eStorageBuffer, 36 }, { vk::DescriptorType::eUniformBufferDynamic, 2 }, { vk::DescriptorType::eStorageBufferDynamic, 2 }, { vk::DescriptorType::eInputAttachment, 36 } }; descriptorPool = device.createDescriptorPoolUnique(vk::DescriptorPoolCreateInfo(vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, 10000, ARRAY_SIZE(pool_sizes), pool_sizes)); LoadPipelineCache(); heap.Init(physicalDevice, device); FindDepthFormat(); // the HDR output, when the frontend took it (libretro.cpp) if (hdr_output.format != HDR_OUTPUT_NONE) { hdr_vk_code_t code; hdrFormat = hdr_output.format == HDR_OUTPUT_SCRGB ? VK_FORMAT_R16G16B16A16_SFLOAT : VK_FORMAT_A2B10G10R10_UNORM_PACK32; code.vert = RasterSpvWords(RSPV_SSAA_RESOLVE_VERT, &code.vert_size); code.frag = RasterSpvWords(RSPV_HDR_ENCODE_FRAG, &code.frag_size); code.frag_subpass = RasterSpvWords(RSPV_HDR_ENCODE_FRAG_SUBPASS, &code.frag_subpass_size); code.frag_subpass_ssaa = RasterSpvWords(RSPV_HDR_ENCODE_FRAG_SUBPASS_SSAA, &code.frag_subpass_ssaa_size); code.frag_sums = RasterSpvWords(RSPV_HDR_ENCODE_FRAG_SUMS, &code.frag_sums_size); code.frag_linear = RasterSpvWords(RSPV_HDR_ENCODE_FRAG_LINEAR, &code.frag_linear_size); code.frag_sums_linear = RasterSpvWords(RSPV_HDR_ENCODE_FRAG_SUMS_LINEAR, &code.frag_sums_linear_size); { // (by enum hdr_bloom_stage) static const RasterSpv bloom[HDR_BLOOM_STAGES] = { RSPV_HDR_BLOOM_BRIGHT, RSPV_HDR_BLOOM_BLUR, RSPV_HDR_BLOOM_DOWN, RSPV_HDR_BLOOM_DOWN_KARIS, RSPV_HDR_BLOOM_UP }; for (int i = 0; i < HDR_BLOOM_STAGES; i++) code.bloom[i] = RasterSpvWords(bloom[i], &code.bloom_size[i]); } code.post = RasterSpvWords(RSPV_HDR_POST_FRAG, &code.post_size); hdr = hdr_vk_new(retro_render_if, heap.Raw(), (VkPipelineCache)pipelineCache, hdrFormat, (unsigned)swapChainSize, &code); if (hdr == nullptr) ERROR_LOG(RENDERER, "Vulkan: the HDR output could not be set up; the picture goes out as 24-bit"); else NOTICE_LOG(RENDERER, "Vulkan: HDR output, %s", hdr_output.format == HDR_OUTPUT_SCRGB ? "scRGB (RGBA16F)" : "HDR10 (A2B10G10R10)"); } retro_image.image_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; retro_image.create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; retro_image.create_info.pNext = nullptr; retro_image.create_info.format = (VkFormat)colorFormat; retro_image.create_info.viewType = VK_IMAGE_VIEW_TYPE_2D; retro_image.create_info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; retro_image.create_info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; retro_image.create_info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; retro_image.create_info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; retro_image.create_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; retro_image.create_info.subresourceRange.baseArrayLayer = 0; retro_image.create_info.subresourceRange.layerCount = 1; retro_image.create_info.subresourceRange.baseMipLevel = 0; retro_image.create_info.subresourceRange.levelCount = 1; retro_image.create_info.flags = 0; return true; } /* The budget of the largest device-local heap, by VK_EXT_memory_budget * when the device has it (a physical device query: the extension does not * have to be enabled on the device the frontend made), and half that heap * when it has not */ void VulkanContext::QueryVramBudget() { bool haveBudget = false; if (::vkGetPhysicalDeviceMemoryProperties2 != nullptr) for (const auto& property : physicalDevice.enumerateDeviceExtensionProperties()) if (!strcmp(property.extensionName, VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) { haveBudget = true; break; } vk::PhysicalDeviceMemoryBudgetPropertiesEXT budget; vk::PhysicalDeviceMemoryProperties memory; if (haveBudget) { vk::PhysicalDeviceMemoryProperties2 memory2; memory2.pNext = &budget; physicalDevice.getMemoryProperties2(&memory2); memory = memory2.memoryProperties; } else memory = physicalDevice.getMemoryProperties(); u32 heap = VK_MAX_MEMORY_HEAPS; for (u32 i = 0; i < memory.memoryHeapCount && i < VK_MAX_MEMORY_HEAPS; i++) if ((memory.memoryHeaps[i].flags & vk::MemoryHeapFlagBits::eDeviceLocal) && (heap == VK_MAX_MEMORY_HEAPS || memory.memoryHeaps[i].size > memory.memoryHeaps[heap].size)) heap = i; if (heap == VK_MAX_MEMORY_HEAPS) heap = 0; // (there is always a heap, and one at least is device-local) vramBudget = VramBudget(); if (haveBudget && budget.heapBudget[heap] != 0) { vramBudget.budget_kib = vram_kib(budget.heapBudget[heap]); vramBudget.usage_kib = vram_kib(budget.heapUsage[heap]); vramBudget.exact = true; } else vramBudget.budget_kib = vram_kib(memory.memoryHeaps[heap].size / 2); vram_budget_log("Vulkan", vramBudget); } u32 VulkanContext::SsaaSamples(u32 requested, u32 perSampleKib, u32 fixedKib, bool transient, const char *what) const { if (requested <= 1) return 1; /* Shaded per sample, or it is not supersampling: a device the frontend * made has no sample rate shading the core knows of */ if (!sampleRateShading) { WARN_LOG(RENDERER, "%s: SSAA off: the device was made without sample rate shading", what); return 1; } const u32 device = ssaa_device_samples(requested, ssaaSampleCounts); /* Attachments that are never stored take no memory where the device * has memory that is only backed when used (tilers) */ const bool free = transient && lazyMemory; const u32 samples = ssaa_clamp_samples(device, vramBudget.available_kib(), fixedKib, free ? 0 : perSampleKib); NOTICE_LOG(RENDERER, "%s: SSAA %u samples a pixel (asked %u, the device can draw %u; %u MB a sample%s, %u MB of the budget left)", what, samples, requested, device, perSampleKib >> 10, free ? " if backed" : "", vramBudget.available_kib() >> 10); return samples; } u32 VulkanContext::GetDepthBytes() const { return depthFormat == vk::Format::eD32SfloatS8Uint ? 8 : 4; } vk::Format VulkanContext::FindDepthFormat() { const vk::Format depthFormats[] = { vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint, vk::Format::eD16UnormS8Uint }; vk::ImageTiling tiling; depthFormat = vk::Format::eUndefined; for (int i = 0; i < ARRAY_SIZE(depthFormats); i++) { vk::FormatProperties formatProperties = physicalDevice.getFormatProperties(depthFormats[i]); if (formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eDepthStencilAttachment) { tiling = vk::ImageTiling::eOptimal; depthFormat = depthFormats[i]; break; } } if (depthFormat == vk::Format::eUndefined) { // Try to find a linear format for (int i = 0; i < ARRAY_SIZE(depthFormats); i++) { vk::FormatProperties formatProperties = physicalDevice.getFormatProperties(depthFormats[i]); if (formatProperties.linearTilingFeatures & vk::FormatFeatureFlagBits::eDepthStencilAttachment) { tiling = vk::ImageTiling::eLinear; depthFormat = depthFormats[i]; break; } } if (depthFormat == vk::Format::eUndefined) die("No supported depth/stencil format found"); } NOTICE_LOG(RENDERER, "Using depth format %s tiling %s", vk::to_string(depthFormat).c_str(), vk::to_string(tiling).c_str()); return depthFormat; } void VulkanContext::PresentFrame(vk::Image image, vk::ImageView imageView, vk::Offset2D extent, bool hdr) { if (this->hdr != nullptr) retro_image.create_info.format = hdr ? hdrFormat : (VkFormat)colorFormat; retro_image.image_view = (VkImageView)imageView; retro_image.create_info.image = (VkImage)image; retro_render_if->set_image(retro_render_if->handle, &retro_image, 0, nullptr, VK_QUEUE_FAMILY_IGNORED); // once a frame: memory left empty by a change of size goes back to the driver heap.Frame(); } u32 VulkanContext::HdrRing(CommandPool& pool) { u32 i; for (i = 0; i < ARRAY_SIZE(hdrPools) && hdrPools[i] != nullptr; i++) if (hdrPools[i] == &pool) return i; if (i < ARRAY_SIZE(hdrPools)) hdrPools[i] = &pool; return i; // (past the last ring: the encode leaves it alone) } void VulkanContext::HdrPrepare(CommandPool& pool, vk::CommandBuffer cmd) { if (hdr != nullptr) hdr_vk_prepare(hdr, &hdr_output, (VkCommandBuffer)cmd, HdrRing(pool), (unsigned)pool.GetIndex(), pool.GetFrames()); } vk::UniquePipeline VulkanContext::HdrSubpassPipeline(vk::RenderPass renderPass, u32 subpass, u32 samples) { if (hdr == nullptr) return vk::UniquePipeline(); const VkPipeline p = hdr_vk_subpass_pipeline(hdr, (VkRenderPass)renderPass, subpass, samples); if (p == VK_NULL_HANDLE) ERROR_LOG(RENDERER, "Vulkan: the HDR output's subpass pipeline could not be made"); return vk::UniquePipeline(vk::Pipeline(p), vk::ObjectDestroy(device)); } void VulkanContext::HdrSubpassDraw(CommandPool& pool, vk::CommandBuffer cmd, vk::Pipeline pipeline, vk::ImageView picture, vk::ImageView samples, vk::Extent2D extent) { /* (every picture is 8 bits a channel, dithered; with SSAA's samples, * what is encoded is their exact average, which is not) */ if (hdr != nullptr) hdr_vk_subpass_draw(hdr, &hdr_output, (VkCommandBuffer)cmd, HdrRing(pool), (unsigned)pool.GetIndex(), (VkPipeline)pipeline, (VkImageView)picture, (VkImageView)samples, extent.width, extent.height, !samples); } bool VulkanContext::HdrPostActive() const { return hdr != nullptr && hdr_vk_post_active(); } bool VulkanContext::HdrSumsReady() { return hdr != nullptr && hdr_vk_sums_ready(hdr); } bool VulkanContext::HdrEncode(CommandPool& pool, vk::Image& image, vk::ImageView& view, vk::Extent2D size, vk::ImageView sums) { if (hdr == nullptr) return false; VkImage i = (VkImage)image; VkImageView v = (VkImageView)view; vk::CommandBuffer cmd = pool.Allocate(); cmd.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit)); const bool done = hdr_vk_encode(hdr, &hdr_output, (VkCommandBuffer)cmd, HdrRing(pool), (unsigned)pool.GetIndex(), pool.GetFrames(), (unsigned)GetCurrentImageIndex(), &i, &v, size.width, size.height, true, (VkImageView)sums); cmd.end(); if (done) { image = vk::Image(i); view = vk::ImageView(v); } return done; } /* The frontend keeps the last picture it was handed and draws it again * for as long as no other comes: on a frame the game draws nothing new * in, behind its menu, and - with threaded video - from its video thread * while this thread has gone on, so that a picture handed over can still * be in a frame not yet drawn. wait_sync_index does not cover that (it is * for the index the core draws into next, and RetroArch's own driver * waits for nothing). A picture destroyed is a picture the frontend then * reads freed memory from. So the frontend is told to let go of it first, * by an image of none: RetroArch's threaded video draws what it has * queued and waits for its own submissions then. The queue the frontend * and the core share is then waited out, for a frontend that only forgets * the image and whatever it submitted with it. A change of renderer is * rare and the machine stands still for it; no frame waits here. The next * picture the new renderer draws is handed over as ever. */ void VulkanContext::ReleasePicture() { if (retro_render_if == nullptr || !device) return; retro_render_if->set_image(retro_render_if->handle, nullptr, 0, nullptr, VK_QUEUE_FAMILY_IGNORED); WaitQueueIdle(); } void VulkanContext::WaitQueueIdle() { if (retro_render_if == nullptr || !device) return; retro_render_if->lock_queue(retro_render_if->handle); (void)queue.waitIdle(); retro_render_if->unlock_queue(retro_render_if->handle); } void VulkanContext::Term() { if (device) { device.waitIdle(); // (the worker's cache merged into this one) VkPsoWorker::Instance().Stop(pipelineCache); SavePipelineCache(); } // (before the heap it has its memory from) hdr_vk_free(hdr); hdr = nullptr; for (CommandPool *& p : hdrPools) p = nullptr; descriptorPool.reset(); heap.Term(); if (pipelineCache) device.destroyPipelineCache(pipelineCache); pipelineCache = nullptr; VkPsoWorker::Instance().SetDrawCache(nullptr); psoKeys.Term(); extendedDynamicState = false; extendedDynamicState3 = false; pipelineCacheControl = false; dynamicState = {}; } /* The cache file: this header, the keys of the pipelines made (PsoKeyLog), * then the device's pipeline cache data. A file without the header is a * cache of an earlier core's, the device's data alone. */ struct PsoCacheFileHeader { char magic[8]; u32 version; // of the keys' layout (pipeline.h, oit_pipeline.h) u32 keyCount; u64 dataSize; }; static const char PsoCacheMagic[8] = { 'F', 'C', 'V', 'K', 'P', 'S', 'O', 0 }; static const u32 PsoCacheVersion = 1; void VulkanContext::LoadPipelineCache() { psoKeys.Init(); std::string cachePath = get_writable_data_path(PipelineCacheFileName); void *file = nullptr; int64_t fileSize = 0; const u8 *data = nullptr; size_t dataSize = 0; if (filestream_read_file(cachePath.c_str(), &file, &fileSize) && file != nullptr && fileSize > 0) { PsoCacheFileHeader header; if ((size_t)fileSize >= sizeof(header)) memcpy(&header, file, sizeof(header)); if ((size_t)fileSize >= sizeof(header) && !memcmp(header.magic, PsoCacheMagic, sizeof(PsoCacheMagic))) { const size_t keyBytes = (size_t)header.keyCount * sizeof(u64); if (keyBytes / sizeof(u64) == header.keyCount && sizeof(header) + keyBytes <= (size_t)fileSize && header.dataSize <= (u64)fileSize - sizeof(header) - keyBytes) { if (header.version == PsoCacheVersion) { std::vector keys(header.keyCount); if (!keys.empty()) memcpy(&keys[0], (const u8 *)file + sizeof(header), keyBytes); psoKeys.Load(keys.data(), header.keyCount); } data = (const u8 *)file + sizeof(header) + keyBytes; dataSize = (size_t)header.dataSize; } } else { data = (const u8 *)file; dataSize = (size_t)fileSize; } } VkPipelineCacheCreateInfo info = {}; info.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; info.flags = pipelineCacheControl ? pso_ext::PIPELINE_CACHE_CREATE_EXTERNALLY_SYNCHRONIZED : 0; info.initialDataSize = dataSize; info.pInitialData = data; VkPipelineCache cache = VK_NULL_HANDLE; if (vkCreatePipelineCache((VkDevice)device, &info, nullptr, &cache) != VK_SUCCESS) { info.initialDataSize = 0; info.pInitialData = nullptr; if (vkCreatePipelineCache((VkDevice)device, &info, nullptr, &cache) != VK_SUCCESS) cache = VK_NULL_HANDLE; } pipelineCache = vk::PipelineCache(cache); // the worker's, from the same data; the fixed pipelines a draw makes itself, in this one VkPsoWorker::Instance().SetDrawCache(pipelineCache); VkPsoWorker::Instance().Start(device, data, dataSize, pipelineCacheControl); savedBuilt = 0; u32 keyCount = 0; psoKeys.Keys(keyCount); NOTICE_LOG(RENDERER, "Vulkan pipeline cache loaded from %s: %zd bytes, %u pipeline keys", cachePath.c_str(), dataSize, keyCount); free(file); } void VulkanContext::SavePipelineCache() { if (!pipelineCache) return; VkPsoWorker& worker = VkPsoWorker::Instance(); // nothing made since the last save: as it is const u32 made = worker.Built() + worker.SyncBuilt(); if (made == savedBuilt && savedBuilt != 0) return; worker.WaitIdle(); worker.MergeInto(pipelineCache); size_t dataSize = 0; if (vkGetPipelineCacheData((VkDevice)device, (VkPipelineCache)pipelineCache, &dataSize, nullptr) != VK_SUCCESS) return; u32 keyCount = 0; const u64 *keys = psoKeys.Keys(keyCount); PsoCacheFileHeader header; memcpy(header.magic, PsoCacheMagic, sizeof(PsoCacheMagic)); header.version = PsoCacheVersion; header.keyCount = keyCount; std::vector file(sizeof(header) + keyCount * sizeof(u64) + dataSize); if (vkGetPipelineCacheData((VkDevice)device, (VkPipelineCache)pipelineCache, &dataSize, &file[sizeof(header) + keyCount * sizeof(u64)]) != VK_SUCCESS) return; header.dataSize = dataSize; memcpy(&file[0], &header, sizeof(header)); if (keyCount != 0) memcpy(&file[sizeof(header)], keys, keyCount * sizeof(u64)); file.resize(sizeof(header) + keyCount * sizeof(u64) + dataSize); // (written beside it, then put in its place: a write cut short leaves the one there was) const std::string cachePath = get_writable_data_path(PipelineCacheFileName); const std::string tmpPath = cachePath + ".tmp"; RFILE *f = filestream_open(tmpPath.c_str(), RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); if (f == nullptr) return; bool written = filestream_write(f, &file[0], (int64_t)file.size()) == (int64_t)file.size(); filestream_close(f); if (written) { filestream_delete(cachePath.c_str()); written = filestream_rename(tmpPath.c_str(), cachePath.c_str()) == 0; } if (!written) filestream_delete(tmpPath.c_str()); else NOTICE_LOG(RENDERER, "Vulkan pipeline cache saved to %s: %zd bytes, %u pipeline keys", cachePath.c_str(), dataSize, keyCount); savedBuilt = made; }