Files
punktfunk/crates/pf-presenter/src/vk/mod.rs
T
enricobuehler 61118cbdd4 refactor(presenter/W8): split vk.rs into vk/ directory module
Break the 2513-line pf-presenter/src/vk.rs into a vk/ directory module (mod.rs +
6 concern submodules), keeping ALL type definitions in vk/mod.rs so every submodule
(a descendant of vk) sees the Presenter/OverlayPipe/etc. private fields with no
field bumps:
  - vk/setup.rs      : Presenter::new + device/format/present-mode selection
  - vk/present.rs    : the per-frame present path (present + CSC record + AVVkFrame
                       sync) — HOT PATH, moved whole
  - vk/reconfig.rs   : swapchain recreate/resize + HDR reconfiguration
  - vk/resources.rs  : video-image/staging (re)build + Retired-frame destruction
  - vk/overlay_pipe.rs: the presenter-side overlay composite pipeline
  - vk/gpu.rs        : memory allocation, image barriers, geometry helpers (+ tests)
vk/mod.rs keeps FrameInput/Presenter/OverlayPipe/VideoImage/Staging/Retired/HwCtx*
+ the public accessors + Drop. Methods/free-fns a sibling submodule calls became
pub(super) (~18); zero field bumps, zero re-exports (Presenter/FrameInput never
leave mod.rs). lib.rs unchanged (`pub mod vk;` resolves to vk/mod.rs). The moved
overlay shader include_bytes! gained one `../` for the deeper dir. Pure move; no
behavior change; the hot present path keeps only static pub(super) calls (inlinable).

Verified both platforms: Linux (home-worker-5) clippy -p pf-presenter
(--all-targets -D warnings) + test; Windows (winbox, ASCII CARGO_HOME) clippy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:39:36 +02:00

290 lines
12 KiB
Rust

//! The Vulkan presenter: swapchain + two frame paths into one device-local RGBA video
//! image, then a letterboxed `vkCmdBlitImage` composite.
//!
//! * **Software** (`FrameInput::Cpu`): staging upload + `copy_buffer_to_image` (row
//! stride via `buffer_row_length`) — transfer-only, runs on every GPU.
//! * **Hardware** (`FrameInput::Dmabuf`): the decoder's NV12 dmabuf imported per-plane
//! (`dmabuf.rs`) and converted by the CSC render pass (`csc.rs`) — zero-copy, gated on
//! the four import extensions at device creation; boxes without them (NVIDIA
//! proprietary by design) report `supports_dmabuf() == false` and the caller keeps the
//! decoder on software.
//!
//! Pacing: one frame in flight (the submit fence is waited before each record), MAILBOX
//! when available, FIFO otherwise (`PUNKTFUNK_PRESENT_MODE=fifo|mailbox|immediate`
//! overrides — see `pick_present_mode` for why an arrival-paced presenter must not
//! block in FIFO's present queue). Present is arrival-paced by the caller: a frame
//! input on each decoded frame, `FrameInput::Redraw` re-blits the retained video image
//! (expose/resize redraws).
use crate::csc::CscPass;
#[cfg(target_os = "linux")]
use crate::dmabuf::HwFrame;
use crate::overlay::SharedDevice;
use ash::vk;
#[cfg(target_os = "linux")]
use pf_client_core::video::DmabufFrame;
use pf_client_core::video::{CpuFrame, VkVideoFrame};
mod gpu;
mod overlay_pipe;
mod present;
mod reconfig;
mod resources;
mod setup;
/// One presenter iteration's video input.
pub enum FrameInput<'a> {
/// No new frame — re-composite the retained video image (expose/resize).
Redraw,
Cpu(&'a CpuFrame),
#[cfg(target_os = "linux")]
Dmabuf(DmabufFrame),
/// FFmpeg Vulkan Video output — a VkImage already on THIS device (zero copy).
VkFrame(VkVideoFrame),
/// D3D11VA hand-off — a shareable NT-handle texture to import (`d3d11.rs`).
#[cfg(windows)]
D3d11(pf_client_core::video::D3d11Frame),
/// PyroWave planar output — three R8 plane views already on THIS device, decode
/// fence-complete, GENERAL layout (`pf_client_core::video_pyrowave`).
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
PyroWave(pf_client_core::video_pyrowave::PyroWavePlanarFrame),
}
/// The dmabuf/CSC machinery, present only when the device carries the import extensions.
#[cfg(target_os = "linux")]
struct HwCtx {
ext_mem_fd: ash::khr::external_memory_fd::Device,
}
/// The D3D11 shared-texture import machinery, present only when the device carries
/// `VK_KHR_external_memory_win32` + `VK_KHR_win32_keyed_mutex`.
#[cfg(windows)]
struct HwCtxWin {
ext_mem_win32: ash::khr::external_memory_win32::Device,
}
/// A submitted hardware frame parked until the in-flight fence proves the GPU reads
/// done: imported dmabuf planes, or a Vulkan-Video frame (FFmpeg's image — we own only
/// the plane views; dropping the frame's guard releases the AVFrame back to the pool).
enum Retired {
#[cfg(target_os = "linux")]
Dmabuf(HwFrame),
#[cfg(windows)]
D3d11(crate::d3d11::HwFrame),
Vk {
frame: VkVideoFrame,
views: [vk::ImageView; 2],
},
}
/// The overlay composite: one premultiplied-alpha quad blended over the swapchain image
/// after the video blit (the §6.1 contract's presenter half). Always built — it has no
/// Skia dependency and costs nothing while no overlay frame arrives (the render pass
/// isn't even recorded).
struct OverlayPipe {
render_pass: vk::RenderPass,
set_layout: vk::DescriptorSetLayout,
pipeline_layout: vk::PipelineLayout,
pipeline: vk::Pipeline,
desc_pool: vk::DescriptorPool,
desc_set: vk::DescriptorSet,
sampler: vk::Sampler,
/// Per-swapchain-image render targets, rebuilt with the swapchain.
views: Vec<vk::ImageView>,
framebuffers: Vec<vk::Framebuffer>,
}
/// The one video image (device-local RGBA the size of the decoded stream) + its staging.
/// `view`/`framebuffer` exist only on hw-capable devices (the CSC pass renders into it).
struct VideoImage {
image: vk::Image,
memory: vk::DeviceMemory,
view: vk::ImageView,
framebuffer: vk::Framebuffer,
width: u32,
height: u32,
}
struct Staging {
buffer: vk::Buffer,
memory: vk::DeviceMemory,
ptr: *mut u8,
capacity: usize,
}
pub struct Presenter {
// Field order = drop order documentation only; teardown is explicit in `Drop`.
entry: ash::Entry,
instance: ash::Instance,
surface_i: ash::khr::surface::Instance,
surface: vk::SurfaceKHR,
pdev: vk::PhysicalDevice,
mem_props: vk::PhysicalDeviceMemoryProperties,
device: ash::Device,
swap_d: ash::khr::swapchain::Device,
queue: vk::Queue,
qfi: u32,
/// Dmabuf import — `None` when the device lacks the import extensions (the CSC
/// pass itself is unconditional: Vulkan-Video frames need it everywhere).
#[cfg(target_os = "linux")]
hw: Option<HwCtx>,
/// D3D11 shared-texture import — `None` when the device lacks the win32 external
/// memory / keyed-mutex extensions.
#[cfg(windows)]
hw_win: Option<HwCtxWin>,
csc: CscPass,
/// The planar (3-plane) CSC variant for PyroWave frames; built only when the device
/// passed the pyrowave probe.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
csc_planar: Option<CscPass>,
/// FFmpeg Vulkan Video decode handles — `None` when the stack can't do it.
video_export: Option<pf_client_core::video::VulkanDecodeDevice>,
/// The console-UI composite quad (§6.1's presenter half).
overlay_pipe: OverlayPipe,
/// The submitted hardware frame (dmabuf plane images + guard, or a Vulkan-Video
/// frame + our plane views): its GPU reads end with the in-flight fence, so it's
/// destroyed right after the next fence wait.
retired_hw: Option<Retired>,
/// External-sync lock over this device's queues, shared with FFmpeg (via
/// [`pf_client_core::video::VulkanDecodeDevice::queue_lock`] → its
/// `lock_queue`/`unlock_queue` callbacks) and the Skia overlay: FFmpeg preps on the
/// SAME graphics queue from the pump thread, so every `vkQueueSubmit`/
/// `vkQueuePresentKHR`/`vkQueueWaitIdle`/`vkDeviceWaitIdle` here must hold it —
/// the unsynchronized overlap was an intermittent `VK_ERROR_DEVICE_LOST`.
queue_lock: std::sync::Arc<pf_client_core::video::QueueLock>,
format: vk::SurfaceFormatKHR,
/// The surface's HDR10/ST.2084 pairing, when the stack offers one.
hdr10_format: Option<vk::SurfaceFormatKHR>,
/// PQ frames are on screen and the swapchain is in HDR10 mode.
hdr_active: bool,
/// One-shot latch: a PQ frame arrived but the surface offers no HDR10 colorspace, so the
/// CSC pass silently tone-maps to SDR. Warned once — the single most useful signal for
/// diagnosing "HDR isn't advertised" (e.g. gamescope's WSI layer invisible in a flatpak
/// sandbox) vs. the host simply not sending PQ.
hdr_downgrade_warned: bool,
/// `VK_EXT_hdr_metadata` device fns when the driver offers them (gamescope/KDE do).
hdr_metadata_d: Option<ash::ext::hdr_metadata::Device>,
/// The host's latest ST.2086/CLL metadata (the 0xCE plane) — pushed to the
/// swapchain whenever HDR10 mode is live; `None` until the first datagram lands
/// (a generic HDR10 baseline is pushed meanwhile).
hdr_meta: Option<punktfunk_core::quic::HdrMeta>,
/// The video image / CSC attachment format for the current mode.
video_format: vk::Format,
present_mode: vk::PresentModeKHR,
swapchain: vk::SwapchainKHR,
images: Vec<vk::Image>,
extent: vk::Extent2D,
/// Per-swapchain-image render-finished semaphores (present consumes them on the
/// image's schedule — one shared semaphore could be re-submitted while a previous
/// present still holds it).
render_sems: Vec<vk::Semaphore>,
acquire_sem: vk::Semaphore,
fence: vk::Fence,
cmd_pool: vk::CommandPool,
cmd_buf: vk::CommandBuffer,
staging: Option<Staging>,
video: Option<VideoImage>,
/// The submit fence has a submission pending (wait before recording again — also
/// what makes the single staging buffer safe to overwrite).
submitted: bool,
}
impl Presenter {
/// Whether the hardware (dmabuf) path exists on this device — callers keep the
/// decoder on software when it doesn't.
#[cfg(target_os = "linux")]
pub fn supports_dmabuf(&self) -> bool {
self.hw.is_some()
}
/// Whether the D3D11 shared-texture path exists on this device — callers keep the
/// decoder on software when it doesn't.
#[cfg(windows)]
pub fn supports_d3d11(&self) -> bool {
self.hw_win.is_some()
}
/// The FFmpeg Vulkan Video decode handle bundle — `None` when this stack can't
/// (device < 1.3, missing video extensions/queue/features). The decoder chain
/// falls back to VAAPI/software then.
pub fn vulkan_decode(&self) -> Option<pf_client_core::video::VulkanDecodeDevice> {
self.video_export.clone()
}
/// Full device idle — TEARDOWN ONLY, and only after the session pump thread has
/// been joined (it submits FFmpeg decode work; wait-idle's external-sync rule
/// covers every queue on the device). Mid-session code uses the fence quiesce.
/// The queue lock is held as cheap insurance against a straggling submitter.
pub fn wait_idle(&self) {
let _q = self.queue_lock.guard();
unsafe { self.device.device_wait_idle() }.ok();
}
/// The device handles the console-UI overlay renders on (§6.1). Valid for the
/// presenter's lifetime; the run loop drops the overlay first.
pub fn shared_device(&self) -> SharedDevice {
SharedDevice {
entry: self.entry.clone(),
instance: self.instance.clone(),
physical_device: self.pdev,
device: self.device.clone(),
queue: self.queue,
queue_family_index: self.qfi,
queue_lock: self.queue_lock.clone(),
}
}
}
impl Drop for Presenter {
fn drop(&mut self) {
unsafe {
{
// Insurance against a straggling submitter (the run loop joins the
// pump before dropping us, so this is normally uncontended).
let _q = self.queue_lock.guard();
self.device.device_wait_idle().ok();
}
if let Some(f) = self.retired_hw.take() {
f.destroy(&self.device); // idle above — the GPU reads are done
}
if let Some(s) = self.staging.take() {
self.device.unmap_memory(s.memory);
self.device.destroy_buffer(s.buffer, None);
self.device.free_memory(s.memory, None);
}
if let Some(v) = self.video.take() {
if v.framebuffer != vk::Framebuffer::null() {
self.device.destroy_framebuffer(v.framebuffer, None);
}
if v.view != vk::ImageView::null() {
self.device.destroy_image_view(v.view, None);
}
self.device.destroy_image(v.image, None);
self.device.free_memory(v.memory, None);
}
#[cfg(target_os = "linux")]
self.hw.take();
self.csc.destroy(&self.device);
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
if let Some(p) = &self.csc_planar {
p.destroy(&self.device);
}
self.overlay_pipe.destroy(&self.device);
for s in self.render_sems.drain(..) {
self.device.destroy_semaphore(s, None);
}
self.device.destroy_semaphore(self.acquire_sem, None);
self.device.destroy_fence(self.fence, None);
self.device.destroy_command_pool(self.cmd_pool, None);
if self.swapchain != vk::SwapchainKHR::null() {
self.swap_d.destroy_swapchain(self.swapchain, None);
}
self.device.destroy_device(None);
self.surface_i.destroy_surface(self.surface, None);
self.instance.destroy_instance(None);
}
// `entry` (the libvulkan handle) drops last, after every vk call is done.
let _ = &self.entry;
}
}