diff --git a/docs/configuration.md b/docs/configuration.md index 43bf4c69b63..fb6ce69c5a1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -3346,6 +3346,43 @@ consent prompt for directory ACLs, so the Web UI shows setup steps for correctin +### vk_quality + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Description + Determines encoder tradeoff between quality and speed. + @note{This option only applies when using Vulkan [encoder](#encoder).} +
Default@code{}balanced@endcode
Example@code{} + vk_quality = quality + @endcode
OptionsspeedSpeed (prefer speed)
balancedBalanced (default)
qualityQuality (prefer quality)
+ ## Software Encoder ### sw_preset diff --git a/src/config.cpp b/src/config.cpp index 7bdd9f7c5ac..ad44a29d8d8 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -774,6 +774,7 @@ namespace config { { 2, // vk.tune (default: ll - low latency) 2, // vk.rc_mode (default: cbr) + 2, // vk.quality (default: 2 = balanced, 1 = speed, 3 = quality) }, {}, // capture @@ -1693,6 +1694,16 @@ namespace config { int_f(vars, "vk_tune", video.vk.tune); int_f(vars, "vk_rc_mode", video.vk.rc_mode); + std::string vk_quality; + string_f(vars, "vk_quality", vk_quality); + static const std::unordered_map vk_quality_map = { + {"speed"sv, 1}, + {"balanced"sv, 2}, + {"quality"sv, 3} + }; + if (auto it = vk_quality_map.find(vk_quality); it != vk_quality_map.end()) { + video.vk.quality = it->second; + } string_f(vars, "capture", video.capture); string_f(vars, "encoder", video.encoder); diff --git a/src/config.h b/src/config.h index 8bd3aa4d533..76176e00f51 100644 --- a/src/config.h +++ b/src/config.h @@ -132,6 +132,7 @@ namespace config { struct { int tune; ///< Vulkan encoder tuning mode: default, HQ, LL, ULL, or lossless. int rc_mode; ///< Vulkan encoder rate-control mode: driver, CQP, CBR, or VBR. + int quality; ///< Vulkan encoder quality preset: 1 = speed, 2 = balanced (default), 3 = quality. } vk; ///< Vulkan encoder options. std::string capture; ///< Capture backend name selected by configuration. diff --git a/src/platform/linux/vulkan_encode.cpp b/src/platform/linux/vulkan_encode.cpp index 1e6a78994e4..94b353df2ee 100644 --- a/src/platform/linux/vulkan_encode.cpp +++ b/src/platform/linux/vulkan_encode.cpp @@ -39,9 +39,151 @@ using namespace std::literals; namespace vk { + /** + * @brief Query and validate encoder quality level for a given codec. + * + * This uses FFmpeg's Vulkan context to query maxQualityLevels and validate + * the requested level using vkGetPhysicalDeviceVideoEncodeQualityLevelPropertiesKHR. + * If the requested level is invalid, returns 0 to indicate failure. + * + * @param codec_id FFmpeg codec ID (AV_CODEC_ID_H264, AV_CODEC_ID_HEVC, AV_CODEC_ID_AV1). + * @param vk_ctx FFmpeg's AVVulkanDeviceContext with initialized Vulkan handles. + * @param requested_level Requested quality level to validate (0 = speed, max = quality). + * @param[out] max_level Maximum quality level supported by the driver. + * @return Validated quality level (requested_level if valid, 0 if invalid). + */ + static uint32_t query_and_validate_quality_level(AVCodecID codec_id, AVVulkanDeviceContext *vk_ctx, uint32_t requested_level, uint32_t *max_level) { + *max_level = 0; + + if (!vk_ctx || !vk_ctx->inst || !vk_ctx->phys_dev || !vk_ctx->get_proc_addr) { + return 0; + } + + auto vkGetPhysicalDeviceVideoCapabilitiesKHR_fn = (PFN_vkGetPhysicalDeviceVideoCapabilitiesKHR) + vk_ctx->get_proc_addr(vk_ctx->inst, "vkGetPhysicalDeviceVideoCapabilitiesKHR"); + + if (!vkGetPhysicalDeviceVideoCapabilitiesKHR_fn) { + return 0; + } + + // Codec-specific profile extensions (zero-initialized) + VkVideoEncodeH264ProfileInfoKHR h264_profile = {}; + h264_profile.sType = VK_STRUCTURE_TYPE_VIDEO_ENCODE_H264_PROFILE_INFO_KHR; + h264_profile.stdProfileIdc = STD_VIDEO_H264_PROFILE_IDC_HIGH; + + VkVideoEncodeH265ProfileInfoKHR h265_profile = {}; + h265_profile.sType = VK_STRUCTURE_TYPE_VIDEO_ENCODE_H265_PROFILE_INFO_KHR; + h265_profile.stdProfileIdc = STD_VIDEO_H265_PROFILE_IDC_MAIN; + + VkVideoEncodeAV1ProfileInfoKHR av1_profile = {}; + av1_profile.sType = VK_STRUCTURE_TYPE_VIDEO_ENCODE_AV1_PROFILE_INFO_KHR; + av1_profile.stdProfile = STD_VIDEO_AV1_PROFILE_MAIN; + + // Codec-specific capabilities extensions (zero-initialized) + VkVideoEncodeH264CapabilitiesKHR h264_caps = {}; + h264_caps.sType = VK_STRUCTURE_TYPE_VIDEO_ENCODE_H264_CAPABILITIES_KHR; + + VkVideoEncodeH265CapabilitiesKHR h265_caps = {}; + h265_caps.sType = VK_STRUCTURE_TYPE_VIDEO_ENCODE_H265_CAPABILITIES_KHR; + + VkVideoEncodeAV1CapabilitiesKHR av1_caps = {}; + av1_caps.sType = VK_STRUCTURE_TYPE_VIDEO_ENCODE_AV1_CAPABILITIES_KHR; + + // Usage info chain (follows FFmpeg's pattern) + VkVideoEncodeUsageInfoKHR usage_info = {}; + usage_info.sType = VK_STRUCTURE_TYPE_VIDEO_ENCODE_USAGE_INFO_KHR; + usage_info.videoUsageHints = VK_VIDEO_ENCODE_USAGE_STREAMING_BIT_KHR; + usage_info.videoContentHints = VK_VIDEO_ENCODE_CONTENT_DEFAULT_KHR; + usage_info.tuningMode = VK_VIDEO_ENCODE_TUNING_MODE_LOW_LATENCY_KHR; + + // Profile info + VkVideoProfileInfoKHR profile = {}; + profile.sType = VK_STRUCTURE_TYPE_VIDEO_PROFILE_INFO_KHR; + profile.pNext = &usage_info; + profile.chromaSubsampling = VK_VIDEO_CHROMA_SUBSAMPLING_420_BIT_KHR; + profile.lumaBitDepth = VK_VIDEO_COMPONENT_BIT_DEPTH_8_BIT_KHR; + profile.chromaBitDepth = VK_VIDEO_COMPONENT_BIT_DEPTH_8_BIT_KHR; + + // Encode capabilities output + VkVideoEncodeCapabilitiesKHR enc_caps = {}; + enc_caps.sType = VK_STRUCTURE_TYPE_VIDEO_ENCODE_CAPABILITIES_KHR; + + // Set codec operation and chain codec-specific extensions (profile and caps) + switch (codec_id) { + case AV_CODEC_ID_H264: + profile.videoCodecOperation = VK_VIDEO_CODEC_OPERATION_ENCODE_H264_BIT_KHR; + usage_info.pNext = &h264_profile; + enc_caps.pNext = &h264_caps; + break; + case AV_CODEC_ID_HEVC: + profile.videoCodecOperation = VK_VIDEO_CODEC_OPERATION_ENCODE_H265_BIT_KHR; + usage_info.pNext = &h265_profile; + enc_caps.pNext = &h265_caps; + break; + case AV_CODEC_ID_AV1: + profile.videoCodecOperation = VK_VIDEO_CODEC_OPERATION_ENCODE_AV1_BIT_KHR; + usage_info.pNext = &av1_profile; + enc_caps.pNext = &av1_caps; + break; + default: + return 0; + } + + // Main capabilities output + VkVideoCapabilitiesKHR caps = {}; + caps.sType = VK_STRUCTURE_TYPE_VIDEO_CAPABILITIES_KHR; + caps.pNext = &enc_caps; + + VkResult result = vkGetPhysicalDeviceVideoCapabilitiesKHR_fn(vk_ctx->phys_dev, &profile, &caps); + if (result != VK_SUCCESS) { + return 0; + } + + *max_level = enc_caps.maxQualityLevels; + BOOST_LOG(debug) << "[vulkan] Driver max quality level: "sv << *max_level; + + // If requested level is 0 or within range, try to validate it + if (requested_level > *max_level) { + BOOST_LOG(warning) << "[vulkan] Requested quality level "sv << requested_level + << " exceeds max "sv << *max_level << ", using max"sv; + requested_level = *max_level; + } + + // Try to validate the requested level + auto vkGetPhysicalDeviceVideoEncodeQualityLevelPropertiesKHR_fn = (PFN_vkGetPhysicalDeviceVideoEncodeQualityLevelPropertiesKHR) + vk_ctx->get_proc_addr(vk_ctx->inst, "vkGetPhysicalDeviceVideoEncodeQualityLevelPropertiesKHR"); + + if (vkGetPhysicalDeviceVideoEncodeQualityLevelPropertiesKHR_fn) { + VkPhysicalDeviceVideoEncodeQualityLevelInfoKHR quality_level_info = {}; + quality_level_info.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VIDEO_ENCODE_QUALITY_LEVEL_INFO_KHR; + quality_level_info.pVideoProfile = &profile; + quality_level_info.qualityLevel = requested_level; + + VkVideoEncodeQualityLevelPropertiesKHR quality_props = {}; + quality_props.sType = VK_STRUCTURE_TYPE_VIDEO_ENCODE_QUALITY_LEVEL_PROPERTIES_KHR; + + result = vkGetPhysicalDeviceVideoEncodeQualityLevelPropertiesKHR_fn(vk_ctx->phys_dev, &quality_level_info, &quality_props); + if (result != VK_SUCCESS) { + return 0; + } + } + + return requested_level; + } + // Match a DRI render node path to a Vulkan device index via VK_EXT_physical_device_drm. // Returns the index as a string (e.g. "1"), or empty string if no match. + // Result is cached since render device doesn't change during runtime. static std::string find_vulkan_index_for_render_node(const char *render_path) { + // Cache result to avoid repeated Vulkan instance creation (~10-20ms overhead) + static std::string cached_result; + static std::string cached_path; + static bool cached = false; + + if (cached && cached_path == render_path) { + return cached_result; + } + struct stat node_stat; if (stat(render_path, &node_stat) < 0) { return {}; @@ -85,6 +227,12 @@ namespace vk { } } vkDestroyInstance(inst, nullptr); + + // Cache the result + cached_path = render_path; + cached_result = result; + cached = true; + return result; } @@ -193,6 +341,55 @@ namespace vk { if (config::video.vk.rc_mode == 4) { ctx->rc_min_rate = 0; } + + // Map quality preset to driver's quality range (same abstraction as VAAPI) + // 1 = speed, 2 = balanced (default), 3 = quality + // Note: Vulkan quality is 0 = fastest, higher = slower/better quality + int quality_preset = config::video.vk.quality; + if (quality_preset > 0 && ctx->hw_frames_ctx) { + // Reuse FFmpeg's Vulkan instance and device instead of creating new ones + auto *frames_ctx = (AVHWFramesContext *) ctx->hw_frames_ctx->data; + auto *dev_ctx = (AVHWDeviceContext *) frames_ctx->device_ref->data; + auto *vk_ctx = (AVVulkanDeviceContext *) dev_ctx->hwctx; + + // Calculate target quality based on preset + uint32_t max_quality = 0; + uint32_t target_quality = 0; + const char *preset_name = "speed"; + + // First pass: get max quality to calculate target + query_and_validate_quality_level(ctx->codec_id, vk_ctx, 0, &max_quality); + + if (max_quality > 0) { + switch (quality_preset) { + default: + case 1: // speed (level 0 = fastest) + target_quality = 0; + preset_name = "speed"; + break; + case 2: // balanced (middle level) + target_quality = max_quality / 2; + preset_name = "balanced"; + break; + case 3: // quality (max level = best quality) + target_quality = max_quality; + preset_name = "quality"; + break; + } + + // Validate the calculated target quality level + uint32_t validated_quality = query_and_validate_quality_level(ctx->codec_id, vk_ctx, target_quality, &max_quality); + + if (validated_quality == target_quality) { + av_dict_set_int(options, "quality", validated_quality, 0); + BOOST_LOG(info) << "[vulkan] Encoder quality set to "sv << validated_quality + << " ("sv << preset_name << "), driver range: 0-"sv << max_quality; + } else { + BOOST_LOG(warning) << "[vulkan] Quality level "sv << target_quality + << " is not supported by the driver; using default"sv; + } + } + } } /** diff --git a/src_assets/common/assets/web/configs/config_tabs.json b/src_assets/common/assets/web/configs/config_tabs.json index 97bb80925af..c477936c396 100644 --- a/src_assets/common/assets/web/configs/config_tabs.json +++ b/src_assets/common/assets/web/configs/config_tabs.json @@ -166,7 +166,8 @@ "nameKey": "config.category_vulkan_encoder", "options": { "vk_tune": 2, - "vk_rc_mode": 2 + "vk_rc_mode": 2, + "vk_quality": "balanced" } }, { diff --git a/src_assets/common/assets/web/configs/tabs/encoders/VulkanEncoder.vue b/src_assets/common/assets/web/configs/tabs/encoders/VulkanEncoder.vue index 0bb6cfe0d9b..edd408623b1 100644 --- a/src_assets/common/assets/web/configs/tabs/encoders/VulkanEncoder.vue +++ b/src_assets/common/assets/web/configs/tabs/encoders/VulkanEncoder.vue @@ -34,5 +34,16 @@ const config = ref(props.config)
{{ $t('config.vk_rc_mode_desc') }}
+ + +
+ + +
{{ $t('config.vk_quality_desc') }}
+
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index c2f3db92d20..99d6a728e0b 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -462,6 +462,11 @@ "vk_tune_hq": "High Quality (hq)", "vk_tune_ll": "Low Latency (ll) (default)", "vk_tune_ull": "Ultra Low Latency (ull)", + "vk_quality": "Quality", + "vk_quality_desc": "Determines encoder tradeoff between quality and speed.", + "vk_quality_speed": "speed -- prefer speed", + "vk_quality_balanced": "balanced -- balanced (default)", + "vk_quality_quality": "quality -- prefer quality", "virtual_sink": "Virtual Sink", "virtual_sink_desc": "Manually specify a virtual audio device to use. If unset, the device is chosen automatically. We strongly recommend leaving this field blank to use automatic device selection!", "virtual_sink_placeholder": "Steam Streaming Speakers",