pf-presenter's 120 sites are `ash` calls almost without exception, so 120 independent arguments
would have been 120 restatements of the signature — the exact noise this program exists to remove.
They get the `abi.rs` treatment instead: the Vulkan contract stated once in `lib.rs`, each site
naming which of three shapes it is.
The three are not equal, and separating them is the point. CREATE and RECORD carry no real
precondition — the device is owned, the builders are locals, nothing executes until submit. DESTROY
does: the GPU must not still be using the object, and that is established by the path (a fence wait,
a `queue_wait_idle`, a retired swapchain), not by the call. Those sites say so, because getting it
wrong is a use-after-free no type catches. The contract also tells the next person that a block
outside the three shapes needs a real proof, and that writing "as above" is the signal it doesn't
belong in them.
punktfunk-core's Windows half is finished here too: `qos_windows.rs`'s `GetLastError` reads (called
before anything can reset the thread's error slot) and `udp/windows.rs`'s control-message write,
whose argument is that `ctrl` is sized by `WSA_CMSG_SPACE(4)` — computed two lines up — so header
plus payload cannot run past it, and `write_unaligned` is used because `WSA_CMSG_DATA` offers no
alignment guarantee.
⚠️ THE WINDOWS BLIND SPOT BIT A THIRD TIME. A Linux measurement put this crate pair at 113; the
real number was 129 — `d3d11.rs`, `win32.rs`, `qos_windows.rs`, `udp/windows.rs` are all
`cfg`-hidden. Every crate in this sweep had to be finished on .47 after being "done" on .21. For a
cross-platform crate the Linux number is a lower bound, never the answer.
All three crates now deny `undocumented_unsafe_blocks`, which was the goal: it applied to 8 of 11
crates carrying unsafe, and the three exempt ones held 381 items between them — including the C ABI
surface and the presenter. Verified: Linux .21 fmt + both CI clippy steps rc=0; Windows .47 all four
closed crates clippy `-D warnings` rc=0 plus the full Windows CI clippy set and pf-capture's tests.
Only small crates remain unguarded (75 items total, largest 26).
200 lines
8.8 KiB
Rust
200 lines
8.8 KiB
Rust
//! Video-image / staging-buffer (re)build + retired-frame destruction.
|
|
|
|
use super::gpu::subresource_range;
|
|
use super::{Presenter, Retired, Staging, VideoImage};
|
|
use anyhow::Result;
|
|
use ash::vk;
|
|
use pf_client_core::video::CpuFrame;
|
|
|
|
impl Retired {
|
|
pub(super) fn destroy(self, device: &ash::Device) {
|
|
match self {
|
|
#[cfg(target_os = "linux")]
|
|
Retired::Dmabuf(f) => f.destroy(device),
|
|
#[cfg(windows)]
|
|
Retired::D3d11(f) => f.destroy(device),
|
|
Retired::Vk { frame, views } => {
|
|
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned
|
|
// by this type and live for the call, and every builder struct is a local that
|
|
// outlives it.
|
|
unsafe {
|
|
for v in views {
|
|
device.destroy_image_view(v, None);
|
|
}
|
|
}
|
|
drop(frame); // guard drops here — AVFrame (and the VkImage) released
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Presenter {
|
|
/// Copy the frame's RGBA into the staging buffer and (re)build the video image on a
|
|
/// stream-size change. Rows keep their stride — `buffer_row_length` unpacks it.
|
|
pub(super) fn stage_frame(&mut self, f: &CpuFrame) -> Result<()> {
|
|
anyhow::ensure!(
|
|
f.stride % 4 == 0 && f.stride >= f.width as usize * 4,
|
|
"unexpected RGBA stride {} for width {}",
|
|
f.stride,
|
|
f.width
|
|
);
|
|
if self
|
|
.video
|
|
.as_ref()
|
|
.is_none_or(|v| v.width != f.width || v.height != f.height)
|
|
{
|
|
self.rebuild_video_image(f.width, f.height)?;
|
|
tracing::info!(width = f.width, height = f.height, "video image (re)built");
|
|
}
|
|
let needed = f.stride * f.height as usize;
|
|
if self.staging.as_ref().is_none_or(|s| s.capacity < needed) {
|
|
self.rebuild_staging(needed)?;
|
|
}
|
|
let s = self.staging.as_ref().unwrap();
|
|
let n = f.rgba.len().min(needed);
|
|
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this
|
|
// type and live for the call, and every builder struct is a local that outlives it.
|
|
unsafe { std::ptr::copy_nonoverlapping(f.rgba.as_ptr(), s.ptr, n) };
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn rebuild_video_image(&mut self, width: u32, height: u32) -> Result<()> {
|
|
// Fence-quiesce: the old image is only ever referenced by OUR command buffers.
|
|
self.quiesce_own()?;
|
|
if let Some(v) = self.video.take() {
|
|
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by
|
|
// this type and live for the call, and every builder struct is a local that outlives
|
|
// it.
|
|
unsafe {
|
|
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);
|
|
}
|
|
}
|
|
// COLOR_ATTACHMENT is the CSC pass's render target; harmless where hw is absent.
|
|
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this
|
|
// type and live for the call, and every builder struct is a local that outlives it.
|
|
let image = unsafe {
|
|
self.device.create_image(
|
|
&vk::ImageCreateInfo::default()
|
|
.image_type(vk::ImageType::TYPE_2D)
|
|
.format(self.video_format)
|
|
.extent(vk::Extent3D {
|
|
width,
|
|
height,
|
|
depth: 1,
|
|
})
|
|
.mip_levels(1)
|
|
.array_layers(1)
|
|
.samples(vk::SampleCountFlags::TYPE_1)
|
|
.tiling(vk::ImageTiling::OPTIMAL)
|
|
.usage(
|
|
vk::ImageUsageFlags::TRANSFER_DST
|
|
| vk::ImageUsageFlags::TRANSFER_SRC
|
|
| vk::ImageUsageFlags::COLOR_ATTACHMENT,
|
|
)
|
|
.initial_layout(vk::ImageLayout::UNDEFINED),
|
|
None,
|
|
)
|
|
}?;
|
|
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this
|
|
// type and live for the call, and every builder struct is a local that outlives it.
|
|
let reqs = unsafe { self.device.get_image_memory_requirements(image) };
|
|
let memory = self.allocate(reqs, vk::MemoryPropertyFlags::DEVICE_LOCAL)?;
|
|
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this
|
|
// type and live for the call, and every builder struct is a local that outlives it.
|
|
unsafe { self.device.bind_image_memory(image, memory, 0) }?;
|
|
// The CSC pass renders into it — view + framebuffer, unconditional (Vulkan-Video
|
|
// frames need the pass on every device, dmabuf-capable or not).
|
|
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this
|
|
// type and live for the call, and every builder struct is a local that outlives it.
|
|
let view = unsafe {
|
|
self.device.create_image_view(
|
|
&vk::ImageViewCreateInfo::default()
|
|
.image(image)
|
|
.view_type(vk::ImageViewType::TYPE_2D)
|
|
.format(self.video_format)
|
|
.subresource_range(subresource_range()),
|
|
None,
|
|
)
|
|
}?;
|
|
let attachments = [view];
|
|
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this
|
|
// type and live for the call, and every builder struct is a local that outlives it.
|
|
let framebuffer = unsafe {
|
|
self.device.create_framebuffer(
|
|
&vk::FramebufferCreateInfo::default()
|
|
.render_pass(self.csc.render_pass)
|
|
.attachments(&attachments)
|
|
.width(width)
|
|
.height(height)
|
|
.layers(1),
|
|
None,
|
|
)
|
|
}?;
|
|
self.video = Some(VideoImage {
|
|
image,
|
|
memory,
|
|
view,
|
|
framebuffer,
|
|
width,
|
|
height,
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
fn rebuild_staging(&mut self, capacity: usize) -> Result<()> {
|
|
self.quiesce_own()?;
|
|
if let Some(s) = self.staging.take() {
|
|
// SAFETY: per the Vulkan contract above - this destroys objects this type owns, and
|
|
// the GPU is known idle for them (the fence/queue-wait on the path here, or the
|
|
// swapchain being retired), which is the obligation that makes a destroy sound rather
|
|
// than the handle merely being non-null.
|
|
unsafe {
|
|
self.device.unmap_memory(s.memory);
|
|
self.device.destroy_buffer(s.buffer, None);
|
|
self.device.free_memory(s.memory, None);
|
|
}
|
|
}
|
|
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this
|
|
// type and live for the call, and every builder struct is a local that outlives it.
|
|
let buffer = unsafe {
|
|
self.device.create_buffer(
|
|
&vk::BufferCreateInfo::default()
|
|
.size(capacity as u64)
|
|
.usage(vk::BufferUsageFlags::TRANSFER_SRC)
|
|
.sharing_mode(vk::SharingMode::EXCLUSIVE),
|
|
None,
|
|
)
|
|
}?;
|
|
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this
|
|
// type and live for the call, and every builder struct is a local that outlives it.
|
|
let reqs = unsafe { self.device.get_buffer_memory_requirements(buffer) };
|
|
let memory = self.allocate(
|
|
reqs,
|
|
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
|
|
)?;
|
|
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this
|
|
// type and live for the call, and every builder struct is a local that outlives it.
|
|
unsafe { self.device.bind_buffer_memory(buffer, memory, 0) }?;
|
|
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this
|
|
// type and live for the call, and every builder struct is a local that outlives it.
|
|
let ptr = unsafe {
|
|
self.device
|
|
.map_memory(memory, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty())
|
|
}? as *mut u8;
|
|
self.staging = Some(Staging {
|
|
buffer,
|
|
memory,
|
|
ptr,
|
|
capacity,
|
|
});
|
|
Ok(())
|
|
}
|
|
}
|