/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: sw=2 ts=8 et : */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include #include #include #include #include #include #include #include #ifdef __linux__ # include #endif #if defined(MOZ_ASAN) || defined(FUZZING) # include #endif #include "mozilla/ScopeExit.h" #ifdef __SUNPRO_CC # include #endif #include #include "mozilla/GfxInfoUtils.h" #define OUTPUT_PIPE 1 constexpr int CODEC_HW_DEC_H264 = 1 << 4; constexpr int CODEC_HW_DEC_HEVC = 1 << 12; constexpr int CODEC_HW_DEC_VP9 = 1 << 8; constexpr int CODEC_HW_DEC_AV1 = 1 << 10; // Supported codecs are derived from physical device extension list; drivers // only expose VK_KHR_video_decode_* when the device supports that codec. static const char* const kVideoDecodeExtensions[][2] = { {"VK_KHR_video_decode_h264", "H264"}, {"VK_KHR_video_decode_h265", "HEVC"}, {"VK_KHR_video_decode_vp9", "VP9"}, {"VK_KHR_video_decode_av1", "AV1"}, }; static const char kInstanceExtension[] = "VK_KHR_get_physical_device_properties2"; static const char* const kVulkanVideoDecodeCoreExtensions[] = { "VK_KHR_video_queue", "VK_KHR_video_decode_queue", }; struct InstanceFunctions { PFN_vkDestroyInstance vkDestroyInstance; PFN_vkEnumeratePhysicalDevices vkEnumeratePhysicalDevices; PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties; PFN_vkGetPhysicalDeviceProperties2 vkGetPhysicalDeviceProperties2; PFN_vkEnumerateDeviceExtensionProperties vkEnumerateDeviceExtensionProperties; }; static bool LoadInstanceFunctions(PFN_vkGetInstanceProcAddr aGetProcAddr, VkInstance aInstance, InstanceFunctions* aOut) { aOut->vkDestroyInstance = (PFN_vkDestroyInstance)aGetProcAddr(aInstance, "vkDestroyInstance"); aOut->vkEnumeratePhysicalDevices = (PFN_vkEnumeratePhysicalDevices)aGetProcAddr( aInstance, "vkEnumeratePhysicalDevices"); aOut->vkGetPhysicalDeviceProperties = (PFN_vkGetPhysicalDeviceProperties)aGetProcAddr( aInstance, "vkGetPhysicalDeviceProperties"); aOut->vkGetPhysicalDeviceProperties2 = (PFN_vkGetPhysicalDeviceProperties2)aGetProcAddr( aInstance, "vkGetPhysicalDeviceProperties2"); aOut->vkEnumerateDeviceExtensionProperties = (PFN_vkEnumerateDeviceExtensionProperties)aGetProcAddr( aInstance, "vkEnumerateDeviceExtensionProperties"); return aOut->vkDestroyInstance && aOut->vkEnumeratePhysicalDevices && aOut->vkGetPhysicalDeviceProperties && aOut->vkEnumerateDeviceExtensionProperties; } static bool DeviceHasVulkanVideoDecodeCoreExtensions( const InstanceFunctions* aInst, VkPhysicalDevice aDevice) { uint32_t extensionCount = 0; VkResult res = aInst->vkEnumerateDeviceExtensionProperties( aDevice, nullptr, &extensionCount, nullptr); if (res != VK_SUCCESS || extensionCount == 0) { return false; } VkExtensionProperties* extensions = (VkExtensionProperties*)malloc( extensionCount * sizeof(VkExtensionProperties)); if (!extensions) { return false; } res = aInst->vkEnumerateDeviceExtensionProperties( aDevice, nullptr, &extensionCount, extensions); if (res != VK_SUCCESS) { free(extensions); return false; } bool decodeExtensionsMatched = false; for (const char* req : kVulkanVideoDecodeCoreExtensions) { decodeExtensionsMatched = false; for (uint32_t e = 0; e < extensionCount; e++) { if (strcmp(extensions[e].extensionName, req) == 0) { decodeExtensionsMatched = true; break; } } if (!decodeExtensionsMatched) { log("vulkantest: missing required extension %s\n", req); free(extensions); return false; } } free(extensions); return true; } struct DeviceCandidate { VkPhysicalDevice device; VkPhysicalDeviceProperties props; int matchesRenderer; }; static int CompareCandidates(const void* a, const void* b) { const DeviceCandidate* p1 = (const DeviceCandidate*)a; const DeviceCandidate* p2 = (const DeviceCandidate*)b; if (p1->matchesRenderer != p2->matchesRenderer) { return p2->matchesRenderer - p1->matchesRenderer; } return (int)(p2->props.deviceType) - (int)(p1->props.deviceType); } static int GetDeviceCodecs(const InstanceFunctions* aInst, VkPhysicalDevice aDevice, uint32_t aDeviceIndex) { VkPhysicalDeviceProperties props = {}; aInst->vkGetPhysicalDeviceProperties(aDevice, &props); if (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU) { return 0; } log("vulkantest: device %u: %s\n", (unsigned int)aDeviceIndex, props.deviceName); uint32_t extensionCount = 0; VkResult res = aInst->vkEnumerateDeviceExtensionProperties( aDevice, nullptr, &extensionCount, nullptr); if (res != VK_SUCCESS || extensionCount == 0) { return 0; } VkExtensionProperties* extensions = (VkExtensionProperties*)malloc( extensionCount * sizeof(VkExtensionProperties)); if (!extensions) { return 0; } res = aInst->vkEnumerateDeviceExtensionProperties( aDevice, nullptr, &extensionCount, extensions); int codecs = 0; if (res == VK_SUCCESS) { for (uint32_t e = 0; e < extensionCount; e++) { if (strstr(extensions[e].extensionName, "decode") != nullptr) { log("vulkantest: device %u extension %s\n", (unsigned int)aDeviceIndex, extensions[e].extensionName); } } for (uint32_t e = 0; e < extensionCount; e++) { for (const auto& pair : kVideoDecodeExtensions) { if (strcmp(extensions[e].extensionName, pair[0]) == 0) { log("vulkantest: device %u has %s\n", (unsigned int)aDeviceIndex, pair[0]); if (strcmp(pair[1], "H264") == 0) { codecs |= CODEC_HW_DEC_H264; } else if (strcmp(pair[1], "HEVC") == 0) { codecs |= CODEC_HW_DEC_HEVC; } else if (strcmp(pair[1], "VP9") == 0) { codecs |= CODEC_HW_DEC_VP9; } else if (strcmp(pair[1], "AV1") == 0) { codecs |= CODEC_HW_DEC_AV1; } break; } } } } free(extensions); return codecs; } extern "C" { // Query Vulkan Video Decode Support for the GPU decoder device, // following the existing selection logic in FFmpegVideoDecoder.cpp static void vulkantest(const char* aDrmRenderPath) { void* libvulkan = nullptr; void* vkGetInstanceProcAddrPtr = nullptr; VkInstance instance = VK_NULL_HANDLE; uint32_t rendererDrmMajor = 0; uint32_t rendererDrmMinor = 0; log("vulkantest start\n"); #ifdef __linux__ if (aDrmRenderPath && *aDrmRenderPath) { struct stat st = {}; if (stat(aDrmRenderPath, &st) == 0) { rendererDrmMajor = major(st.st_rdev); rendererDrmMinor = minor(st.st_rdev); } } #endif auto autoRelease = mozilla::MakeScopeExit([&] { if (instance != VK_NULL_HANDLE && libvulkan) { InstanceFunctions inst = {}; if (LoadInstanceFunctions( (PFN_vkGetInstanceProcAddr)vkGetInstanceProcAddrPtr, instance, &inst) && inst.vkDestroyInstance) { inst.vkDestroyInstance(instance, nullptr); } } if (libvulkan) { dlclose(libvulkan); } }); libvulkan = dlopen("libvulkan.so.1", RTLD_NOW); if (!libvulkan) { libvulkan = dlopen("libvulkan.so", RTLD_NOW); } if (!libvulkan) { log("vulkantest: libvulkan not found\n"); record_value("VULKAN_SUPPORTED\nFALSE\n"); return; } vkGetInstanceProcAddrPtr = (void*)dlsym(libvulkan, "vkGetInstanceProcAddr"); if (!vkGetInstanceProcAddrPtr) { record_error("Vulkan test failed: vkGetInstanceProcAddr not found."); return; } PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)vkGetInstanceProcAddrPtr; PFN_vkCreateInstance vkCreateInstance = (PFN_vkCreateInstance)vkGetInstanceProcAddr(VK_NULL_HANDLE, "vkCreateInstance"); if (!vkCreateInstance) { record_error("Vulkan test failed: vkCreateInstance not found."); return; } PFN_vkEnumerateInstanceExtensionProperties vkEnumerateInstanceExtensionProperties = (PFN_vkEnumerateInstanceExtensionProperties)vkGetInstanceProcAddr( VK_NULL_HANDLE, "vkEnumerateInstanceExtensionProperties"); bool useProperties2 = false; if (vkEnumerateInstanceExtensionProperties) { uint32_t extCount = 0; VkResult r = vkEnumerateInstanceExtensionProperties(nullptr, &extCount, nullptr); if (r == VK_SUCCESS && extCount > 0) { VkExtensionProperties* exts = (VkExtensionProperties*)malloc( extCount * sizeof(VkExtensionProperties)); if (exts) { r = vkEnumerateInstanceExtensionProperties(nullptr, &extCount, exts); if (r == VK_SUCCESS) { for (uint32_t i = 0; i < extCount; i++) { if (strcmp(exts[i].extensionName, kInstanceExtension) == 0) { useProperties2 = true; break; } } } free(exts); } } } VkApplicationInfo appInfo = {}; appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; appInfo.pApplicationName = "vulkantest"; appInfo.applicationVersion = 1; appInfo.pEngineName = "vulkantest"; appInfo.engineVersion = 1; appInfo.apiVersion = VK_API_VERSION_1_0; VkInstanceCreateInfo createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; createInfo.pApplicationInfo = &appInfo; const char* enabledExt = nullptr; if (useProperties2) { enabledExt = kInstanceExtension; createInfo.enabledExtensionCount = 1; createInfo.ppEnabledExtensionNames = &enabledExt; } VkResult res = vkCreateInstance(&createInfo, nullptr, &instance); if (res != VK_SUCCESS) { log("vulkantest: vkCreateInstance failed %d\n", (int)res); record_value("VULKAN_SUPPORTED\nFALSE\n"); return; } InstanceFunctions inst = {}; if (!LoadInstanceFunctions(vkGetInstanceProcAddr, instance, &inst)) { record_error("Vulkan test failed: required entry points not found."); return; } uint32_t physicalDeviceCount = 0; res = inst.vkEnumeratePhysicalDevices(instance, &physicalDeviceCount, nullptr); if (res != VK_SUCCESS || physicalDeviceCount == 0) { log("vulkantest: no physical devices\n"); record_value("VULKAN_SUPPORTED\nFALSE\n"); return; } VkPhysicalDevice* physicalDevices = (VkPhysicalDevice*)malloc(physicalDeviceCount * sizeof(VkPhysicalDevice)); if (!physicalDevices) { record_error("Vulkan test failed: out of memory."); return; } mozilla::ScopeExit freeDevices([&] { free(physicalDevices); }); res = inst.vkEnumeratePhysicalDevices(instance, &physicalDeviceCount, physicalDevices); if (res != VK_SUCCESS) { record_value("VULKAN_SUPPORTED\nFALSE\n"); return; } log("vulkantest: %u physical device(s)\n", (unsigned int)physicalDeviceCount); size_t candidateCap = 8; DeviceCandidate* candidates = (DeviceCandidate*)malloc(candidateCap * sizeof(DeviceCandidate)); if (!candidates) { record_error("Vulkan test failed: out of memory."); return; } mozilla::ScopeExit freeCandidates([&] { free(candidates); }); int idx = 0; for (int i = 0; i < (int)physicalDeviceCount; i++) { VkPhysicalDeviceProperties p = {}; int isDecoderMatchesRendererFound = 0; if (rendererDrmMajor && rendererDrmMinor && inst.vkGetPhysicalDeviceProperties2) { VkPhysicalDeviceDrmPropertiesEXT drmProps = {}; drmProps.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRM_PROPERTIES_EXT; VkPhysicalDeviceProperties2 props2 = {}; props2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; props2.pNext = &drmProps; inst.vkGetPhysicalDeviceProperties2(physicalDevices[i], &props2); p = props2.properties; isDecoderMatchesRendererFound = (drmProps.hasRender && (uint32_t)rendererDrmMajor == (uint32_t)drmProps.renderMajor && (uint32_t)rendererDrmMinor == (uint32_t)drmProps.renderMinor) || (drmProps.hasPrimary && (uint32_t)rendererDrmMajor == (uint32_t)drmProps.primaryMajor && (uint32_t)rendererDrmMinor == (uint32_t)drmProps.primaryMinor) ? 1 : 0; } else { inst.vkGetPhysicalDeviceProperties(physicalDevices[i], &p); } uint32_t vmaj = VK_API_VERSION_MAJOR(p.apiVersion); uint32_t vmin = VK_API_VERSION_MINOR(p.apiVersion); log("vulkantest: physdev[%d] %s type=%d vulkan=%u.%u " "matches_renderer=%d\n", i, p.deviceName, (int)p.deviceType, (unsigned int)vmaj, (unsigned int)vmin, isDecoderMatchesRendererFound); if (p.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU) { log("vulkantest: physdev[%d] skip (CPU)\n", i); continue; } if (!(vmaj > 1 || (vmaj == 1 && vmin >= 3))) { log("vulkantest: physdev[%d] skip (need Vulkan 1.3+)\n", i); continue; } if (!DeviceHasVulkanVideoDecodeCoreExtensions(&inst, physicalDevices[i])) { log("vulkantest: physdev[%d] skip (no VK_KHR_video_queue / " "VK_KHR_video_decode_queue)\n", i); continue; } if ((size_t)idx >= candidateCap) { candidateCap *= 2; DeviceCandidate* next = (DeviceCandidate*)realloc( candidates, candidateCap * sizeof(DeviceCandidate)); if (!next) { record_error("Vulkan test failed: out of memory."); return; } candidates = next; } candidates[idx].device = physicalDevices[i]; candidates[idx].props = p; candidates[idx].matchesRenderer = isDecoderMatchesRendererFound; log("vulkantest: physdev[%d] -> decode candidate[%d]\n", i, idx); idx++; } if (idx <= 0) { log("vulkantest: no suitable devices (need non-CPU, Vulkan 1.3+, " "VK_KHR_video_queue + VK_KHR_video_decode_queue)\n"); record_value("VULKAN_SUPPORTED\nFALSE\n"); return; } qsort(candidates, idx, sizeof(DeviceCandidate), CompareCandidates); const DeviceCandidate* chosen = &candidates[0]; log("vulkantest: selected: %s (matches_renderer=%d)\n", chosen->props.deviceName, chosen->matchesRenderer); record_value("VULKAN_DEVICE\n%s\n", chosen->props.deviceName); int codecs = GetDeviceCodecs(&inst, chosen->device, 0); log("vulkantest: codecs bitmask %d\n", codecs); if (codecs != 0) { record_value("VULKAN_SUPPORTED\nTRUE\n"); record_value("VULKAN_HWCODECS\n%d\n", codecs); } else { record_value("VULKAN_SUPPORTED\nFALSE\n"); } log("vulkantest finished\n"); } } // extern "C" static void PrintUsage() { printf( "Firefox Vulkan video decode probe utility\n" "\n" "usage: vulkantest [options]\n" "\n" "Options:\n" "\n" " -h --help show this message\n" " -p --probe probe Vulkan (no DRM device)\n" " -d --drm drm_device probe Vulkan (drm_device is ignored)\n" "\n"); } int main(int argc, char** argv) { struct option longOptions[] = {{"help", no_argument, nullptr, 'h'}, {"probe", no_argument, nullptr, 'p'}, {"drm", required_argument, nullptr, 'd'}, {nullptr, 0, nullptr, 0}}; const char* shortOptions = "hpd:"; int c; bool doProbe = false; const char* drmPath = nullptr; while ((c = getopt_long(argc, argv, shortOptions, longOptions, nullptr)) != -1) { switch (c) { case 'p': doProbe = true; break; case 'd': doProbe = true; drmPath = optarg; break; case 'h': default: break; } } if (doProbe || optind < argc) { #if defined(MOZ_ASAN) || defined(FUZZING) signal(SIGSEGV, SIG_DFL); #endif const char* env = getenv("MOZ_GFX_DEBUG"); enable_logging = env && *env == '1'; output_pipe = OUTPUT_PIPE; if (!enable_logging) { close_logging(); } vulkantest(drmPath); record_flush(); return EXIT_SUCCESS; } PrintUsage(); return 0; }