fb8deb31a5
A PQ stream on the D3D11VA backend was always tone-mapped to sRGB by the
VideoProcessor — with D3D11VA now auto's first choice on Intel (40030e90),
Intel Windows users would have lost HDR entirely. When the presenter can
import an RGB10A2 D3D11 texture AND offers an HDR10 swapchain (the new
VulkanDecodeDevice::d3d11_hdr10 probe), the hand-off ring switches to
RGB10A2 and the VideoProcessor does a pure colorspace conversion (YCbCr
G2084 -> RGB G2084, no tone mapping); the emitted frame carries PQ/BT.2020
color, so the presenter flips its HDR10 swapchain and video image exactly
as it does for Vulkan Video PQ frames, and the blit passes the PQ values
through untouched. SDR-only paths keep the tonemap-to-sRGB BGRA8 ring;
in-band PQ flips rebuild the ring like a resize (generation-bumped).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
630 lines
29 KiB
Rust
630 lines
29 KiB
Rust
//! Presenter bring-up: instance → surface → device → swapchain (init-time construction).
|
|
|
|
#[cfg(target_os = "linux")]
|
|
use super::HwCtx;
|
|
#[cfg(windows)]
|
|
use super::HwCtxWin;
|
|
use super::{OverlayPipe, Presenter};
|
|
use crate::csc::CscPass;
|
|
#[cfg(target_os = "linux")]
|
|
use crate::dmabuf;
|
|
use anyhow::{anyhow, bail, Context as _, Result};
|
|
use ash::vk;
|
|
use ash::vk::Handle as _;
|
|
use std::ffi::CString;
|
|
|
|
impl Presenter {
|
|
/// Bring up instance → surface → device → swapchain over an SDL window.
|
|
/// `instance_extensions` comes from `VideoSubsystem::vulkan_instance_extensions()`.
|
|
pub fn new(window: &sdl3::video::Window, instance_extensions: &[String]) -> Result<Presenter> {
|
|
let entry = unsafe { ash::Entry::load() }.context("libvulkan not loadable")?;
|
|
|
|
let app_name = CString::new("punktfunk-session").unwrap();
|
|
// 1.3: FFmpeg's Vulkan hwcontext requires an instance of at least 1.3 (any
|
|
// current loader accepts it regardless of device support; device-level gating
|
|
// happens below).
|
|
let app_info = vk::ApplicationInfo::default()
|
|
.application_name(&app_name)
|
|
.api_version(vk::API_VERSION_1_3);
|
|
// HDR10 presentation needs the extended colorspaces at the INSTANCE level.
|
|
let mut instance_extensions: Vec<String> = instance_extensions.to_vec();
|
|
let inst_available =
|
|
unsafe { entry.enumerate_instance_extension_properties(None) }.unwrap_or_default();
|
|
let has_colorspace_ext = inst_available
|
|
.iter()
|
|
.any(|e| e.extension_name_as_c_str() == Ok(c"VK_EXT_swapchain_colorspace"));
|
|
if has_colorspace_ext {
|
|
instance_extensions.push("VK_EXT_swapchain_colorspace".into());
|
|
}
|
|
let ext_cstrings: Vec<CString> = instance_extensions
|
|
.iter()
|
|
.map(|e| CString::new(e.as_str()).unwrap())
|
|
.collect();
|
|
let ext_ptrs: Vec<*const i8> = ext_cstrings.iter().map(|e| e.as_ptr()).collect();
|
|
let instance = unsafe {
|
|
entry.create_instance(
|
|
&vk::InstanceCreateInfo::default()
|
|
.application_info(&app_info)
|
|
.enabled_extension_names(&ext_ptrs),
|
|
None,
|
|
)
|
|
}
|
|
.context("vkCreateInstance")?;
|
|
let surface_i = ash::khr::surface::Instance::new(&entry, &instance);
|
|
|
|
let surface = unsafe { window.vulkan_create_surface(instance.handle()) }
|
|
.map_err(|e| anyhow!("SDL_Vulkan_CreateSurface: {e}"))?;
|
|
|
|
let (pdev, qfi) = pick_device(&instance, &surface_i, surface)?;
|
|
let mem_props = unsafe { instance.get_physical_device_memory_properties(pdev) };
|
|
{
|
|
let props = unsafe { instance.get_physical_device_properties(pdev) };
|
|
let name = props
|
|
.device_name_as_c_str()
|
|
.map(|c| c.to_string_lossy().into_owned())
|
|
.unwrap_or_default();
|
|
tracing::info!(device = %name, queue_family = qfi, "vulkan device");
|
|
}
|
|
|
|
// The dmabuf import set is optional: enabled when the device offers all four,
|
|
// else that path is off (`supports_dmabuf() == false`). Windows has no
|
|
// dmabuf/DRM-PRIME — the whole import path is compiled out there.
|
|
let available = unsafe { instance.enumerate_device_extension_properties(pdev) }?;
|
|
let has = |name: &std::ffi::CStr| {
|
|
available
|
|
.iter()
|
|
.any(|e| e.extension_name_as_c_str() == Ok(name))
|
|
};
|
|
#[cfg(target_os = "linux")]
|
|
let hw_capable = dmabuf::DEVICE_EXTENSIONS.iter().all(|n| has(n));
|
|
let mut dev_exts = vec![ash::khr::swapchain::NAME.as_ptr()];
|
|
#[cfg(target_os = "linux")]
|
|
if hw_capable {
|
|
dev_exts.extend(dmabuf::DEVICE_EXTENSIONS.iter().map(|n| n.as_ptr()));
|
|
} else {
|
|
tracing::info!(
|
|
"device lacks the dmabuf import extensions — VAAPI hardware frames \
|
|
unavailable"
|
|
);
|
|
}
|
|
// D3D11 shared-texture import (the D3D11VA decode hand-off) — optional exactly
|
|
// like the dmabuf set; a device without it keeps Vulkan-Video/software decode.
|
|
// Extensions alone aren't the whole gate: the driver must also report the
|
|
// multiplanar NV12 image as IMPORTABLE from a D3D11 texture handle
|
|
// (vkGetPhysicalDeviceImageFormatProperties2 — creating an unsupported external
|
|
// image is UB, observed as VK_ERROR_DEVICE_LOST at the first submits on NVIDIA).
|
|
#[cfg(windows)]
|
|
let (import_bgra8, import_rgb10) = crate::d3d11::import_supported(&instance, pdev);
|
|
#[cfg(windows)]
|
|
let win_capable = crate::d3d11::DEVICE_EXTENSIONS.iter().all(|n| has(n)) && import_bgra8;
|
|
#[cfg(windows)]
|
|
if win_capable {
|
|
dev_exts.extend(crate::d3d11::DEVICE_EXTENSIONS.iter().map(|n| n.as_ptr()));
|
|
} else {
|
|
tracing::info!(
|
|
"device lacks the win32 external-memory/keyed-mutex extensions — D3D11VA \
|
|
hardware frames unavailable"
|
|
);
|
|
}
|
|
// The adapter LUID (for the D3D11VA backend to create its decode device on the
|
|
// SAME adapter). Core 1.1 query; valid on effectively every Windows driver.
|
|
let mut id_props = vk::PhysicalDeviceIDProperties::default();
|
|
let mut props2 = vk::PhysicalDeviceProperties2::default().push_next(&mut id_props);
|
|
unsafe { instance.get_physical_device_properties2(pdev, &mut props2) };
|
|
let adapter_luid: Option<[u8; 8]> =
|
|
(id_props.device_luid_valid == vk::TRUE).then_some(id_props.device_luid);
|
|
// Static HDR metadata (ST.2086 mastering + CLL) to the presentation engine.
|
|
// Compositors key their "this app is HDR" signaling on the client pushing
|
|
// metadata via vkSetHdrMetadataEXT in addition to picking the HDR10 colorspace
|
|
// (gamescope's SteamOS HDR badge and per-app tone-map targets among them) —
|
|
// the colorspace alone leaves the app looking SDR to the shell.
|
|
let has_hdr_metadata = has(ash::ext::hdr_metadata::NAME);
|
|
if has_hdr_metadata {
|
|
dev_exts.push(ash::ext::hdr_metadata::NAME.as_ptr());
|
|
}
|
|
|
|
// --- Vulkan Video decode (the FFmpeg-on-our-device path) ---------------------
|
|
// Probed, never required: a capable stack gets the video extensions, a second
|
|
// (decode) queue, and the features FFmpeg's decoder needs; anything less means
|
|
// `vulkan_decode() == None` and the decoder chain falls back (VAAPI/software).
|
|
let dev_props = unsafe { instance.get_physical_device_properties(pdev) };
|
|
let dev_is_13 = vk::api_version_major(dev_props.api_version) > 1
|
|
|| vk::api_version_minor(dev_props.api_version) >= 3;
|
|
let mut have_pid = vk::PhysicalDevicePresentIdFeaturesKHR::default();
|
|
let mut have_pwait = vk::PhysicalDevicePresentWaitFeaturesKHR::default();
|
|
let mut have_f11 = vk::PhysicalDeviceVulkan11Features::default();
|
|
let mut have_f12 = vk::PhysicalDeviceVulkan12Features::default();
|
|
let mut have_f13 = vk::PhysicalDeviceVulkan13Features::default();
|
|
// Present-id/present-wait (on-glass timing, latency plan T0.2): query the feature
|
|
// structs only when the device lists both extensions.
|
|
let present_wait_exts =
|
|
has(ash::khr::present_id::NAME) && has(ash::khr::present_wait::NAME);
|
|
let mut have_f2 = vk::PhysicalDeviceFeatures2::default()
|
|
.push_next(&mut have_f11)
|
|
.push_next(&mut have_f12)
|
|
.push_next(&mut have_f13);
|
|
if present_wait_exts {
|
|
have_f2 = have_f2.push_next(&mut have_pid).push_next(&mut have_pwait);
|
|
}
|
|
unsafe { instance.get_physical_device_features2(pdev, &mut have_f2) };
|
|
// Copy the one base-features fact out NOW: `have_f2` mutably borrows the chained
|
|
// structs through its pNext chain, so any later use of it would pin those borrows —
|
|
// every read of a chained struct below must come after this, have_f2's last use.
|
|
let have_shader_int16 = have_f2.features.shader_int16;
|
|
let present_wait_ok = present_wait_exts
|
|
&& have_pid.present_id == vk::TRUE
|
|
&& have_pwait.present_wait == vk::TRUE;
|
|
let features_ok = have_f11.sampler_ycbcr_conversion == vk::TRUE
|
|
&& have_f12.timeline_semaphore == vk::TRUE
|
|
&& have_f13.synchronization2 == vk::TRUE;
|
|
// PyroWave decode (the wired-LAN wavelet codec, design/pyrowave-codec-plan.md §4.5):
|
|
// plain Vulkan-1.3 compute on THIS device — no video extensions. Probed alongside so a
|
|
// capable device gets the features enabled below and advertises the codec; anything
|
|
// less simply never sets the CODEC_PYROWAVE bit.
|
|
let pyrowave_ok = dev_is_13
|
|
&& have_shader_int16 == vk::TRUE
|
|
&& have_f12.storage_buffer8_bit_access == vk::TRUE
|
|
&& have_f12.timeline_semaphore == vk::TRUE
|
|
&& have_f13.subgroup_size_control == vk::TRUE
|
|
&& have_f13.compute_full_subgroups == vk::TRUE
|
|
&& have_f13.synchronization2 == vk::TRUE;
|
|
|
|
// The decode queue family + which codec operations it can run.
|
|
let decode_family: Option<(u32, vk::VideoCodecOperationFlagsKHR)> = {
|
|
let n = unsafe { instance.get_physical_device_queue_family_properties2_len(pdev) };
|
|
let mut video: Vec<vk::QueueFamilyVideoPropertiesKHR> =
|
|
vec![vk::QueueFamilyVideoPropertiesKHR::default(); n];
|
|
let mut props: Vec<vk::QueueFamilyProperties2> = video
|
|
.iter_mut()
|
|
.map(|v| vk::QueueFamilyProperties2::default().push_next(v))
|
|
.collect();
|
|
unsafe { instance.get_physical_device_queue_family_properties2(pdev, &mut props) };
|
|
// `props` mutably borrows `video` (push_next); copy the flags out, then
|
|
// read the driver-filled video properties directly.
|
|
let flags: Vec<vk::QueueFlags> = props
|
|
.iter()
|
|
.map(|p| p.queue_family_properties.queue_flags)
|
|
.collect();
|
|
drop(props);
|
|
flags
|
|
.iter()
|
|
.zip(&video)
|
|
.enumerate()
|
|
.find(|(_, (f, _))| f.contains(vk::QueueFlags::VIDEO_DECODE_KHR))
|
|
.map(|(i, (_, v))| (i as u32, v.video_codec_operations))
|
|
};
|
|
|
|
const VIDEO_BASE: [&std::ffi::CStr; 2] = [
|
|
ash::khr::video_queue::NAME,
|
|
ash::khr::video_decode_queue::NAME,
|
|
];
|
|
const VIDEO_CODECS: [&std::ffi::CStr; 3] = [
|
|
ash::khr::video_decode_h264::NAME,
|
|
ash::khr::video_decode_h265::NAME,
|
|
c"VK_KHR_video_decode_av1",
|
|
];
|
|
let codec_exts: Vec<&std::ffi::CStr> =
|
|
VIDEO_CODECS.into_iter().filter(|n| has(n)).collect();
|
|
let video_ok = dev_is_13
|
|
&& features_ok
|
|
&& decode_family.is_some()
|
|
&& VIDEO_BASE.iter().all(|n| has(n))
|
|
&& !codec_exts.is_empty();
|
|
|
|
let (decode_qf, decode_caps) = decode_family.unwrap_or((qfi, Default::default()));
|
|
let mut video_ext_names: Vec<&std::ffi::CStr> = Vec::new();
|
|
if video_ok {
|
|
video_ext_names.extend(VIDEO_BASE);
|
|
video_ext_names.extend(&codec_exts);
|
|
// Optional decoder niceties FFmpeg uses when present.
|
|
for opt in [c"VK_KHR_video_maintenance1", c"VK_KHR_video_maintenance2"] {
|
|
if has(opt) {
|
|
video_ext_names.push(opt);
|
|
}
|
|
}
|
|
dev_exts.extend(video_ext_names.iter().map(|n| n.as_ptr()));
|
|
tracing::info!(
|
|
decode_qf,
|
|
caps = ?decode_caps,
|
|
exts = ?video_ext_names,
|
|
"Vulkan Video decode available on this device"
|
|
);
|
|
} else {
|
|
tracing::info!(
|
|
dev_is_13,
|
|
features_ok,
|
|
decode_family = decode_family.is_some(),
|
|
"Vulkan Video decode unavailable — decoder falls back (VAAPI/software)"
|
|
);
|
|
}
|
|
|
|
// Present-id/present-wait: enable when fully supported — the presenter then runs
|
|
// the on-glass PresentTimer; otherwise the display stamp stays submit-time.
|
|
if present_wait_ok {
|
|
dev_exts.push(ash::khr::present_id::NAME.as_ptr());
|
|
dev_exts.push(ash::khr::present_wait::NAME.as_ptr());
|
|
}
|
|
let mut en_pid = vk::PhysicalDevicePresentIdFeaturesKHR::default().present_id(true);
|
|
let mut en_pwait = vk::PhysicalDevicePresentWaitFeaturesKHR::default().present_wait(true);
|
|
|
|
// Enable only the features the video path needs, and only where supported
|
|
// (harmless when the path is off; reported to FFmpeg via device_features).
|
|
let mut en_f11 = vk::PhysicalDeviceVulkan11Features::default()
|
|
.sampler_ycbcr_conversion(have_f11.sampler_ycbcr_conversion == vk::TRUE);
|
|
let mut en_f12 = vk::PhysicalDeviceVulkan12Features::default()
|
|
.timeline_semaphore(have_f12.timeline_semaphore == vk::TRUE)
|
|
.storage_buffer8_bit_access(pyrowave_ok)
|
|
.shader_float16(pyrowave_ok && have_f12.shader_float16 == vk::TRUE);
|
|
let mut en_f13 = vk::PhysicalDeviceVulkan13Features::default()
|
|
.synchronization2(have_f13.synchronization2 == vk::TRUE)
|
|
.subgroup_size_control(pyrowave_ok)
|
|
.compute_full_subgroups(pyrowave_ok);
|
|
let mut en_f2 = vk::PhysicalDeviceFeatures2::default()
|
|
.push_next(&mut en_f11)
|
|
.push_next(&mut en_f12)
|
|
.push_next(&mut en_f13);
|
|
if present_wait_ok {
|
|
en_f2 = en_f2.push_next(&mut en_pid).push_next(&mut en_pwait);
|
|
}
|
|
en_f2.features.shader_int16 = if pyrowave_ok { vk::TRUE } else { vk::FALSE };
|
|
|
|
let priorities = [1.0f32];
|
|
let mut queue_info = vec![vk::DeviceQueueCreateInfo::default()
|
|
.queue_family_index(qfi)
|
|
.queue_priorities(&priorities)];
|
|
if video_ok && decode_qf != qfi {
|
|
queue_info.push(
|
|
vk::DeviceQueueCreateInfo::default()
|
|
.queue_family_index(decode_qf)
|
|
.queue_priorities(&priorities),
|
|
);
|
|
}
|
|
let device = unsafe {
|
|
instance.create_device(
|
|
pdev,
|
|
&vk::DeviceCreateInfo::default()
|
|
.queue_create_infos(&queue_info)
|
|
.enabled_extension_names(&dev_exts)
|
|
.push_next(&mut en_f2),
|
|
None,
|
|
)
|
|
}
|
|
.context("vkCreateDevice")?;
|
|
let swap_d = ash::khr::swapchain::Device::new(&instance, &device);
|
|
let present_timer = present_wait_ok.then(|| {
|
|
super::present_timing::PresentTimer::spawn(ash::khr::present_wait::Device::new(
|
|
&instance, &device,
|
|
))
|
|
});
|
|
tracing::info!(
|
|
present_wait = present_wait_ok,
|
|
"on-glass present timing (VK_KHR_present_wait)"
|
|
);
|
|
let hdr_metadata_d =
|
|
has_hdr_metadata.then(|| ash::ext::hdr_metadata::Device::new(&instance, &device));
|
|
let queue = unsafe { device.get_device_queue(qfi, 0) };
|
|
#[cfg(target_os = "linux")]
|
|
let hw = if hw_capable {
|
|
Some(HwCtx {
|
|
ext_mem_fd: ash::khr::external_memory_fd::Device::new(&instance, &device),
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
#[cfg(windows)]
|
|
let hw_win = win_capable.then(|| HwCtxWin {
|
|
ext_mem_win32: ash::khr::external_memory_win32::Device::new(&instance, &device),
|
|
});
|
|
let csc = CscPass::new(&device, vk::Format::R8G8B8A8_UNORM)?;
|
|
// Starts SDR like `csc`; an HDR (PQ) pyrowave session rebuilds it at the 10-bit
|
|
// intermediate via `set_hdr_mode`, exactly like the H.26x pass.
|
|
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
|
|
let csc_planar = if pyrowave_ok {
|
|
Some(CscPass::new_planar(&device, vk::Format::R8G8B8A8_UNORM)?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// The exported handle bundle: FFmpeg Vulkan Video handles when the device can
|
|
// decode, AND (Windows) the D3D11-interop facts — so it's built whenever EITHER
|
|
// consumer needs it; `video_decode`/`d3d11_import` tell the decoder chain which
|
|
// paths are real. Extension lists must mirror creation exactly — FFmpeg keys its
|
|
// code paths off the strings.
|
|
// One lock per device for queue external sync (FFmpeg + Skia + this presenter
|
|
// all funnel their queue calls through it — see the `queue_lock` field docs).
|
|
let queue_lock = std::sync::Arc::new(pf_client_core::video::QueueLock::new());
|
|
#[cfg(windows)]
|
|
let export_worthy = video_ok || win_capable || pyrowave_ok;
|
|
#[cfg(not(windows))]
|
|
let export_worthy = video_ok || pyrowave_ok;
|
|
let video_export = if export_worthy {
|
|
let qf_props = unsafe { instance.get_physical_device_queue_family_properties(pdev) };
|
|
let mut device_extensions: Vec<CString> =
|
|
vec![CString::from(ash::khr::swapchain::NAME)];
|
|
#[cfg(target_os = "linux")]
|
|
if hw_capable {
|
|
device_extensions
|
|
.extend(dmabuf::DEVICE_EXTENSIONS.iter().map(|n| CString::from(*n)));
|
|
}
|
|
#[cfg(windows)]
|
|
if win_capable {
|
|
device_extensions.extend(
|
|
crate::d3d11::DEVICE_EXTENSIONS
|
|
.iter()
|
|
.map(|n| CString::from(*n)),
|
|
);
|
|
}
|
|
if has_hdr_metadata {
|
|
device_extensions.push(CString::from(ash::ext::hdr_metadata::NAME));
|
|
}
|
|
device_extensions.extend(video_ext_names.iter().map(|n| CString::from(*n)));
|
|
Some(pf_client_core::video::VulkanDecodeDevice {
|
|
get_instance_proc_addr: entry.static_fn().get_instance_proc_addr as usize,
|
|
instance: instance.handle().as_raw() as usize,
|
|
physical_device: pdev.as_raw() as usize,
|
|
device: device.handle().as_raw() as usize,
|
|
vendor_id: dev_props.vendor_id,
|
|
device_name: dev_props
|
|
.device_name_as_c_str()
|
|
.map(|c| c.to_string_lossy().into_owned())
|
|
.unwrap_or_default(),
|
|
graphics_qf: qfi,
|
|
graphics_queue_flags: qf_props[qfi as usize].queue_flags.as_raw(),
|
|
decode_qf,
|
|
decode_video_caps: decode_caps.as_raw(),
|
|
instance_extensions: instance_extensions
|
|
.iter()
|
|
.map(|e| CString::new(e.as_str()).unwrap())
|
|
.collect(),
|
|
device_extensions,
|
|
f_sampler_ycbcr: have_f11.sampler_ycbcr_conversion == vk::TRUE,
|
|
f_timeline_semaphore: have_f12.timeline_semaphore == vk::TRUE,
|
|
f_synchronization2: have_f13.synchronization2 == vk::TRUE,
|
|
f_shader_int16: pyrowave_ok,
|
|
f_storage_buffer8: pyrowave_ok,
|
|
f_subgroup_size_control: pyrowave_ok,
|
|
f_compute_full_subgroups: pyrowave_ok,
|
|
f_shader_float16: pyrowave_ok && have_f12.shader_float16 == vk::TRUE,
|
|
api_version: dev_props.api_version,
|
|
queue_families: queue_info.iter().map(|q| q.queue_family_index).collect(),
|
|
pyrowave_decode: pyrowave_ok,
|
|
video_decode: video_ok,
|
|
#[cfg(windows)]
|
|
d3d11_import: win_capable,
|
|
#[cfg(not(windows))]
|
|
d3d11_import: false,
|
|
// Filled in below — the HDR10 surface facts arrive with pick_formats.
|
|
d3d11_hdr10: false,
|
|
adapter_luid,
|
|
queue_lock: queue_lock.clone(),
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
#[cfg(windows)]
|
|
let mut video_export = video_export;
|
|
|
|
let (format, hdr10_format) = pick_formats(&surface_i, pdev, surface, has_colorspace_ext)?;
|
|
// The D3D11VA backend may emit its HDR (RGB10 PQ) ring only when this device can
|
|
// import the 10-bit texture AND the surface offers an HDR10 swapchain to pass it
|
|
// through to; otherwise a PQ stream keeps the decoder-side tonemap to sRGB.
|
|
#[cfg(windows)]
|
|
if let Some(v) = video_export.as_mut() {
|
|
v.d3d11_hdr10 = win_capable && import_rgb10 && hdr10_format.is_some();
|
|
}
|
|
let present_mode = pick_present_mode(&surface_i, pdev, surface)?;
|
|
tracing::info!(
|
|
?format,
|
|
?hdr10_format,
|
|
?present_mode,
|
|
hdr_metadata = has_hdr_metadata,
|
|
"swapchain config"
|
|
);
|
|
let overlay_pipe = OverlayPipe::new(&device, format.format)?;
|
|
|
|
let cmd_pool = unsafe {
|
|
device.create_command_pool(
|
|
&vk::CommandPoolCreateInfo::default()
|
|
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER)
|
|
.queue_family_index(qfi),
|
|
None,
|
|
)
|
|
}?;
|
|
let cmd_buf = unsafe {
|
|
device.allocate_command_buffers(
|
|
&vk::CommandBufferAllocateInfo::default()
|
|
.command_pool(cmd_pool)
|
|
.level(vk::CommandBufferLevel::PRIMARY)
|
|
.command_buffer_count(1),
|
|
)
|
|
}?[0];
|
|
let acquire_sem =
|
|
unsafe { device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None) }?;
|
|
let fence = unsafe {
|
|
device.create_fence(
|
|
&vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED),
|
|
None,
|
|
)
|
|
}?;
|
|
|
|
let mut p = Presenter {
|
|
entry,
|
|
instance,
|
|
surface_i,
|
|
surface,
|
|
pdev,
|
|
mem_props,
|
|
device,
|
|
swap_d,
|
|
queue,
|
|
qfi,
|
|
#[cfg(target_os = "linux")]
|
|
hw,
|
|
#[cfg(windows)]
|
|
hw_win,
|
|
csc,
|
|
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
|
|
csc_planar,
|
|
video_export,
|
|
overlay_pipe,
|
|
retired_hw: None,
|
|
queue_lock,
|
|
format,
|
|
hdr10_format,
|
|
hdr_active: false,
|
|
hdr_downgrade_warned: false,
|
|
hdr_metadata_d,
|
|
hdr_meta: None,
|
|
video_format: vk::Format::R8G8B8A8_UNORM,
|
|
present_mode,
|
|
swapchain: vk::SwapchainKHR::null(),
|
|
images: Vec::new(),
|
|
extent: vk::Extent2D::default(),
|
|
render_sems: Vec::new(),
|
|
acquire_sem,
|
|
fence,
|
|
cmd_pool,
|
|
cmd_buf,
|
|
staging: None,
|
|
video: None,
|
|
submitted: false,
|
|
present_timer,
|
|
next_present_id: 0,
|
|
last_presented: None,
|
|
};
|
|
p.recreate_swapchain(window)?;
|
|
Ok(p)
|
|
}
|
|
}
|
|
|
|
/// First physical device with a queue family that does graphics + present here;
|
|
/// `PUNKTFUNK_VK_DEVICE=<index>` overrides on multi-GPU boxes.
|
|
fn pick_device(
|
|
instance: &ash::Instance,
|
|
surface_i: &ash::khr::surface::Instance,
|
|
surface: vk::SurfaceKHR,
|
|
) -> Result<(vk::PhysicalDevice, u32)> {
|
|
let devices = unsafe { instance.enumerate_physical_devices() }?;
|
|
let forced: Option<usize> = std::env::var("PUNKTFUNK_VK_DEVICE")
|
|
.ok()
|
|
.and_then(|v| v.parse().ok());
|
|
let mut candidates: Vec<vk::PhysicalDevice> = match forced {
|
|
Some(i) => devices.get(i).copied().into_iter().collect(),
|
|
None => devices,
|
|
};
|
|
// Rank the candidates (stable sort; the index override wins outright):
|
|
// 1. The Settings GPU pick — `PUNKTFUNK_VK_ADAPTER` carries the adapter's marketing
|
|
// name (the WinUI shell's picker stores DXGI's, which matches Vulkan's for the
|
|
// same GPU): exact match, then substring, plain order when nothing matches
|
|
// (eGPU unplugged, stale setting).
|
|
// 2. Discrete over integrated: enumeration order puts the iGPU FIRST on some
|
|
// hybrids (observed: Ryzen iGPU ahead of an RTX dGPU), and the iGPU's video
|
|
// engine is the far weaker decoder — first-enumerated was a silent footgun.
|
|
if forced.is_none() {
|
|
let want = std::env::var("PUNKTFUNK_VK_ADAPTER")
|
|
.ok()
|
|
.map(|w| w.trim().to_lowercase())
|
|
.filter(|w| !w.is_empty());
|
|
candidates.sort_by_key(|d| {
|
|
let props = unsafe { instance.get_physical_device_properties(*d) };
|
|
let name = props
|
|
.device_name_as_c_str()
|
|
.map(|c| c.to_string_lossy().to_lowercase())
|
|
.unwrap_or_default();
|
|
let name_rank = match &want {
|
|
Some(w) if name == *w => 0,
|
|
Some(w) if name.contains(w.as_str()) || w.contains(&name) => 1,
|
|
Some(_) => 2,
|
|
None => 0,
|
|
};
|
|
let type_rank = match props.device_type {
|
|
vk::PhysicalDeviceType::DISCRETE_GPU => 0,
|
|
vk::PhysicalDeviceType::INTEGRATED_GPU => 1,
|
|
_ => 2,
|
|
};
|
|
(name_rank, type_rank)
|
|
});
|
|
}
|
|
for pdev in candidates {
|
|
let families = unsafe { instance.get_physical_device_queue_family_properties(pdev) };
|
|
for (i, f) in families.iter().enumerate() {
|
|
let graphics = f.queue_flags.contains(vk::QueueFlags::GRAPHICS);
|
|
let present =
|
|
unsafe { surface_i.get_physical_device_surface_support(pdev, i as u32, surface) }
|
|
.unwrap_or(false);
|
|
if graphics && present {
|
|
return Ok((pdev, i as u32));
|
|
}
|
|
}
|
|
}
|
|
bail!("no Vulkan device with a graphics+present queue family")
|
|
}
|
|
|
|
/// SDR: prefer BGRA8 UNORM (the near-universal presentable format); RGBA8 second; else
|
|
/// whatever the surface offers first. UNORM (not SRGB) — the decoded RGBA is already
|
|
/// display-referred, the blit must not re-encode it. HDR: a 10-bit UNORM format paired
|
|
/// with the HDR10/ST.2084 colorspace, when the instance ext + surface offer one (KDE/
|
|
/// gamescope with HDR enabled; absent elsewhere → the shader tonemaps instead).
|
|
pub(super) fn pick_formats(
|
|
surface_i: &ash::khr::surface::Instance,
|
|
pdev: vk::PhysicalDevice,
|
|
surface: vk::SurfaceKHR,
|
|
colorspace_ext: bool,
|
|
) -> Result<(vk::SurfaceFormatKHR, Option<vk::SurfaceFormatKHR>)> {
|
|
let formats = unsafe { surface_i.get_physical_device_surface_formats(pdev, surface) }?;
|
|
let mut sdr = None;
|
|
for want in [vk::Format::B8G8R8A8_UNORM, vk::Format::R8G8B8A8_UNORM] {
|
|
if let Some(f) = formats
|
|
.iter()
|
|
.find(|f| f.format == want && f.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR)
|
|
{
|
|
sdr = Some(*f);
|
|
break;
|
|
}
|
|
}
|
|
let sdr = sdr
|
|
.or_else(|| formats.first().copied())
|
|
.ok_or_else(|| anyhow!("surface offers no formats"))?;
|
|
let hdr10 = colorspace_ext
|
|
.then(|| {
|
|
formats
|
|
.iter()
|
|
.find(|f| {
|
|
f.color_space == vk::ColorSpaceKHR::HDR10_ST2084_EXT
|
|
&& matches!(
|
|
f.format,
|
|
vk::Format::A2B10G10R10_UNORM_PACK32
|
|
| vk::Format::A2R10G10B10_UNORM_PACK32
|
|
)
|
|
})
|
|
.copied()
|
|
})
|
|
.flatten();
|
|
Ok((sdr, hdr10))
|
|
}
|
|
|
|
/// MAILBOX when the surface offers it, FIFO otherwise (`PUNKTFUNK_PRESENT_MODE=
|
|
/// fifo|mailbox|immediate` overrides). Both are tear-free, but an arrival-paced
|
|
/// presenter must not block in FIFO's present queue: when the compositor holds images
|
|
/// for a vblank pass (gamescope's composite path) or arrival cadence drifts against
|
|
/// refresh, `acquire_next_image` stalls most of a refresh — a standing 11-13 ms added
|
|
/// to every frame at 60 Hz. MAILBOX never queues more than the newest frame, so the
|
|
/// pipeline stays at decode latency and a late frame is replaced, not waited for.
|
|
fn pick_present_mode(
|
|
surface_i: &ash::khr::surface::Instance,
|
|
pdev: vk::PhysicalDevice,
|
|
surface: vk::SurfaceKHR,
|
|
) -> Result<vk::PresentModeKHR> {
|
|
let modes = unsafe { surface_i.get_physical_device_surface_present_modes(pdev, surface) }?;
|
|
let want = match std::env::var("PUNKTFUNK_PRESENT_MODE").ok().as_deref() {
|
|
Some("fifo") => vk::PresentModeKHR::FIFO,
|
|
Some("immediate") => vk::PresentModeKHR::IMMEDIATE,
|
|
_ => vk::PresentModeKHR::MAILBOX,
|
|
};
|
|
Ok(if modes.contains(&want) {
|
|
want
|
|
} else {
|
|
vk::PresentModeKHR::FIFO // always available per spec
|
|
})
|
|
}
|