Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -3346,6 +3346,43 @@ consent prompt for directory ACLs, so the Web UI shows setup steps for correctin
</tr>
</table>

### vk_quality

<table>
<tr>
<td>Description</td>
<td colspan="2">
Determines encoder tradeoff between quality and speed.
@note{This option only applies when using Vulkan [encoder](#encoder).}
</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">@code{}balanced@endcode</td>
</tr>
<tr>
<td>Example</td>
<td colspan="2">@code{}
vk_quality = quality
@endcode</td>
</tr>
<tr>
<td>Options</td>
<td>speed</td>
<td>Speed (prefer speed)</td>
</tr>
<tr>
<td></td>
<td>balanced</td>
<td>Balanced (default)</td>
</tr>
<tr>
<td></td>
<td>quality</td>
<td>Quality (prefer quality)</td>
</tr>
</table>

## Software Encoder

### sw_preset
Expand Down
11 changes: 11 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<std::string_view, int> 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);
Expand Down
1 change: 1 addition & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
197 changes: 197 additions & 0 deletions src/platform/linux/vulkan_encode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
Expand Down Expand Up @@ -85,6 +227,12 @@ namespace vk {
}
}
vkDestroyInstance(inst, nullptr);

// Cache the result
cached_path = render_path;
cached_result = result;
cached = true;

return result;
}

Expand Down Expand Up @@ -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;
}
}
}
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src_assets/common/assets/web/configs/config_tabs.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@
"nameKey": "config.category_vulkan_encoder",
"options": {
"vk_tune": 2,
"vk_rc_mode": 2
"vk_rc_mode": 2,
"vk_quality": "balanced"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,16 @@ const config = ref(props.config)
</select>
<div class="form-text">{{ $t('config.vk_rc_mode_desc') }}</div>
</div>

<!-- Quality -->
<div class="mb-3">
<label for="vk_quality" class="form-label">{{ $t('config.vk_quality') }}</label>
<select id="vk_quality" class="form-select" v-model="config.vk_quality">
<option value="speed">{{ $t('config.vk_quality_speed') }}</option>
<option value="balanced">{{ $t('config.vk_quality_balanced') }}</option>
<option value="quality">{{ $t('config.vk_quality_quality') }}</option>
</select>
<div class="form-text">{{ $t('config.vk_quality_desc') }}</div>
</div>
</div>
</template>
5 changes: 5 additions & 0 deletions src_assets/common/assets/web/public/assets/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading