Files
punktfunk/crates/pf-presenter/src/vk/gpu.rs
T
enricobuehlerandClaude Opus 4.8 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

258 lines
9.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Low-level GPU helpers: memory allocation, image barriers, AVVkFrame sync, geometry.
use super::Presenter;
use anyhow::{Context as _, Result};
use ash::vk;
impl Presenter {
/// Wait the in-flight fence: OUR command buffers are done (staging, video image,
/// old-swapchain images). Deliberately NOT `vkDeviceWaitIdle` — the pump thread
/// submits FFmpeg's Vulkan decode work concurrently, and wait-idle's external-sync
/// rule over every device queue would race it (observed as a resize crash).
pub(super) fn quiesce_own(&mut self) -> Result<()> {
unsafe {
if self.submitted {
self.device.wait_for_fences(&[self.fence], true, u64::MAX)?;
self.submitted = false;
}
}
Ok(())
}
pub(super) fn allocate(
&self,
reqs: vk::MemoryRequirements,
flags: vk::MemoryPropertyFlags,
) -> Result<vk::DeviceMemory> {
let type_index = (0..self.mem_props.memory_type_count)
.find(|&i| {
reqs.memory_type_bits & (1 << i) != 0
&& self.mem_props.memory_types[i as usize]
.property_flags
.contains(flags)
})
.with_context(|| format!("no memory type for {flags:?}"))?;
unsafe {
self.device.allocate_memory(
&vk::MemoryAllocateInfo::default()
.allocation_size(reqs.size)
.memory_type_index(type_index),
None,
)
}
.context("vkAllocateMemory")
}
}
/// The Contain-fit letterbox: video (vw×vh) into the swapchain extent, centered.
pub(super) fn letterbox(extent: vk::Extent2D, vw: u32, vh: u32) -> (vk::Offset3D, vk::Offset3D) {
let (ew, eh) = (f64::from(extent.width), f64::from(extent.height));
let scale = (ew / f64::from(vw.max(1))).min(eh / f64::from(vh.max(1)));
let dw = (f64::from(vw) * scale).round();
let dh = (f64::from(vh) * scale).round();
let ox = ((ew - dw) / 2.0).floor() as i32;
let oy = ((eh - dh) / 2.0).floor() as i32;
(
vk::Offset3D { x: ox, y: oy, z: 0 },
vk::Offset3D {
x: (ox + dw as i32).min(extent.width as i32),
y: (oy + dh as i32).min(extent.height as i32),
z: 1,
},
)
}
pub(super) fn subresource_layers() -> vk::ImageSubresourceLayers {
vk::ImageSubresourceLayers::default()
.aspect_mask(vk::ImageAspectFlags::COLOR)
.layer_count(1)
}
pub(super) fn subresource_range() -> vk::ImageSubresourceRange {
vk::ImageSubresourceRange::default()
.aspect_mask(vk::ImageAspectFlags::COLOR)
.level_count(1)
.layer_count(1)
}
/// Acquire a Vulkan-Video frame's image from the decode queue family (EXCLUSIVE
/// sharing) and transition it for sampling. `src_qf == dst_qf` (or IGNORED/CONCURRENT)
/// degrades to a plain layout transition. The matching decode-side acquire happens in
/// FFmpeg, keyed off the queue_family we write back after submission.
///
/// `srcStage` is FRAGMENT_SHADER — NOT TOP_OF_PIPE — deliberately: the submit waits the
/// frame's decode-complete timeline semaphore with `wait_dst_stage_mask =
/// FRAGMENT_SHADER`, and a semaphore wait only orders operations whose first sync scope
/// INTERSECTS that mask (the dependency-chain rule). With TOP_OF_PIPE the barrier's
/// layout transition (VIDEO_DECODE_DST/DPB → SHADER_READ_ONLY) formed no chain with the
/// wait and could execute while the decode queue was still writing the image. On RADV
/// that transition physically touches the image (metadata/decompression), so the race
/// showed as green/yellow block corruption exactly at freshly-decoded (damaged) regions
/// — the Steam Deck cursor-trail artifact. NVIDIA treats the transition as a no-op,
/// which is why the same code looked clean there.
pub(super) fn vkframe_acquire_barrier(
device: &ash::Device,
cmd: vk::CommandBuffer,
image: vk::Image,
old_layout: vk::ImageLayout,
src_qf: u32,
dst_qf: u32,
) {
let (src, dst) = if src_qf == dst_qf || src_qf == vk::QUEUE_FAMILY_IGNORED {
(vk::QUEUE_FAMILY_IGNORED, vk::QUEUE_FAMILY_IGNORED)
} else {
(src_qf, dst_qf)
};
let b = vk::ImageMemoryBarrier::default()
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::SHADER_READ)
.old_layout(old_layout)
.new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.src_queue_family_index(src)
.dst_queue_family_index(dst)
.image(image)
.subresource_range(subresource_range());
unsafe {
device.cmd_pipeline_barrier(
cmd,
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::DependencyFlags::empty(),
&[],
&[],
&[b],
);
}
}
/// Acquire an imported D3D11 texture from the EXTERNAL queue family as a copy source.
/// The keyed mutex on the submit is the actual cross-API ordering; per the
/// external-memory rules an UNDEFINED-old-layout transition on externally-bound memory
/// preserves the contents (unlike ordinary images), so this is purely the
/// layout/ownership hop.
#[cfg(windows)]
pub(super) fn external_acquire_barrier(
device: &ash::Device,
cmd: vk::CommandBuffer,
image: vk::Image,
qfi: u32,
) {
let b = vk::ImageMemoryBarrier::default()
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::TRANSFER_READ)
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
.src_queue_family_index(vk::QUEUE_FAMILY_EXTERNAL)
.dst_queue_family_index(qfi)
.image(image)
.subresource_range(subresource_range());
unsafe {
device.cmd_pipeline_barrier(
cmd,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[b],
);
}
}
/// Acquire a dmabuf plane image from its foreign owner (the VAAPI decoder): queue-family
/// transfer FOREIGN → ours, UNDEFINED → SHADER_READ_ONLY (content is preserved across
/// the transfer regardless of the UNDEFINED old-layout, per the external-memory rules).
#[cfg(target_os = "linux")]
pub(super) fn foreign_acquire_barrier(
device: &ash::Device,
cmd: vk::CommandBuffer,
image: vk::Image,
qfi: u32,
) {
let b = vk::ImageMemoryBarrier::default()
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::SHADER_READ)
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.src_queue_family_index(vk::QUEUE_FAMILY_FOREIGN_EXT)
.dst_queue_family_index(qfi)
.image(image)
.subresource_range(subresource_range());
unsafe {
device.cmd_pipeline_barrier(
cmd,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::DependencyFlags::empty(),
&[],
&[],
&[b],
);
}
}
/// A full-subresource layout transition with the conservative ALL_COMMANDS/TRANSFER
/// scopes this transfer-only pipeline needs (per-frame granularity, not per-stage).
pub(super) fn barrier(
device: &ash::Device,
cmd: vk::CommandBuffer,
image: vk::Image,
from: vk::ImageLayout,
to: vk::ImageLayout,
) {
let b = vk::ImageMemoryBarrier::default()
.src_access_mask(vk::AccessFlags::MEMORY_WRITE)
.dst_access_mask(vk::AccessFlags::MEMORY_READ | vk::AccessFlags::MEMORY_WRITE)
.old_layout(from)
.new_layout(to)
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.image(image)
.subresource_range(subresource_range());
unsafe {
device.cmd_pipeline_barrier(
cmd,
vk::PipelineStageFlags::ALL_COMMANDS,
vk::PipelineStageFlags::ALL_COMMANDS,
vk::DependencyFlags::empty(),
&[],
&[],
&[b],
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn letterbox_pillarboxes_a_wide_window() {
// 16:10 video in a 21:9-ish window: full height, centered horizontally.
let (a, b) = letterbox(
vk::Extent2D {
width: 3440,
height: 1440,
},
1280,
800,
);
assert_eq!((a.y, b.y), (0, 1440));
assert_eq!(b.x - a.x, 2304); // 1280 * (1440/800)
assert_eq!(a.x, (3440 - 2304) / 2);
}
#[test]
fn letterbox_matches_exact_fit() {
let (a, b) = letterbox(
vk::Extent2D {
width: 1280,
height: 800,
},
1280,
800,
);
assert_eq!((a.x, a.y), (0, 0));
assert_eq!((b.x, b.y), (1280, 800));
}
}