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 |
+
+
+ | Options |
+ speed |
+ Speed (prefer speed) |
+
+
+ |
+ balanced |
+ Balanced (default) |
+
+
+ |
+ quality |
+ Quality (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",