| /* |
| * Copyright 2023 Google LLC |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "src/gpu/vk/VulkanUtilsPriv.h" |
| |
| #include "include/core/SkStream.h" |
| #include "include/gpu/vk/VulkanBackendContext.h" |
| #include "include/private/base/SkAssert.h" |
| #include "include/private/base/SkDebug.h" |
| #include "include/private/base/SkTFitsIn.h" |
| #include "include/private/base/SkTo.h" |
| #include "src/gpu/vk/VulkanInterface.h" |
| |
| #include <algorithm> |
| #include <vector> |
| |
| namespace skgpu { |
| |
| /** |
| * Define a macro that both ganesh and graphite can use to make simple calls into Vulkan so we can |
| * share more code between them. |
| */ |
| #define SHARED_GR_VULKAN_CALL(IFACE, X) (IFACE)->fFunctions.f##X |
| |
| /** |
| * Parse the driver version number in VkPhysicalDeviceProperties::driverVersion according to the |
| * driver ID. |
| */ |
| DriverVersion ParseVulkanDriverVersion(VkDriverId driverId, uint32_t driverVersion) { |
| // Most drivers follow the VK_MAKE_API_VERSION convention. The exceptions are documented in the |
| // switch cases below. |
| switch (driverId) { |
| case VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS: |
| // Windows Intel driver versions are built in the following format: |
| // |
| // Major (18 bits) | Minor (14 bits) |
| // |
| return DriverVersion(driverVersion >> 14, driverVersion & 0x3FFF); |
| case VK_DRIVER_ID_NVIDIA_PROPRIETARY: |
| // Nvidia proprietary driver version is in the following format: |
| // |
| // Major (10 bits) | Minor (8 bits) | SubMinor (8 bits) | Patch (6 bits) |
| // |
| return DriverVersion(driverVersion >> 22, driverVersion >> 14 & 0xFF); |
| case VK_DRIVER_ID_QUALCOMM_PROPRIETARY: |
| // Qualcomm proprietary driver version has changed over time. In the new format, it's |
| // almost following the VK_MAKE_API_VERSION convention, except the top bit is set. With |
| // VK_API_VERSION_MAJOR, this bit is masked out (corresponding to a value of 512), which |
| // is typically expected to be visible in the version, i.e. the version is 512.NNNN. The |
| // old format is unknown, and is considered 0.NNNN. |
| if ((driverVersion & 0x80000000) != 0) { |
| return DriverVersion(VK_API_VERSION_MAJOR(driverVersion) | 512, |
| VK_API_VERSION_MINOR(driverVersion)); |
| } |
| |
| return DriverVersion(0, driverVersion); |
| case VK_DRIVER_ID_MOLTENVK: |
| // MoltenVK driver version is in the following format: |
| // |
| // Major * 10000 + Minor * 100 + patch |
| // |
| return DriverVersion(driverVersion / 10000, (driverVersion / 100) % 100); |
| default: |
| return DriverVersion(VK_API_VERSION_MAJOR(driverVersion), |
| VK_API_VERSION_MINOR(driverVersion)); |
| } |
| } |
| |
| /** |
| * Returns a populated VkSamplerYcbcrConversionCreateInfo object based on VulkanYcbcrConversionInfo |
| */ |
| void SetupSamplerYcbcrConversionInfo(VkSamplerYcbcrConversionCreateInfo* outInfo, |
| std::optional<VkFilter>* requiredSamplerFilter, |
| const VulkanYcbcrConversionInfo& conversionInfo) { |
| #ifdef SK_DEBUG |
| const VkFormatFeatureFlags& featureFlags = conversionInfo.fFormatFeatures; |
| |
| // Format feature flags are only representative of an external format's capabilities, so skip |
| // these checks in the case of using a known format or if the featureFlags were clearly not |
| // filled in. |
| if (conversionInfo.fFormat == VK_FORMAT_UNDEFINED && featureFlags) { |
| if (conversionInfo.fXChromaOffset == VK_CHROMA_LOCATION_MIDPOINT || |
| conversionInfo.fYChromaOffset == VK_CHROMA_LOCATION_MIDPOINT) { |
| SkASSERT(featureFlags & VK_FORMAT_FEATURE_MIDPOINT_CHROMA_SAMPLES_BIT); |
| } |
| if (conversionInfo.fXChromaOffset == VK_CHROMA_LOCATION_COSITED_EVEN || |
| conversionInfo.fYChromaOffset == VK_CHROMA_LOCATION_COSITED_EVEN) { |
| SkASSERT(featureFlags & VK_FORMAT_FEATURE_COSITED_CHROMA_SAMPLES_BIT); |
| } |
| if (conversionInfo.fChromaFilter == VK_FILTER_LINEAR) { |
| SkASSERT(featureFlags & VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_LINEAR_FILTER_BIT); |
| } |
| if (conversionInfo.fForceExplicitReconstruction) { |
| SkASSERT(featureFlags & |
| VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_CHROMA_RECONSTRUCTION_EXPLICIT_FORCEABLE_BIT); |
| } |
| } |
| #endif |
| |
| VkFilter chromaFilter = conversionInfo.fChromaFilter; |
| if (!(conversionInfo.fFormatFeatures & |
| VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_SEPARATE_RECONSTRUCTION_FILTER_BIT)) { |
| if (!(conversionInfo.fFormatFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT)) { |
| // Because we don't have have separate reconstruction filter, the min, mag and |
| // chroma filter must all match. However, we also don't support linear sampling so |
| // the min/mag filter have to be nearest. Therefore, we force the chroma filter to |
| // be nearest regardless of support for the feature |
| // VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_LINEAR_FILTER_BIT. |
| chromaFilter = VK_FILTER_NEAREST; |
| } |
| |
| // Let the caller know that it must match min and mag filters with the chroma filter. |
| *requiredSamplerFilter = chromaFilter; |
| } |
| |
| outInfo->sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO; |
| outInfo->pNext = nullptr; |
| outInfo->format = conversionInfo.fFormat; |
| outInfo->ycbcrModel = conversionInfo.fYcbcrModel; |
| outInfo->ycbcrRange = conversionInfo.fYcbcrRange; |
| outInfo->components = conversionInfo.fComponents; |
| outInfo->xChromaOffset = conversionInfo.fXChromaOffset; |
| outInfo->yChromaOffset = conversionInfo.fYChromaOffset; |
| outInfo->chromaFilter = chromaFilter; |
| outInfo->forceExplicitReconstruction = conversionInfo.fForceExplicitReconstruction; |
| } |
| |
| bool SerializeVkYCbCrInfo(SkWStream* stream, const VulkanYcbcrConversionInfo& info) { |
| SkASSERT(SkTFitsIn<uint64_t>(info.fFormat)); |
| // fExternalFormat is already a uint64_t |
| SkASSERT(SkTFitsIn<uint8_t>(info.fYcbcrModel)); |
| SkASSERT(SkTFitsIn<uint8_t>(info.fYcbcrRange)); |
| SkASSERT(SkTFitsIn<uint8_t>(info.fXChromaOffset)); |
| SkASSERT(SkTFitsIn<uint8_t>(info.fYChromaOffset)); |
| SkASSERT(SkTFitsIn<uint64_t>(info.fChromaFilter)); |
| SkASSERT(SkTFitsIn<uint64_t>(info.fFormatFeatures)); |
| SkASSERT(SkTFitsIn<uint8_t>(info.fComponents.r)); |
| SkASSERT(SkTFitsIn<uint8_t>(info.fComponents.g)); |
| SkASSERT(SkTFitsIn<uint8_t>(info.fComponents.b)); |
| SkASSERT(SkTFitsIn<uint8_t>(info.fComponents.a)); |
| // fForceExplicitReconstruction is a VkBool32 |
| |
| // TODO(robertphillips): this isn't as densely packed as possible |
| if (!stream->write64(static_cast<uint64_t>(info.fFormat))) { return false; } |
| if (!stream->write64(info.fExternalFormat)) { return false; } |
| if (!stream->write8(static_cast<uint8_t>(info.fYcbcrModel))) { return false; } |
| if (!info.isValid()) { |
| return true; |
| } |
| |
| if (!stream->write8(static_cast<uint8_t>(info.fYcbcrRange))) { return false; } |
| if (!stream->write8(static_cast<uint8_t>(info.fXChromaOffset))) { return false; } |
| if (!stream->write8(static_cast<uint8_t>(info.fYChromaOffset))) { return false; } |
| if (!stream->write64(static_cast<uint64_t>(info.fChromaFilter))) { return false; } |
| if (!stream->write64(static_cast<uint64_t>(info.fFormatFeatures))) { return false; } |
| if (!stream->write8(static_cast<uint8_t>(info.fComponents.r))) { return false; } |
| if (!stream->write8(static_cast<uint8_t>(info.fComponents.g))) { return false; } |
| if (!stream->write8(static_cast<uint8_t>(info.fComponents.b))) { return false; } |
| if (!stream->write8(static_cast<uint8_t>(info.fComponents.a))) { return false; } |
| if (!stream->writeBool(SkToBool(info.fForceExplicitReconstruction))) { return false;} |
| |
| return true; |
| } |
| |
| bool DeserializeVkYCbCrInfo(SkStream* stream, VulkanYcbcrConversionInfo* out) { |
| uint64_t tmp64; |
| uint8_t tmp8; |
| |
| if (!stream->readU64(&tmp64)) { return false; } |
| out->fFormat = static_cast<VkFormat>(tmp64); |
| |
| if (!stream->readU64(&tmp64)) { return false; } |
| out->fExternalFormat = tmp64; |
| |
| if (!stream->readU8(&tmp8)) { return false; } |
| out->fYcbcrModel = static_cast<VkSamplerYcbcrModelConversion>(tmp8); |
| |
| if (!out->isValid()) { |
| return true; |
| } |
| |
| if (!stream->readU8(&tmp8)) { return false; } |
| out->fYcbcrRange = static_cast<VkSamplerYcbcrRange>(tmp8); |
| |
| if (!stream->readU8(&tmp8)) { return false; } |
| out->fXChromaOffset = static_cast<VkChromaLocation>(tmp8); |
| |
| if (!stream->readU8(&tmp8)) { return false; } |
| out->fYChromaOffset = static_cast<VkChromaLocation>(tmp8); |
| |
| if (!stream->readU64(&tmp64)) { return false; } |
| out->fChromaFilter = static_cast<VkFilter>(tmp64); |
| |
| if (!stream->readU64(&tmp64)) { return false; } |
| out->fFormatFeatures = static_cast<VkFormatFeatureFlags>(tmp64); |
| |
| if (!stream->readU8(&tmp8)) { return false; } |
| out->fComponents.r = static_cast<VkComponentSwizzle>(tmp8); |
| |
| if (!stream->readU8(&tmp8)) { return false; } |
| out->fComponents.g = static_cast<VkComponentSwizzle>(tmp8); |
| |
| if (!stream->readU8(&tmp8)) { return false; } |
| out->fComponents.b = static_cast<VkComponentSwizzle>(tmp8); |
| |
| if (!stream->readU8(&tmp8)) { return false; } |
| out->fComponents.a = static_cast<VkComponentSwizzle>(tmp8); |
| |
| bool tmpBool; |
| if (!stream->readBool(&tmpBool)) { return false; } |
| out->fForceExplicitReconstruction = tmpBool; |
| |
| return false; |
| } |
| |
| #ifdef SK_BUILD_FOR_ANDROID |
| |
| /** |
| * Shared Vulkan AHardwareBuffer utility functions between graphite and ganesh |
| */ |
| void GetYcbcrConversionInfoFromFormatProps( |
| VulkanYcbcrConversionInfo* outConversionInfo, |
| const VkAndroidHardwareBufferFormatPropertiesANDROID& formatProps) { |
| outConversionInfo->fYcbcrModel = formatProps.suggestedYcbcrModel; |
| outConversionInfo->fYcbcrRange = formatProps.suggestedYcbcrRange; |
| outConversionInfo->fComponents = formatProps.samplerYcbcrConversionComponents; |
| outConversionInfo->fXChromaOffset = formatProps.suggestedXChromaOffset; |
| outConversionInfo->fYChromaOffset = formatProps.suggestedYChromaOffset; |
| outConversionInfo->fForceExplicitReconstruction = VK_FALSE; |
| outConversionInfo->fExternalFormat = formatProps.externalFormat; |
| outConversionInfo->fFormatFeatures = formatProps.formatFeatures; |
| if (VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_LINEAR_FILTER_BIT & |
| formatProps.formatFeatures) { |
| outConversionInfo->fChromaFilter = VK_FILTER_LINEAR; |
| } else { |
| outConversionInfo->fChromaFilter = VK_FILTER_NEAREST; |
| } |
| } |
| |
| bool GetAHardwareBufferProperties( |
| VkAndroidHardwareBufferFormatPropertiesANDROID* outHwbFormatProps, |
| VkAndroidHardwareBufferPropertiesANDROID* outHwbProps, |
| const skgpu::VulkanInterface* interface, |
| const AHardwareBuffer* hwBuffer, |
| VkDevice device) { |
| outHwbFormatProps->sType = |
| VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID; |
| outHwbFormatProps->pNext = nullptr; |
| |
| outHwbProps->sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID; |
| outHwbProps->pNext = outHwbFormatProps; |
| |
| VkResult result = |
| SHARED_GR_VULKAN_CALL(interface, |
| GetAndroidHardwareBufferProperties(device, |
| hwBuffer, |
| outHwbProps)); |
| if (result != VK_SUCCESS) { |
| // The spec suggests VK_ERROR_OUT_OF_HOST_MEMORY and VK_ERROR_INVALID_EXTERNAL_HANDLE_KHR |
| // are the only failure codes, but some platforms may report others, such as |
| // VK_ERROR_FORMAT_NOT_SUPPORTED (-11). |
| SkDebugf("Failed to get AndroidHardwareBufferProperties (result:%d)", result); |
| #if __ANDROID_API__ >= 26 |
| AHardwareBuffer_Desc hwbDesc; |
| AHardwareBuffer_describe(hwBuffer, &hwbDesc); |
| SkDebugf("^ %" PRIu32 "x%" PRIu32 " AHB -- format:%" PRIu32 ", usage:%" PRIu64 |
| ", layers:%" PRIu32, |
| hwbDesc.width, |
| hwbDesc.height, |
| hwbDesc.format, |
| hwbDesc.usage, |
| hwbDesc.layers); |
| #endif |
| return false; |
| } |
| return true; |
| } |
| |
| bool AllocateAndBindImageMemory(skgpu::VulkanAlloc* outVulkanAlloc, |
| VkImage image, |
| const VkPhysicalDeviceMemoryProperties2& phyDevMemProps, |
| const VkAndroidHardwareBufferPropertiesANDROID& hwbProps, |
| AHardwareBuffer* hardwareBuffer, |
| const skgpu::VulkanInterface* interface, |
| VkDevice device) { |
| VkResult result; |
| uint32_t typeIndex = 0; |
| bool foundHeap = false; |
| uint32_t memTypeCnt = phyDevMemProps.memoryProperties.memoryTypeCount; |
| for (uint32_t i = 0; i < memTypeCnt && !foundHeap; ++i) { |
| if (hwbProps.memoryTypeBits & (1 << i)) { |
| const VkPhysicalDeviceMemoryProperties& pdmp = phyDevMemProps.memoryProperties; |
| uint32_t supportedFlags = pdmp.memoryTypes[i].propertyFlags & |
| VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; |
| if (supportedFlags == VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) { |
| typeIndex = i; |
| foundHeap = true; |
| } |
| } |
| } |
| |
| /** |
| * Fallback to use any available memory type for AHB. |
| * |
| * For external memory import, compatible memory types are decided by the Vulkan driver since |
| * the memory has been allocated externally. There are usually special requirements against |
| * external memory. e.g. AHB allocated with CPU R/W often usage bits is only importable for |
| * non-device-local heap on some AMD systems. |
| */ |
| if (!foundHeap && hwbProps.memoryTypeBits) { |
| typeIndex = ffs(hwbProps.memoryTypeBits) - 1; |
| foundHeap = true; |
| } |
| if (!foundHeap) { |
| return false; |
| } |
| |
| VkImportAndroidHardwareBufferInfoANDROID hwbImportInfo; |
| hwbImportInfo.sType = VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID; |
| hwbImportInfo.pNext = nullptr; |
| hwbImportInfo.buffer = hardwareBuffer; |
| |
| VkMemoryDedicatedAllocateInfo dedicatedAllocInfo; |
| dedicatedAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; |
| dedicatedAllocInfo.pNext = &hwbImportInfo; |
| dedicatedAllocInfo.image = image; |
| dedicatedAllocInfo.buffer = VK_NULL_HANDLE; |
| |
| VkMemoryAllocateInfo allocInfo = { |
| VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, // sType |
| &dedicatedAllocInfo, // pNext |
| hwbProps.allocationSize, // allocationSize |
| typeIndex, // memoryTypeIndex |
| }; |
| |
| VkDeviceMemory memory; |
| result = SHARED_GR_VULKAN_CALL(interface, |
| AllocateMemory(device, &allocInfo, nullptr, &memory)); |
| if (result != VK_SUCCESS) { |
| return false; |
| } |
| |
| VkBindImageMemoryInfo bindImageInfo; |
| bindImageInfo.sType = VK_STRUCTURE_TYPE_BIND_IMAGE_MEMORY_INFO; |
| bindImageInfo.pNext = nullptr; |
| bindImageInfo.image = image; |
| bindImageInfo.memory = memory; |
| bindImageInfo.memoryOffset = 0; |
| |
| result = SHARED_GR_VULKAN_CALL(interface, BindImageMemory2(device, 1, &bindImageInfo)); |
| if (result != VK_SUCCESS) { |
| SHARED_GR_VULKAN_CALL(interface, FreeMemory(device, memory, nullptr)); |
| return false; |
| } |
| |
| outVulkanAlloc->fMemory = memory; |
| outVulkanAlloc->fOffset = 0; |
| outVulkanAlloc->fSize = hwbProps.allocationSize; |
| outVulkanAlloc->fFlags = 0; |
| outVulkanAlloc->fBackendMemory = 0; |
| return true; |
| } |
| |
| #endif // SK_BUILD_FOR_ANDROID |
| |
| // Note: since this is called from Vulkan result-checking functions, any Vk calls this function |
| // makes must NOT be checked with those same functions to avoid infinite recursion. |
| void InvokeDeviceLostCallback(const skgpu::VulkanInterface* vulkanInterface, |
| VkDevice vkDevice, |
| skgpu::VulkanDeviceLostContext deviceLostContext, |
| skgpu::VulkanDeviceLostProc deviceLostProc, |
| bool supportsDeviceFaultInfoExtension) { |
| if (!deviceLostProc) { |
| return; |
| } |
| |
| std::vector<VkDeviceFaultAddressInfoEXT> addressInfos = {}; |
| std::vector<VkDeviceFaultVendorInfoEXT> vendorInfos = {}; |
| std::vector<std::byte> vendorBinaryData = {}; |
| |
| if (!supportsDeviceFaultInfoExtension) { |
| deviceLostProc(deviceLostContext, |
| "No details: VK_EXT_device_fault not available/enabled.", |
| addressInfos, |
| vendorInfos, |
| vendorBinaryData); |
| return; |
| } |
| |
| // Query counts |
| VkDeviceFaultCountsEXT faultCounts = {}; |
| faultCounts.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_COUNTS_EXT; |
| VkResult result = SHARED_GR_VULKAN_CALL(vulkanInterface, |
| GetDeviceFaultInfo(vkDevice, &faultCounts, NULL)); |
| if (result != VK_SUCCESS) { |
| deviceLostProc( |
| deviceLostContext, |
| "No details: VK_EXT_device_fault error counting failed: " + std::to_string(result), |
| addressInfos, |
| vendorInfos, |
| vendorBinaryData); |
| return; |
| } |
| |
| // Prepare storage |
| addressInfos.resize(faultCounts.addressInfoCount); |
| vendorInfos.resize(faultCounts.vendorInfoCount); |
| vendorBinaryData.resize(faultCounts.vendorBinarySize); |
| |
| // Query fault info |
| VkDeviceFaultInfoEXT faultInfo = {}; |
| faultInfo.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_INFO_EXT; |
| faultInfo.pAddressInfos = addressInfos.data(); |
| faultInfo.pVendorInfos = vendorInfos.data(); |
| faultInfo.pVendorBinaryData = |
| faultCounts.vendorBinarySize > 0 ? vendorBinaryData.data() : nullptr; |
| result = SHARED_GR_VULKAN_CALL(vulkanInterface, |
| GetDeviceFaultInfo(vkDevice, &faultCounts, &faultInfo)); |
| if (result != VK_SUCCESS) { |
| deviceLostProc( |
| deviceLostContext, |
| "No details: VK_EXT_device_fault info dumping failed: " + std::to_string(result), |
| addressInfos, |
| vendorInfos, |
| vendorBinaryData); |
| return; |
| } |
| |
| deviceLostProc(deviceLostContext, |
| std::string(faultInfo.description), |
| addressInfos, |
| vendorInfos, |
| vendorBinaryData); |
| } |
| |
| sk_sp<skgpu::VulkanInterface> MakeInterface(const skgpu::VulkanBackendContext& context, |
| const skgpu::VulkanExtensions* extOverride, |
| uint32_t* instanceVersionOut, |
| uint32_t* physDevVersionOut) { |
| if (!extOverride) { |
| extOverride = context.fVkExtensions; |
| } |
| SkASSERT(extOverride); |
| PFN_vkEnumerateInstanceVersion localEnumerateInstanceVersion = |
| reinterpret_cast<PFN_vkEnumerateInstanceVersion>( |
| context.fGetProc("vkEnumerateInstanceVersion", VK_NULL_HANDLE, VK_NULL_HANDLE)); |
| uint32_t instanceVersion = 0; |
| // Vulkan 1.1 is required, so vkEnumerateInstanceVersion should always be available. |
| SkASSERT(localEnumerateInstanceVersion != nullptr); |
| VkResult err = localEnumerateInstanceVersion(&instanceVersion); |
| if (err) { |
| return nullptr; |
| } |
| |
| PFN_vkGetPhysicalDeviceProperties localGetPhysicalDeviceProperties = |
| reinterpret_cast<PFN_vkGetPhysicalDeviceProperties>(context.fGetProc( |
| "vkGetPhysicalDeviceProperties", context.fInstance, VK_NULL_HANDLE)); |
| SkASSERT(localGetPhysicalDeviceProperties != nullptr); |
| |
| VkPhysicalDeviceProperties physDeviceProperties; |
| localGetPhysicalDeviceProperties(context.fPhysicalDevice, &physDeviceProperties); |
| uint32_t physDevVersion = physDeviceProperties.apiVersion; |
| |
| uint32_t apiVersion = context.fMaxAPIVersion ? context.fMaxAPIVersion : instanceVersion; |
| |
| instanceVersion = std::min(instanceVersion, apiVersion); |
| physDevVersion = std::min(physDevVersion, apiVersion); |
| |
| if (instanceVersion < VK_API_VERSION_1_1 || physDevVersion < VK_API_VERSION_1_1) { |
| SK_ABORT("Vulkan 1.1 is required but not available. " |
| "Instance version: %#08X, Device version: %#08X", |
| instanceVersion, physDevVersion); |
| return nullptr; |
| } |
| |
| sk_sp<skgpu::VulkanInterface> interface(new skgpu::VulkanInterface(context.fGetProc, |
| context.fInstance, |
| context.fDevice, |
| instanceVersion, |
| physDevVersion, |
| extOverride)); |
| if (!interface->validate(instanceVersion, physDevVersion, extOverride)) { |
| return nullptr; |
| } |
| if (physDevVersionOut) { |
| *physDevVersionOut = physDevVersion; |
| } |
| if (instanceVersionOut) { |
| *instanceVersionOut = instanceVersion; |
| } |
| return interface; |
| } |
| |
| } // namespace skgpu |