fix(presenter,core): close the last of the proof-lint hole — the Vulkan contract, stated once

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).
This commit is contained in:
2026-07-29 08:48:42 +02:00
parent dda9ed1aa2
commit 65cd388a52
16 changed files with 339 additions and 4 deletions
+37
View File
@@ -91,6 +91,8 @@ impl CscPass {
.dst_stage_mask(vk::PipelineStageFlags::TRANSFER) .dst_stage_mask(vk::PipelineStageFlags::TRANSFER)
.dst_access_mask(vk::AccessFlags::TRANSFER_READ), .dst_access_mask(vk::AccessFlags::TRANSFER_READ),
]; ];
// 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 render_pass = unsafe { let render_pass = unsafe {
device.create_render_pass( device.create_render_pass(
&vk::RenderPassCreateInfo::default() &vk::RenderPassCreateInfo::default()
@@ -102,6 +104,8 @@ impl CscPass {
} }
.context("CSC render pass")?; .context("CSC render pass")?;
// 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 sampler = unsafe { let sampler = unsafe {
device.create_sampler( device.create_sampler(
&vk::SamplerCreateInfo::default() &vk::SamplerCreateInfo::default()
@@ -125,6 +129,8 @@ impl CscPass {
.immutable_samplers(&samplers) .immutable_samplers(&samplers)
}) })
.collect(); .collect();
// 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 set_layout = unsafe { let set_layout = unsafe {
device.create_descriptor_set_layout( device.create_descriptor_set_layout(
&vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings), &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
@@ -135,6 +141,8 @@ impl CscPass {
let push = [vk::PushConstantRange::default() let push = [vk::PushConstantRange::default()
.stage_flags(vk::ShaderStageFlags::FRAGMENT) .stage_flags(vk::ShaderStageFlags::FRAGMENT)
.size(64)]; // three vec4 rows + a params vec4 (mode, tonemap peak) .size(64)]; // three vec4 rows + a params vec4 (mode, tonemap peak)
// 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 pipeline_layout = unsafe { let pipeline_layout = unsafe {
device.create_pipeline_layout( device.create_pipeline_layout(
&vk::PipelineLayoutCreateInfo::default() &vk::PipelineLayoutCreateInfo::default()
@@ -147,6 +155,8 @@ impl CscPass {
let pool_sizes = [vk::DescriptorPoolSize::default() let pool_sizes = [vk::DescriptorPoolSize::default()
.ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) .ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(plane_bindings)]; .descriptor_count(plane_bindings)];
// 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 desc_pool = unsafe { let desc_pool = unsafe {
device.create_descriptor_pool( device.create_descriptor_pool(
&vk::DescriptorPoolCreateInfo::default() &vk::DescriptorPoolCreateInfo::default()
@@ -155,6 +165,8 @@ impl CscPass {
None, 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 desc_set = unsafe { let desc_set = unsafe {
device.allocate_descriptor_sets( device.allocate_descriptor_sets(
&vk::DescriptorSetAllocateInfo::default() &vk::DescriptorSetAllocateInfo::default()
@@ -203,6 +215,9 @@ impl CscPass {
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(&infos[1]), .image_info(&infos[1]),
]; ];
// SAFETY: per the Vulkan contract above - recorded into a command buffer this code owns
// and has begun, referencing handles it also owns; nothing is submitted until the
// recording is ended.
unsafe { device.update_descriptor_sets(&writes, &[]) }; unsafe { device.update_descriptor_sets(&writes, &[]) };
} }
@@ -223,10 +238,17 @@ impl CscPass {
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(&infos[b as usize]) .image_info(&infos[b as usize])
}); });
// SAFETY: per the Vulkan contract above - recorded into a command buffer this code owns
// and has begun, referencing handles it also owns; nothing is submitted until the
// recording is ended.
unsafe { device.update_descriptor_sets(&writes, &[]) }; unsafe { device.update_descriptor_sets(&writes, &[]) };
} }
pub fn destroy(&self, device: &ash::Device) { pub fn destroy(&self, device: &ash::Device) {
// 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 { unsafe {
device.destroy_pipeline(self.pipeline, None); device.destroy_pipeline(self.pipeline, None);
device.destroy_pipeline_layout(self.pipeline_layout, None); device.destroy_pipeline_layout(self.pipeline_layout, None);
@@ -255,15 +277,23 @@ pub(crate) fn build_fullscreen_pipeline(
&include_bytes!("../shaders/fullscreen.vert.spv")[..], &include_bytes!("../shaders/fullscreen.vert.spv")[..],
))?; ))?;
let frag = ash::util::read_spv(&mut std::io::Cursor::new(frag_spv))?; let frag = ash::util::read_spv(&mut std::io::Cursor::new(frag_spv))?;
// 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 vert_mod = unsafe { let vert_mod = unsafe {
device.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&vert), None) device.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&vert), 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 frag_mod = unsafe { let frag_mod = unsafe {
device.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&frag), None) device.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&frag), None)
}; };
let frag_mod = match frag_mod { let frag_mod = match frag_mod {
Ok(m) => m, Ok(m) => m,
Err(e) => { Err(e) => {
// 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 { device.destroy_shader_module(vert_mod, None) }; unsafe { device.destroy_shader_module(vert_mod, None) };
return Err(e).context("fragment shader module"); return Err(e).context("fragment shader module");
} }
@@ -325,9 +355,16 @@ pub(crate) fn build_fullscreen_pipeline(
.layout(layout) .layout(layout)
.render_pass(render_pass); .render_pass(render_pass);
let pipeline = let pipeline =
// SAFETY: per the Vulkan contract above - a create/allocate call on the live device, over
// builder structs that are locals outliving the call; the handle it returns is owned by
// the value being built here.
unsafe { device.create_graphics_pipelines(vk::PipelineCache::null(), &[info], None) } unsafe { device.create_graphics_pipelines(vk::PipelineCache::null(), &[info], None) }
.map_err(|(_, e)| e) .map_err(|(_, e)| e)
.context("CSC pipeline"); .context("CSC pipeline");
// 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 { unsafe {
device.destroy_shader_module(vert_mod, None); device.destroy_shader_module(vert_mod, None);
device.destroy_shader_module(frag_mod, None); device.destroy_shader_module(frag_mod, None);
+24
View File
@@ -48,6 +48,8 @@ fn format_importable(
.push_next(&mut ext_info); .push_next(&mut ext_info);
let mut ext_props = vk::ExternalImageFormatProperties::default(); let mut ext_props = vk::ExternalImageFormatProperties::default();
let mut props = vk::ImageFormatProperties2::default().push_next(&mut ext_props); let mut props = vk::ImageFormatProperties2::default().push_next(&mut ext_props);
// SAFETY: per the Vulkan contract in lib.rs - a read-only query on the live instance/device,
// filling locals returned by value.
unsafe { instance.get_physical_device_image_format_properties2(pdev, &fmt_info, &mut props) } unsafe { instance.get_physical_device_image_format_properties2(pdev, &fmt_info, &mut props) }
.is_ok() .is_ok()
&& ext_props && ext_props
@@ -90,6 +92,9 @@ impl HwFrame {
} }
pub fn destroy(self, device: &ash::Device) { pub fn destroy(self, device: &ash::Device) {
// SAFETY: per the Vulkan contract in lib.rs - destroys objects this type owns, on a path
// where the GPU is known idle for them; that idleness is the obligation, not the handle
// being non-null.
unsafe { unsafe {
device.destroy_image(self.image, None); device.destroy_image(self.image, None);
device.free_memory(self.memory, None); device.free_memory(self.memory, None);
@@ -122,6 +127,9 @@ pub fn import(
// the whole job. Kept maximally "identical" to the D3D11 resource (no view-format // the whole job. Kept maximally "identical" to the D3D11 resource (no view-format
// aliasing, no extra usages). // aliasing, no extra usages).
let mut external_info = vk::ExternalMemoryImageCreateInfo::default().handle_types(handle_type); let mut external_info = vk::ExternalMemoryImageCreateInfo::default().handle_types(handle_type);
// SAFETY: per the Vulkan contract in lib.rs - a create/allocate/import call on the live
// device, over builder structs that are locals outliving the call; the handle returned is
// owned by the value being built here.
let image = unsafe { let image = unsafe {
device.create_image( device.create_image(
&vk::ImageCreateInfo::default() &vk::ImageCreateInfo::default()
@@ -153,10 +161,14 @@ pub fn import(
// The handle's importable memory types, intersected with the image's requirement. // The handle's importable memory types, intersected with the image's requirement.
let handle = frame.handle as vk::HANDLE; let handle = frame.handle as vk::HANDLE;
let mut handle_props = vk::MemoryWin32HandlePropertiesKHR::default(); let mut handle_props = vk::MemoryWin32HandlePropertiesKHR::default();
// SAFETY: per the Vulkan contract in lib.rs - a read-only query on the live
// instance/device, filling locals returned by value.
unsafe { unsafe {
ext_mem_win32.get_memory_win32_handle_properties(handle_type, handle, &mut handle_props) ext_mem_win32.get_memory_win32_handle_properties(handle_type, handle, &mut handle_props)
} }
.context("vkGetMemoryWin32HandlePropertiesKHR")?; .context("vkGetMemoryWin32HandlePropertiesKHR")?;
// SAFETY: per the Vulkan contract in lib.rs - a read-only query on the live
// instance/device, filling locals returned by value.
let reqs = unsafe { device.get_image_memory_requirements(image) }; let reqs = unsafe { device.get_image_memory_requirements(image) };
let bits = reqs.memory_type_bits & handle_props.memory_type_bits; let bits = reqs.memory_type_bits & handle_props.memory_type_bits;
let type_index = (0..32u32) let type_index = (0..32u32)
@@ -169,6 +181,9 @@ pub fn import(
.handle_type(handle_type) .handle_type(handle_type)
.handle(handle); .handle(handle);
let mut dedicated = vk::MemoryDedicatedAllocateInfo::default().image(image); let mut dedicated = vk::MemoryDedicatedAllocateInfo::default().image(image);
// SAFETY: per the Vulkan contract in lib.rs - a create/allocate/import call on the live
// device, over builder structs that are locals outliving the call; the handle returned is
// owned by the value being built here.
let memory = unsafe { let memory = unsafe {
device.allocate_memory( device.allocate_memory(
&vk::MemoryAllocateInfo::default() &vk::MemoryAllocateInfo::default()
@@ -180,7 +195,13 @@ pub fn import(
) )
} }
.context("import D3D11 texture memory")?; .context("import D3D11 texture memory")?;
// SAFETY: per the Vulkan contract in lib.rs - a create/allocate/import call on the live
// device, over builder structs that are locals outliving the call; the handle returned is
// owned by the value being built here.
if let Err(e) = unsafe { device.bind_image_memory(image, memory, 0) } { if let Err(e) = unsafe { device.bind_image_memory(image, memory, 0) } {
// SAFETY: per the Vulkan contract in lib.rs - destroys objects this type owns, on a
// path where the GPU is known idle for them; that idleness is the obligation, not the
// handle being non-null.
unsafe { device.free_memory(memory, None) }; unsafe { device.free_memory(memory, None) };
return Err(e).context("bind imported memory"); return Err(e).context("bind imported memory");
} }
@@ -189,6 +210,9 @@ pub fn import(
let memory = match result { let memory = match result {
Ok(m) => m, Ok(m) => m,
Err(e) => { Err(e) => {
// SAFETY: per the Vulkan contract in lib.rs - destroys objects this type owns, on a
// path where the GPU is known idle for them; that idleness is the obligation, not the
// handle being non-null.
unsafe { device.destroy_image(image, None) }; unsafe { device.destroy_image(image, None) };
return Err(e); return Err(e);
} }
+34
View File
@@ -61,6 +61,8 @@ impl HwFrame {
} }
pub fn destroy(self, device: &ash::Device) { pub fn destroy(self, device: &ash::Device) {
// 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 { unsafe {
for v in self.views { for v in self.views {
device.destroy_image_view(v, None); device.destroy_image_view(v, None);
@@ -136,6 +138,10 @@ pub fn import(
{ {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
// 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 { unsafe {
device.destroy_image(luma_img, None); device.destroy_image(luma_img, None);
device.free_memory(luma_mem, None); device.free_memory(luma_mem, None);
@@ -145,6 +151,9 @@ pub fn import(
}; };
let view = |image, format| { let view = |image, format| {
// SAFETY: per the Vulkan contract above - a create/allocate call on the live device, over
// builder structs that are locals outliving the call; the handle it returns is owned by
// the value being built here.
unsafe { unsafe {
device.create_image_view( device.create_image_view(
&vk::ImageViewCreateInfo::default() &vk::ImageViewCreateInfo::default()
@@ -162,6 +171,10 @@ pub fn import(
} }
.context("plane image view") .context("plane image view")
}; };
// 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.
let destroy_images = |views: &[vk::ImageView]| unsafe { let destroy_images = |views: &[vk::ImageView]| unsafe {
for v in views { for v in views {
device.destroy_image_view(*v, None); device.destroy_image_view(*v, None);
@@ -228,6 +241,8 @@ fn plane_image(
.plane_layouts(&plane_layouts); .plane_layouts(&plane_layouts);
let mut external_info = vk::ExternalMemoryImageCreateInfo::default() let mut external_info = vk::ExternalMemoryImageCreateInfo::default()
.handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT); .handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT);
// 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 { let image = unsafe {
device.create_image( device.create_image(
&vk::ImageCreateInfo::default() &vk::ImageCreateInfo::default()
@@ -256,6 +271,9 @@ fn plane_image(
let result = (|| { let result = (|| {
// The fd's importable memory types, intersected with the image's requirement. // The fd's importable memory types, intersected with the image's requirement.
let mut fd_props = vk::MemoryFdPropertiesKHR::default(); let mut fd_props = vk::MemoryFdPropertiesKHR::default();
// SAFETY: per the Vulkan contract above - a create/allocate call on the live device, over
// builder structs that are locals outliving the call; the handle it returns is owned by
// the value being built here.
unsafe { unsafe {
ext_mem_fd.get_memory_fd_properties( ext_mem_fd.get_memory_fd_properties(
vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT, vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT,
@@ -264,6 +282,8 @@ fn plane_image(
) )
} }
.context("vkGetMemoryFdPropertiesKHR")?; .context("vkGetMemoryFdPropertiesKHR")?;
// 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 { device.get_image_memory_requirements(image) }; let reqs = unsafe { device.get_image_memory_requirements(image) };
let bits = reqs.memory_type_bits & fd_props.memory_type_bits; let bits = reqs.memory_type_bits & fd_props.memory_type_bits;
let type_index = (0..32u32) let type_index = (0..32u32)
@@ -271,6 +291,8 @@ fn plane_image(
.context("no importable memory type for dmabuf")?; .context("no importable memory type for dmabuf")?;
// Vulkan owns the fd it imports — dup so the decoder guard keeps the original. // Vulkan owns the fd it imports — dup so the decoder guard keeps the original.
// 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 owned = unsafe { BorrowedFd::borrow_raw(fd) } let owned = unsafe { BorrowedFd::borrow_raw(fd) }
.try_clone_to_owned() .try_clone_to_owned()
.context("dup dmabuf fd")?; .context("dup dmabuf fd")?;
@@ -278,6 +300,8 @@ fn plane_image(
.handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT) .handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT)
.fd(owned.into_raw_fd()); .fd(owned.into_raw_fd());
let mut dedicated = vk::MemoryDedicatedAllocateInfo::default().image(image); let mut dedicated = vk::MemoryDedicatedAllocateInfo::default().image(image);
// 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 memory = unsafe { let memory = unsafe {
device.allocate_memory( device.allocate_memory(
&vk::MemoryAllocateInfo::default() &vk::MemoryAllocateInfo::default()
@@ -290,7 +314,13 @@ fn plane_image(
} }
.context("import dmabuf memory")?; .context("import dmabuf memory")?;
// (On allocate_memory failure Vulkan still closed the dup'd fd — nothing leaks.) // (On allocate_memory failure Vulkan still closed the dup'd fd — nothing leaks.)
// 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.
if let Err(e) = unsafe { device.bind_image_memory(image, memory, 0) } { if let Err(e) = unsafe { device.bind_image_memory(image, memory, 0) } {
// 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 { device.free_memory(memory, None) }; unsafe { device.free_memory(memory, None) };
return Err(e).context("bind imported memory"); return Err(e).context("bind imported memory");
} }
@@ -300,6 +330,10 @@ fn plane_image(
match result { match result {
Ok(memory) => Ok((image, memory)), Ok(memory) => Ok((image, memory)),
Err(e) => { Err(e) => {
// 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 { device.destroy_image(image, None) }; unsafe { device.destroy_image(image, None) };
Err(e) Err(e)
} }
+23
View File
@@ -14,6 +14,29 @@
//! Windows) and `d3d11` is its Windows counterpart (D3D11VA shared-texture import) — //! Windows) and `d3d11` is its Windows counterpart (D3D11VA shared-texture import) —
//! the decode chain there is Vulkan → D3D11VA → software. //! the decode chain there is Vulkan → D3D11VA → software.
// Unsafe-proof program: every `unsafe {}` in this crate carries a `// SAFETY:` proof.
#![deny(clippy::undocumented_unsafe_blocks)]
// THE VULKAN CONTRACT, stated once - most `// SAFETY:` proofs in this crate are an instance of it.
//
// Nearly every `unsafe` here is an `ash` call, which is `unsafe` because Vulkan is a C API, not
// because each call carries its own bespoke obligation. Three shapes recur, and only one of them
// has a real precondition worth restating per site:
//
// * CREATE / ALLOCATE - `create_*`, `allocate_*`. The device is live (this type owns it), and the
// `vk::*CreateInfo` builders are locals that outlive the synchronous call. The handle returned is
// owned by the value being constructed and destroyed in its `Drop`.
// * RECORD - `cmd_*`, `begin/end_command_buffer`, `update_descriptor_sets`. Recorded into a command
// buffer this code owns and has begun, referencing handles it also owns. Nothing executes until
// submit, so a recording error is not yet a memory error.
// * DESTROY - `destroy_*`, `free_*`, `unmap_memory`. THIS is the one with a real obligation: the
// GPU must not still be using the object. That is established on the path, not by the call - a
// fence wait, a `queue_wait_idle`, or the swapchain having been retired - and the per-site proofs
// say so, because getting it wrong is a use-after-free the type system cannot catch.
//
// A block doing something OUTSIDE these three shapes gets a real, specific proof; if you add one and
// find yourself writing "as above", it probably belongs in one of them.
#[cfg(any(target_os = "linux", windows))] #[cfg(any(target_os = "linux", windows))]
pub mod csc; pub mod csc;
#[cfg(any(target_os = "linux", windows))] #[cfg(any(target_os = "linux", windows))]
+2
View File
@@ -1704,6 +1704,8 @@ fn apply_capture(
/// forwarded as touch passthrough. An unknown/invalid id (INVALID) reads as not-direct. /// forwarded as touch passthrough. An unknown/invalid id (INVALID) reads as not-direct.
fn is_direct_touch(touch_id: u64) -> bool { fn is_direct_touch(touch_id: u64) -> bool {
use sdl3::sys::touch::{SDL_GetTouchDeviceType, SDL_TouchDeviceType, SDL_TouchID}; use sdl3::sys::touch::{SDL_GetTouchDeviceType, SDL_TouchDeviceType, SDL_TouchID};
// 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 { SDL_GetTouchDeviceType(SDL_TouchID(touch_id)) == SDL_TouchDeviceType::DIRECT } unsafe { SDL_GetTouchDeviceType(SDL_TouchID(touch_id)) == SDL_TouchDeviceType::DIRECT }
} }
+16
View File
@@ -10,6 +10,8 @@ impl Presenter {
/// submits FFmpeg's Vulkan decode work concurrently, and wait-idle's external-sync /// 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). /// rule over every device queue would race it (observed as a resize crash).
pub(super) fn quiesce_own(&mut self) -> Result<()> { pub(super) fn quiesce_own(&mut self) -> Result<()> {
// 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 { unsafe {
if self.submitted { if self.submitted {
self.device.wait_for_fences(&[self.fence], true, u64::MAX)?; self.device.wait_for_fences(&[self.fence], true, u64::MAX)?;
@@ -31,6 +33,9 @@ impl Presenter {
.contains(flags) .contains(flags)
}) })
.with_context(|| format!("no memory type for {flags:?}"))?; .with_context(|| format!("no memory type for {flags:?}"))?;
// SAFETY: per the Vulkan contract above - a create/allocate call on the live device, over
// builder structs that are locals outliving the call; the handle it returns is owned by
// the value being built here.
unsafe { unsafe {
self.device.allocate_memory( self.device.allocate_memory(
&vk::MemoryAllocateInfo::default() &vk::MemoryAllocateInfo::default()
@@ -111,6 +116,9 @@ pub(super) fn vkframe_acquire_barrier(
.dst_queue_family_index(dst) .dst_queue_family_index(dst)
.image(image) .image(image)
.subresource_range(subresource_range()); .subresource_range(subresource_range());
// SAFETY: per the Vulkan contract above - recorded into a command buffer this code owns and
// has begun, referencing handles it also owns; nothing is submitted until the recording is
// ended.
unsafe { unsafe {
device.cmd_pipeline_barrier( device.cmd_pipeline_barrier(
cmd, cmd,
@@ -145,6 +153,8 @@ pub(super) fn external_acquire_barrier(
.dst_queue_family_index(qfi) .dst_queue_family_index(qfi)
.image(image) .image(image)
.subresource_range(subresource_range()); .subresource_range(subresource_range());
// SAFETY: per the Vulkan contract in lib.rs - recorded into a command buffer this code owns
// and has begun, referencing handles it also owns; nothing runs until submit.
unsafe { unsafe {
device.cmd_pipeline_barrier( device.cmd_pipeline_barrier(
cmd, cmd,
@@ -177,6 +187,9 @@ pub(super) fn foreign_acquire_barrier(
.dst_queue_family_index(qfi) .dst_queue_family_index(qfi)
.image(image) .image(image)
.subresource_range(subresource_range()); .subresource_range(subresource_range());
// SAFETY: per the Vulkan contract above - recorded into a command buffer this code owns and
// has begun, referencing handles it also owns; nothing is submitted until the recording is
// ended.
unsafe { unsafe {
device.cmd_pipeline_barrier( device.cmd_pipeline_barrier(
cmd, cmd,
@@ -208,6 +221,9 @@ pub(super) fn barrier(
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.image(image) .image(image)
.subresource_range(subresource_range()); .subresource_range(subresource_range());
// SAFETY: per the Vulkan contract above - recorded into a command buffer this code owns and
// has begun, referencing handles it also owns; nothing is submitted until the recording is
// ended.
unsafe { unsafe {
device.cmd_pipeline_barrier( device.cmd_pipeline_barrier(
cmd, cmd,
+4
View File
@@ -230,6 +230,8 @@ impl Presenter {
/// The queue lock is held as cheap insurance against a straggling submitter. /// The queue lock is held as cheap insurance against a straggling submitter.
pub fn wait_idle(&self) { pub fn wait_idle(&self) {
let _q = self.queue_lock.guard(); let _q = self.queue_lock.guard();
// 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.device_wait_idle() }.ok(); unsafe { self.device.device_wait_idle() }.ok();
} }
@@ -278,6 +280,8 @@ impl Drop for Presenter {
// after in-flight waits complete, bounded by their 250 ms cap) BEFORE the // after in-flight waits complete, bounded by their 250 ms cap) BEFORE the
// swapchain teardown below. // swapchain teardown below.
self.present_timer.take(); self.present_timer.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 { unsafe {
{ {
// Insurance against a straggling submitter (the run loop joins the // Insurance against a straggling submitter (the run loop joins the
@@ -32,6 +32,8 @@ impl OverlayPipe {
.dst_access_mask( .dst_access_mask(
vk::AccessFlags::COLOR_ATTACHMENT_READ | vk::AccessFlags::COLOR_ATTACHMENT_WRITE, vk::AccessFlags::COLOR_ATTACHMENT_READ | vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
)]; )];
// 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 render_pass = unsafe { let render_pass = unsafe {
device.create_render_pass( device.create_render_pass(
&vk::RenderPassCreateInfo::default() &vk::RenderPassCreateInfo::default()
@@ -43,6 +45,8 @@ impl OverlayPipe {
} }
.context("overlay render pass")?; .context("overlay render pass")?;
// 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 sampler = unsafe { let sampler = unsafe {
device.create_sampler( device.create_sampler(
&vk::SamplerCreateInfo::default() &vk::SamplerCreateInfo::default()
@@ -61,6 +65,8 @@ impl OverlayPipe {
.descriptor_count(1) .descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT) .stage_flags(vk::ShaderStageFlags::FRAGMENT)
.immutable_samplers(&samplers)]; .immutable_samplers(&samplers)];
// 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 set_layout = unsafe { let set_layout = unsafe {
device.create_descriptor_set_layout( device.create_descriptor_set_layout(
&vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings), &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
@@ -68,6 +74,8 @@ impl OverlayPipe {
) )
}?; }?;
let set_layouts = [set_layout]; let set_layouts = [set_layout];
// 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 pipeline_layout = unsafe { let pipeline_layout = unsafe {
device.create_pipeline_layout( device.create_pipeline_layout(
&vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts), &vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts),
@@ -77,6 +85,8 @@ impl OverlayPipe {
let pool_sizes = [vk::DescriptorPoolSize::default() let pool_sizes = [vk::DescriptorPoolSize::default()
.ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) .ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)]; .descriptor_count(1)];
// 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 desc_pool = unsafe { let desc_pool = unsafe {
device.create_descriptor_pool( device.create_descriptor_pool(
&vk::DescriptorPoolCreateInfo::default() &vk::DescriptorPoolCreateInfo::default()
@@ -85,6 +95,8 @@ impl OverlayPipe {
None, 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 desc_set = unsafe { let desc_set = unsafe {
device.allocate_descriptor_sets( device.allocate_descriptor_sets(
&vk::DescriptorSetAllocateInfo::default() &vk::DescriptorSetAllocateInfo::default()
@@ -131,6 +143,9 @@ impl OverlayPipe {
) -> Result<()> { ) -> Result<()> {
self.destroy_targets(device); // no-op after take_targets; safety net otherwise self.destroy_targets(device); // no-op after take_targets; safety net otherwise
for &image in images { for &image in images {
// 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 { let view = unsafe {
device.create_image_view( device.create_image_view(
&vk::ImageViewCreateInfo::default() &vk::ImageViewCreateInfo::default()
@@ -143,6 +158,9 @@ impl OverlayPipe {
}?; }?;
self.views.push(view); self.views.push(view);
let attachments = [view]; 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 fb = unsafe { let fb = unsafe {
device.create_framebuffer( device.create_framebuffer(
&vk::FramebufferCreateInfo::default() &vk::FramebufferCreateInfo::default()
@@ -160,6 +178,8 @@ impl OverlayPipe {
} }
fn destroy_targets(&mut self, device: &ash::Device) { fn destroy_targets(&mut self, device: &ash::Device) {
// 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 { unsafe {
for fb in self.framebuffers.drain(..) { for fb in self.framebuffers.drain(..) {
device.destroy_framebuffer(fb, None); device.destroy_framebuffer(fb, None);
@@ -172,6 +192,10 @@ impl OverlayPipe {
pub(super) fn destroy(&mut self, device: &ash::Device) { pub(super) fn destroy(&mut self, device: &ash::Device) {
self.destroy_targets(device); self.destroy_targets(device);
// 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 { unsafe {
device.destroy_pipeline(self.pipeline, None); device.destroy_pipeline(self.pipeline, None);
device.destroy_pipeline_layout(self.pipeline_layout, None); device.destroy_pipeline_layout(self.pipeline_layout, None);
+33 -4
View File
@@ -113,6 +113,8 @@ impl Presenter {
// One frame in flight: the fence covers the command buffer, the staging buffer // One frame in flight: the fence covers the command buffer, the staging buffer
// AND the previously submitted hw frame — waiting makes all three reusable. // AND the previously submitted hw frame — waiting makes all three reusable.
// 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 { unsafe {
if self.submitted { if self.submitted {
self.device.wait_for_fences(&[self.fence], true, u64::MAX)?; self.device.wait_for_fences(&[self.fence], true, u64::MAX)?;
@@ -189,9 +191,14 @@ impl Presenter {
.dst_binding(0) .dst_binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(&infos)]; .image_info(&infos)];
// SAFETY: per the Vulkan contract above - recorded into a command buffer this code
// owns and has begun, referencing handles it also owns; nothing is submitted until the
// recording is ended.
unsafe { self.device.update_descriptor_sets(&writes, &[]) }; unsafe { self.device.update_descriptor_sets(&writes, &[]) };
} }
// 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 (index, _suboptimal) = match unsafe { let (index, _suboptimal) = match unsafe {
self.swap_d.acquire_next_image( self.swap_d.acquire_next_image(
self.swapchain, self.swapchain,
@@ -218,6 +225,9 @@ impl Presenter {
}; };
let swap_image = self.images[index as usize]; let swap_image = self.images[index as usize];
// SAFETY: per the Vulkan contract above - recorded into a command buffer this code owns
// and has begun, referencing handles it also owns; nothing is submitted until the
// recording is ended.
unsafe { unsafe {
self.device.begin_command_buffer( self.device.begin_command_buffer(
self.cmd_buf, self.cmd_buf,
@@ -628,6 +638,9 @@ impl Presenter {
depth: u8, depth: u8,
msb_packed: bool, msb_packed: bool,
) { ) {
// SAFETY: per the Vulkan contract above - recorded into a command buffer this code owns
// and has begun, referencing handles it also owns; nothing is submitted until the
// recording is ended.
unsafe { unsafe {
self.device.cmd_begin_render_pass( self.device.cmd_begin_render_pass(
self.cmd_buf, self.cmd_buf,
@@ -714,6 +727,9 @@ impl Presenter {
let Some(planar) = self.csc_planar.as_ref() else { let Some(planar) = self.csc_planar.as_ref() else {
return; return;
}; };
// SAFETY: per the Vulkan contract above - recorded into a command buffer this code owns
// and has begun, referencing handles it also owns; nothing is submitted until the
// recording is ended.
unsafe { unsafe {
self.device.cmd_begin_render_pass( self.device.cmd_begin_render_pass(
self.cmd_buf, self.cmd_buf,
@@ -816,11 +832,16 @@ impl Presenter {
); );
}; };
// img[0] is creation-constant (only the sync fields need the frames lock). // img[0] is creation-constant (only the sync fields need the frames lock).
let image = let image = vk::Image::from_raw(
vk::Image::from_raw( // SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned
unsafe { (*(f.vkframe as *const pf_ffvk::AVVkFrame)).img[0] } as u64, // by this type and live for the call, and every builder struct is a local that
); // outlives it.
unsafe { (*(f.vkframe as *const pf_ffvk::AVVkFrame)).img[0] } as u64,
);
let make = |aspect: vk::ImageAspectFlags, format: vk::Format| { let make = |aspect: vk::ImageAspectFlags, format: vk::Format| {
// SAFETY: per the Vulkan contract above - a create/allocate call on the live device,
// over builder structs that are locals outliving the call; the handle it returns is
// owned by the value being built here.
unsafe { unsafe {
self.device.create_image_view( self.device.create_image_view(
&vk::ImageViewCreateInfo::default() &vk::ImageViewCreateInfo::default()
@@ -842,6 +863,10 @@ impl Presenter {
let chroma = match make(vk::ImageAspectFlags::PLANE_1, chroma_fmt) { let chroma = match make(vk::ImageAspectFlags::PLANE_1, chroma_fmt) {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
// 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.destroy_image_view(luma, None) }; unsafe { self.device.destroy_image_view(luma, None) };
return Err(e); return Err(e);
} }
@@ -871,6 +896,8 @@ struct VkFrameSync {
// is required on one platform and a no-op on the other. // is required on one platform and a no-op on the other.
#[allow(clippy::unnecessary_cast)] #[allow(clippy::unnecessary_cast)]
fn lock_vkframe(f: &VkVideoFrame) -> VkFrameSync { fn lock_vkframe(f: &VkVideoFrame) -> VkFrameSync {
// 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 { unsafe {
let lock: unsafe extern "C" fn(*mut pf_ffvk::AVHWFramesContext, *mut pf_ffvk::AVVkFrame) = let lock: unsafe extern "C" fn(*mut pf_ffvk::AVHWFramesContext, *mut pf_ffvk::AVVkFrame) =
std::mem::transmute(f.lock_frame); std::mem::transmute(f.lock_frame);
@@ -890,6 +917,8 @@ fn lock_vkframe(f: &VkVideoFrame) -> VkFrameSync {
/// Write the post-submission state back (FFmpeg waits these on its next use of the /// Write the post-submission state back (FFmpeg waits these on its next use of the
/// frame) and release the lock. On a failed submit only the lock is released. /// frame) and release the lock. On a failed submit only the lock is released.
fn unlock_vkframe(f: &VkVideoFrame, sync: &VkFrameSync, submitted: bool, graphics_qf: u32) { fn unlock_vkframe(f: &VkVideoFrame, sync: &VkFrameSync, submitted: bool, graphics_qf: u32) {
// 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 { unsafe {
let vkf = f.vkframe as *mut pf_ffvk::AVVkFrame; let vkf = f.vkframe as *mut pf_ffvk::AVVkFrame;
if submitted { if submitted {
@@ -58,6 +58,9 @@ impl PresentTimer {
.spawn(move || { .spawn(move || {
while let Ok(job) = rx.recv() { while let Ok(job) = rx.recv() {
// 250 ms cap — see the module doc's lifecycle note. // 250 ms cap — see the module doc's lifecycle note.
// 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 r = unsafe { let r = unsafe {
wait_d.wait_for_present(job.swapchain, job.present_id, 250_000_000) wait_d.wait_for_present(job.swapchain, job.present_id, 250_000_000)
}; };
+23
View File
@@ -47,10 +47,15 @@ impl Presenter {
// the stream for a frame, and only happens on resize/HDR-flip/OUT_OF_DATE. // the stream for a frame, and only happens on resize/HDR-flip/OUT_OF_DATE.
{ {
let _q = self.queue_lock.guard(); let _q = self.queue_lock.guard();
// 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.queue_wait_idle(self.queue) } unsafe { self.device.queue_wait_idle(self.queue) }
.context("vkQueueWaitIdle (swapchain recreate)")?; .context("vkQueueWaitIdle (swapchain recreate)")?;
} }
// 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 caps = unsafe { let caps = unsafe {
self.surface_i self.surface_i
.get_physical_device_surface_capabilities(self.pdev, self.surface) .get_physical_device_surface_capabilities(self.pdev, self.surface)
@@ -91,6 +96,9 @@ impl Presenter {
.present_mode(self.present_mode) .present_mode(self.present_mode)
.clipped(true) .clipped(true)
.old_swapchain(old); .old_swapchain(old);
// SAFETY: per the Vulkan contract above - a create/allocate call on the live device, over
// builder structs that are locals outliving the call; the handle it returns is owned by
// the value being built here.
let swapchain = unsafe { self.swap_d.create_swapchain(&info, None) } let swapchain = unsafe { self.swap_d.create_swapchain(&info, None) }
.map_err(|e| anyhow!("vkCreateSwapchainKHR: {e}{}", kmsdrm_swapchain_hint()))?; .map_err(|e| anyhow!("vkCreateSwapchainKHR: {e}{}", kmsdrm_swapchain_hint()))?;
// The old swapchain and everything tied to its images dies NOW: the fence // The old swapchain and everything tied to its images dies NOW: the fence
@@ -105,6 +113,8 @@ impl Presenter {
// An unclaimed last present belonged to the dying swapchain — drop the claim. // An unclaimed last present belonged to the dying swapchain — drop the claim.
self.last_presented = None; self.last_presented = None;
let (overlay_views, overlay_framebuffers) = self.overlay_pipe.take_targets(); let (overlay_views, overlay_framebuffers) = self.overlay_pipe.take_targets();
// 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 { unsafe {
for fb in overlay_framebuffers { for fb in overlay_framebuffers {
self.device.destroy_framebuffer(fb, None); self.device.destroy_framebuffer(fb, None);
@@ -120,6 +130,8 @@ impl Presenter {
} }
} }
self.swapchain = swapchain; self.swapchain = swapchain;
// SAFETY: per the Vulkan contract above - a read-only query on the live instance/device,
// filling locals returned by value.
self.images = unsafe { self.swap_d.get_swapchain_images(swapchain) }?; self.images = unsafe { self.swap_d.get_swapchain_images(swapchain) }?;
self.extent = extent; self.extent = extent;
self.overlay_pipe.rebuild_targets( self.overlay_pipe.rebuild_targets(
@@ -130,6 +142,9 @@ impl Presenter {
)?; )?;
for _ in 0..self.images.len() { for _ in 0..self.images.len() {
// 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.
self.render_sems.push(unsafe { self.render_sems.push(unsafe {
self.device self.device
.create_semaphore(&vk::SemaphoreCreateInfo::default(), None) .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
@@ -205,6 +220,8 @@ impl Presenter {
.min_luminance(m.min_display_mastering_luminance as f32 / 10_000.0) .min_luminance(m.min_display_mastering_luminance as f32 / 10_000.0)
.max_content_light_level(m.max_cll as f32) .max_content_light_level(m.max_cll as f32)
.max_frame_average_light_level(m.max_fall as f32); .max_frame_average_light_level(m.max_fall as f32);
// 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 { ext.set_hdr_metadata(&[self.swapchain], &[md]) }; unsafe { ext.set_hdr_metadata(&[self.swapchain], &[md]) };
tracing::debug!(from_host = self.hdr_meta.is_some(), "HDR metadata pushed"); tracing::debug!(from_host = self.hdr_meta.is_some(), "HDR metadata pushed");
} }
@@ -238,6 +255,10 @@ impl Presenter {
self.csc_planar = Some(CscPass::new_planar(&self.device, self.video_format)?); self.csc_planar = Some(CscPass::new_planar(&self.device, self.video_format)?);
} }
if let Some(v) = self.video.take() { if let Some(v) = self.video.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 { unsafe {
self.device.destroy_framebuffer(v.framebuffer, None); self.device.destroy_framebuffer(v.framebuffer, None);
self.device.destroy_image_view(v.view, None); self.device.destroy_image_view(v.view, None);
@@ -254,6 +275,8 @@ impl Presenter {
OverlayPipe::new(&self.device, target.format)?, OverlayPipe::new(&self.device, target.format)?,
); );
let (overlay_views, overlay_framebuffers) = old_pipe.take_targets(); let (overlay_views, overlay_framebuffers) = old_pipe.take_targets();
// 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 { unsafe {
for fb in overlay_framebuffers { for fb in overlay_framebuffers {
self.device.destroy_framebuffer(fb, None); self.device.destroy_framebuffer(fb, None);
+30
View File
@@ -14,6 +14,9 @@ impl Retired {
#[cfg(windows)] #[cfg(windows)]
Retired::D3d11(f) => f.destroy(device), Retired::D3d11(f) => f.destroy(device),
Retired::Vk { frame, views } => { 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 { unsafe {
for v in views { for v in views {
device.destroy_image_view(v, None); device.destroy_image_view(v, None);
@@ -49,6 +52,8 @@ impl Presenter {
} }
let s = self.staging.as_ref().unwrap(); let s = self.staging.as_ref().unwrap();
let n = f.rgba.len().min(needed); 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) }; unsafe { std::ptr::copy_nonoverlapping(f.rgba.as_ptr(), s.ptr, n) };
Ok(()) Ok(())
} }
@@ -57,6 +62,9 @@ impl Presenter {
// Fence-quiesce: the old image is only ever referenced by OUR command buffers. // Fence-quiesce: the old image is only ever referenced by OUR command buffers.
self.quiesce_own()?; self.quiesce_own()?;
if let Some(v) = self.video.take() { 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 { unsafe {
if v.framebuffer != vk::Framebuffer::null() { if v.framebuffer != vk::Framebuffer::null() {
self.device.destroy_framebuffer(v.framebuffer, None); self.device.destroy_framebuffer(v.framebuffer, None);
@@ -69,6 +77,8 @@ impl Presenter {
} }
} }
// COLOR_ATTACHMENT is the CSC pass's render target; harmless where hw is absent. // 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 { let image = unsafe {
self.device.create_image( self.device.create_image(
&vk::ImageCreateInfo::default() &vk::ImageCreateInfo::default()
@@ -92,11 +102,17 @@ impl Presenter {
None, 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 reqs = unsafe { self.device.get_image_memory_requirements(image) };
let memory = self.allocate(reqs, vk::MemoryPropertyFlags::DEVICE_LOCAL)?; 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) }?; unsafe { self.device.bind_image_memory(image, memory, 0) }?;
// The CSC pass renders into it — view + framebuffer, unconditional (Vulkan-Video // The CSC pass renders into it — view + framebuffer, unconditional (Vulkan-Video
// frames need the pass on every device, dmabuf-capable or not). // 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 { let view = unsafe {
self.device.create_image_view( self.device.create_image_view(
&vk::ImageViewCreateInfo::default() &vk::ImageViewCreateInfo::default()
@@ -108,6 +124,8 @@ impl Presenter {
) )
}?; }?;
let attachments = [view]; 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 { let framebuffer = unsafe {
self.device.create_framebuffer( self.device.create_framebuffer(
&vk::FramebufferCreateInfo::default() &vk::FramebufferCreateInfo::default()
@@ -133,12 +151,18 @@ impl Presenter {
fn rebuild_staging(&mut self, capacity: usize) -> Result<()> { fn rebuild_staging(&mut self, capacity: usize) -> Result<()> {
self.quiesce_own()?; self.quiesce_own()?;
if let Some(s) = self.staging.take() { 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 { unsafe {
self.device.unmap_memory(s.memory); self.device.unmap_memory(s.memory);
self.device.destroy_buffer(s.buffer, None); self.device.destroy_buffer(s.buffer, None);
self.device.free_memory(s.memory, 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 { let buffer = unsafe {
self.device.create_buffer( self.device.create_buffer(
&vk::BufferCreateInfo::default() &vk::BufferCreateInfo::default()
@@ -148,12 +172,18 @@ impl Presenter {
None, 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 reqs = unsafe { self.device.get_buffer_memory_requirements(buffer) };
let memory = self.allocate( let memory = self.allocate(
reqs, reqs,
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, 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) }?; 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 { let ptr = unsafe {
self.device self.device
.map_memory(memory, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty()) .map_memory(memory, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty())
+68
View File
@@ -17,6 +17,9 @@ impl Presenter {
/// Bring up instance → surface → device → swapchain over an SDL window. /// Bring up instance → surface → device → swapchain over an SDL window.
/// `instance_extensions` comes from `VideoSubsystem::vulkan_instance_extensions()`. /// `instance_extensions` comes from `VideoSubsystem::vulkan_instance_extensions()`.
pub fn new(window: &sdl3::video::Window, instance_extensions: &[String]) -> Result<Presenter> { pub fn new(window: &sdl3::video::Window, instance_extensions: &[String]) -> Result<Presenter> {
// SAFETY: per the Vulkan contract above - a create/allocate call on the live device, over
// builder structs that are locals outliving the call; the handle it returns is owned by
// the value being built here.
let entry = unsafe { ash::Entry::load() }.context("libvulkan not loadable")?; let entry = unsafe { ash::Entry::load() }.context("libvulkan not loadable")?;
let app_name = CString::new("punktfunk-session").unwrap(); let app_name = CString::new("punktfunk-session").unwrap();
@@ -29,6 +32,8 @@ impl Presenter {
// HDR10 presentation needs the extended colorspaces at the INSTANCE level. // HDR10 presentation needs the extended colorspaces at the INSTANCE level.
let mut instance_extensions: Vec<String> = instance_extensions.to_vec(); let mut instance_extensions: Vec<String> = instance_extensions.to_vec();
let inst_available = let inst_available =
// SAFETY: per the Vulkan contract above - a read-only query on the live
// instance/device, filling locals returned by value.
unsafe { entry.enumerate_instance_extension_properties(None) }.unwrap_or_default(); unsafe { entry.enumerate_instance_extension_properties(None) }.unwrap_or_default();
let has_colorspace_ext = inst_available let has_colorspace_ext = inst_available
.iter() .iter()
@@ -44,6 +49,8 @@ impl Presenter {
// hardcoded `*const i8` compiles on the desktop targets and fails to match ash's // hardcoded `*const i8` compiles on the desktop targets and fails to match ash's
// `&[*const c_char]` on ARM. // `&[*const c_char]` on ARM.
let ext_ptrs: Vec<*const c_char> = ext_cstrings.iter().map(|e| e.as_ptr()).collect(); let ext_ptrs: Vec<*const c_char> = ext_cstrings.iter().map(|e| e.as_ptr()).collect();
// 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 instance = unsafe { let instance = unsafe {
entry.create_instance( entry.create_instance(
&vk::InstanceCreateInfo::default() &vk::InstanceCreateInfo::default()
@@ -55,12 +62,19 @@ impl Presenter {
.context("vkCreateInstance")?; .context("vkCreateInstance")?;
let surface_i = ash::khr::surface::Instance::new(&entry, &instance); let surface_i = ash::khr::surface::Instance::new(&entry, &instance);
// SAFETY: per the Vulkan contract above - a create/allocate call on the live device, over
// builder structs that are locals outliving the call; the handle it returns is owned by
// the value being built here.
let surface = unsafe { window.vulkan_create_surface(instance.handle()) } let surface = unsafe { window.vulkan_create_surface(instance.handle()) }
.map_err(|e| anyhow!("SDL_Vulkan_CreateSurface: {e}"))?; .map_err(|e| anyhow!("SDL_Vulkan_CreateSurface: {e}"))?;
let (pdev, qfi) = pick_device(&instance, &surface_i, surface)?; let (pdev, qfi) = pick_device(&instance, &surface_i, surface)?;
// SAFETY: per the Vulkan contract above - a read-only query on the live instance/device,
// filling locals returned by value.
let mem_props = unsafe { instance.get_physical_device_memory_properties(pdev) }; let mem_props = unsafe { instance.get_physical_device_memory_properties(pdev) };
{ {
// SAFETY: per the Vulkan contract above - a read-only query on the live
// instance/device, filling locals returned by value.
let props = unsafe { instance.get_physical_device_properties(pdev) }; let props = unsafe { instance.get_physical_device_properties(pdev) };
let name = props let name = props
.device_name_as_c_str() .device_name_as_c_str()
@@ -72,6 +86,8 @@ impl Presenter {
// The dmabuf import set is optional: enabled when the device offers all four, // The dmabuf import set is optional: enabled when the device offers all four,
// else that path is off (`supports_dmabuf() == false`). Windows has no // else that path is off (`supports_dmabuf() == false`). Windows has no
// dmabuf/DRM-PRIME — the whole import path is compiled out there. // dmabuf/DRM-PRIME — the whole import path is compiled out there.
// SAFETY: per the Vulkan contract above - a read-only query on the live instance/device,
// filling locals returned by value.
let available = unsafe { instance.enumerate_device_extension_properties(pdev) }?; let available = unsafe { instance.enumerate_device_extension_properties(pdev) }?;
let has = |name: &std::ffi::CStr| { let has = |name: &std::ffi::CStr| {
available available
@@ -113,6 +129,8 @@ impl Presenter {
// SAME adapter). Core 1.1 query; valid on effectively every Windows driver. // SAME adapter). Core 1.1 query; valid on effectively every Windows driver.
let mut id_props = vk::PhysicalDeviceIDProperties::default(); let mut id_props = vk::PhysicalDeviceIDProperties::default();
let mut props2 = vk::PhysicalDeviceProperties2::default().push_next(&mut id_props); let mut props2 = vk::PhysicalDeviceProperties2::default().push_next(&mut id_props);
// SAFETY: per the Vulkan contract above - a read-only query on the live instance/device,
// filling locals returned by value.
unsafe { instance.get_physical_device_properties2(pdev, &mut props2) }; unsafe { instance.get_physical_device_properties2(pdev, &mut props2) };
let adapter_luid: Option<[u8; 8]> = let adapter_luid: Option<[u8; 8]> =
(id_props.device_luid_valid == vk::TRUE).then_some(id_props.device_luid); (id_props.device_luid_valid == vk::TRUE).then_some(id_props.device_luid);
@@ -130,6 +148,8 @@ impl Presenter {
// Probed, never required: a capable stack gets the video extensions, a second // Probed, never required: a capable stack gets the video extensions, a second
// (decode) queue, and the features FFmpeg's decoder needs; anything less means // (decode) queue, and the features FFmpeg's decoder needs; anything less means
// `vulkan_decode() == None` and the decoder chain falls back (VAAPI/software). // `vulkan_decode() == None` and the decoder chain falls back (VAAPI/software).
// SAFETY: per the Vulkan contract above - a read-only query on the live instance/device,
// filling locals returned by value.
let dev_props = unsafe { instance.get_physical_device_properties(pdev) }; let dev_props = unsafe { instance.get_physical_device_properties(pdev) };
let dev_is_13 = vk::api_version_major(dev_props.api_version) > 1 let dev_is_13 = vk::api_version_major(dev_props.api_version) > 1
|| vk::api_version_minor(dev_props.api_version) >= 3; || vk::api_version_minor(dev_props.api_version) >= 3;
@@ -149,6 +169,8 @@ impl Presenter {
if present_wait_exts { if present_wait_exts {
have_f2 = have_f2.push_next(&mut have_pid).push_next(&mut have_pwait); have_f2 = have_f2.push_next(&mut have_pid).push_next(&mut have_pwait);
} }
// SAFETY: per the Vulkan contract above - a read-only query on the live instance/device,
// filling locals returned by value.
unsafe { instance.get_physical_device_features2(pdev, &mut have_f2) }; unsafe { instance.get_physical_device_features2(pdev, &mut have_f2) };
// Copy the one base-features fact out NOW: `have_f2` mutably borrows the chained // 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 — // structs through its pNext chain, so any later use of it would pin those borrows —
@@ -174,6 +196,8 @@ impl Presenter {
// The decode queue family + which codec operations it can run. // The decode queue family + which codec operations it can run.
let decode_family: Option<(u32, vk::VideoCodecOperationFlagsKHR)> = { let decode_family: Option<(u32, vk::VideoCodecOperationFlagsKHR)> = {
// SAFETY: per the Vulkan contract above - a read-only query on the live
// instance/device, filling locals returned by value.
let n = unsafe { instance.get_physical_device_queue_family_properties2_len(pdev) }; let n = unsafe { instance.get_physical_device_queue_family_properties2_len(pdev) };
let mut video: Vec<vk::QueueFamilyVideoPropertiesKHR> = let mut video: Vec<vk::QueueFamilyVideoPropertiesKHR> =
vec![vk::QueueFamilyVideoPropertiesKHR::default(); n]; vec![vk::QueueFamilyVideoPropertiesKHR::default(); n];
@@ -181,6 +205,8 @@ impl Presenter {
.iter_mut() .iter_mut()
.map(|v| vk::QueueFamilyProperties2::default().push_next(v)) .map(|v| vk::QueueFamilyProperties2::default().push_next(v))
.collect(); .collect();
// SAFETY: per the Vulkan contract above - a read-only query on the live
// instance/device, filling locals returned by value.
unsafe { instance.get_physical_device_queue_family_properties2(pdev, &mut props) }; unsafe { instance.get_physical_device_queue_family_properties2(pdev, &mut props) };
// `props` mutably borrows `video` (push_next); copy the flags out, then // `props` mutably borrows `video` (push_next); copy the flags out, then
// read the driver-filled video properties directly. // read the driver-filled video properties directly.
@@ -282,6 +308,8 @@ impl Presenter {
.queue_priorities(&priorities), .queue_priorities(&priorities),
); );
} }
// 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 device = unsafe { let device = unsafe {
instance.create_device( instance.create_device(
pdev, pdev,
@@ -305,6 +333,8 @@ impl Presenter {
); );
let hdr_metadata_d = let hdr_metadata_d =
has_hdr_metadata.then(|| ash::ext::hdr_metadata::Device::new(&instance, &device)); has_hdr_metadata.then(|| ash::ext::hdr_metadata::Device::new(&instance, &device));
// SAFETY: per the Vulkan contract above - a read-only query on the live instance/device,
// filling locals returned by value.
let queue = unsafe { device.get_device_queue(qfi, 0) }; let queue = unsafe { device.get_device_queue(qfi, 0) };
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let hw = if hw_capable { let hw = if hw_capable {
@@ -341,6 +371,8 @@ impl Presenter {
#[cfg(not(windows))] #[cfg(not(windows))]
let export_worthy = video_ok || pyrowave_ok; let export_worthy = video_ok || pyrowave_ok;
let video_export = if export_worthy { let video_export = if export_worthy {
// SAFETY: per the Vulkan contract above - a read-only query on the live
// instance/device, filling locals returned by value.
let qf_props = unsafe { instance.get_physical_device_queue_family_properties(pdev) }; let qf_props = unsafe { instance.get_physical_device_queue_family_properties(pdev) };
let mut device_extensions: Vec<CString> = let mut device_extensions: Vec<CString> =
vec![CString::from(ash::khr::swapchain::NAME)]; vec![CString::from(ash::khr::swapchain::NAME)];
@@ -425,6 +457,9 @@ impl Presenter {
); );
let overlay_pipe = OverlayPipe::new(&device, format.format)?; let overlay_pipe = OverlayPipe::new(&device, format.format)?;
// SAFETY: per the Vulkan contract above - recorded into a command buffer this code owns
// and has begun, referencing handles it also owns; nothing is submitted until the
// recording is ended.
let cmd_pool = unsafe { let cmd_pool = unsafe {
device.create_command_pool( device.create_command_pool(
&vk::CommandPoolCreateInfo::default() &vk::CommandPoolCreateInfo::default()
@@ -433,6 +468,9 @@ impl Presenter {
None, None,
) )
}?; }?;
// SAFETY: per the Vulkan contract above - recorded into a command buffer this code owns
// and has begun, referencing handles it also owns; nothing is submitted until the
// recording is ended.
let cmd_buf = unsafe { let cmd_buf = unsafe {
device.allocate_command_buffers( device.allocate_command_buffers(
&vk::CommandBufferAllocateInfo::default() &vk::CommandBufferAllocateInfo::default()
@@ -442,7 +480,12 @@ impl Presenter {
) )
}?[0]; }?[0];
let acquire_sem = let acquire_sem =
// SAFETY: per the Vulkan contract above - a create/allocate call on the live device,
// over builder structs that are locals outliving the call; the handle it returns is
// owned by the value being built here.
unsafe { device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None) }?; unsafe { device.create_semaphore(&vk::SemaphoreCreateInfo::default(), 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 fence = unsafe { let fence = unsafe {
device.create_fence( device.create_fence(
&vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED), &vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED),
@@ -506,11 +549,16 @@ impl Presenter {
/// the whole `PUNKTFUNK_VK_ADAPTER` match key, so a second identical card adds nothing). /// the whole `PUNKTFUNK_VK_ADAPTER` match key, so a second identical card adds nothing).
/// Same 1.3 instance the presenter creates, so the list matches what streaming sees. /// Same 1.3 instance the presenter creates, so the list matches what streaming sees.
pub fn list_adapters() -> Result<Vec<String>> { pub fn list_adapters() -> Result<Vec<String>> {
// SAFETY: per the Vulkan contract above - a create/allocate call on the live device, over
// builder structs that are locals outliving the call; the handle it returns is owned by the
// value being built here.
let entry = unsafe { ash::Entry::load() }.context("libvulkan not loadable")?; let entry = unsafe { ash::Entry::load() }.context("libvulkan not loadable")?;
let app_name = CString::new("punktfunk-session").unwrap(); let app_name = CString::new("punktfunk-session").unwrap();
let app_info = vk::ApplicationInfo::default() let app_info = vk::ApplicationInfo::default()
.application_name(&app_name) .application_name(&app_name)
.api_version(vk::API_VERSION_1_3); .api_version(vk::API_VERSION_1_3);
// 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 instance = unsafe { let instance = unsafe {
entry.create_instance( entry.create_instance(
&vk::InstanceCreateInfo::default().application_info(&app_info), &vk::InstanceCreateInfo::default().application_info(&app_info),
@@ -518,9 +566,13 @@ pub fn list_adapters() -> Result<Vec<String>> {
) )
} }
.context("vkCreateInstance")?; .context("vkCreateInstance")?;
// SAFETY: per the Vulkan contract above - a read-only query on the live instance/device,
// filling locals returned by value.
let mut ranked: Vec<(u8, String)> = unsafe { instance.enumerate_physical_devices() }? let mut ranked: Vec<(u8, String)> = unsafe { instance.enumerate_physical_devices() }?
.into_iter() .into_iter()
.map(|d| { .map(|d| {
// SAFETY: per the Vulkan contract above - a read-only query on the live
// instance/device, filling locals returned by value.
let props = unsafe { instance.get_physical_device_properties(d) }; let props = unsafe { instance.get_physical_device_properties(d) };
let rank = match props.device_type { let rank = match props.device_type {
vk::PhysicalDeviceType::DISCRETE_GPU => 0u8, vk::PhysicalDeviceType::DISCRETE_GPU => 0u8,
@@ -535,6 +587,10 @@ pub fn list_adapters() -> Result<Vec<String>> {
}) })
.filter(|(_, n)| !n.is_empty()) .filter(|(_, n)| !n.is_empty())
.collect(); .collect();
// 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 { instance.destroy_instance(None) }; unsafe { instance.destroy_instance(None) };
ranked.sort_by_key(|(r, _)| *r); // stable: enumeration order within each tier ranked.sort_by_key(|(r, _)| *r); // stable: enumeration order within each tier
let mut names: Vec<String> = Vec::new(); let mut names: Vec<String> = Vec::new();
@@ -553,6 +609,8 @@ fn pick_device(
surface_i: &ash::khr::surface::Instance, surface_i: &ash::khr::surface::Instance,
surface: vk::SurfaceKHR, surface: vk::SurfaceKHR,
) -> Result<(vk::PhysicalDevice, u32)> { ) -> Result<(vk::PhysicalDevice, u32)> {
// SAFETY: per the Vulkan contract above - a read-only query on the live instance/device,
// filling locals returned by value.
let devices = unsafe { instance.enumerate_physical_devices() }?; let devices = unsafe { instance.enumerate_physical_devices() }?;
let forced: Option<usize> = std::env::var("PUNKTFUNK_VK_DEVICE") let forced: Option<usize> = std::env::var("PUNKTFUNK_VK_DEVICE")
.ok() .ok()
@@ -575,6 +633,8 @@ fn pick_device(
.map(|w| w.trim().to_lowercase()) .map(|w| w.trim().to_lowercase())
.filter(|w| !w.is_empty()); .filter(|w| !w.is_empty());
candidates.sort_by_key(|d| { candidates.sort_by_key(|d| {
// SAFETY: per the Vulkan contract above - a read-only query on the live
// instance/device, filling locals returned by value.
let props = unsafe { instance.get_physical_device_properties(*d) }; let props = unsafe { instance.get_physical_device_properties(*d) };
let name = props let name = props
.device_name_as_c_str() .device_name_as_c_str()
@@ -595,10 +655,14 @@ fn pick_device(
}); });
} }
for pdev in candidates { for pdev in candidates {
// SAFETY: per the Vulkan contract above - a read-only query on the live instance/device,
// filling locals returned by value.
let families = unsafe { instance.get_physical_device_queue_family_properties(pdev) }; let families = unsafe { instance.get_physical_device_queue_family_properties(pdev) };
for (i, f) in families.iter().enumerate() { for (i, f) in families.iter().enumerate() {
let graphics = f.queue_flags.contains(vk::QueueFlags::GRAPHICS); let graphics = f.queue_flags.contains(vk::QueueFlags::GRAPHICS);
let present = let present =
// SAFETY: per the Vulkan contract above - a read-only query on the live
// instance/device, filling locals returned by value.
unsafe { surface_i.get_physical_device_surface_support(pdev, i as u32, surface) } unsafe { surface_i.get_physical_device_surface_support(pdev, i as u32, surface) }
.unwrap_or(false); .unwrap_or(false);
if graphics && present { if graphics && present {
@@ -629,6 +693,8 @@ pub(super) fn pick_formats(
let colorspace_ext = colorspace_ext let colorspace_ext = colorspace_ext
&& !std::env::var("PUNKTFUNK_HDR10") && !std::env::var("PUNKTFUNK_HDR10")
.is_ok_and(|v| matches!(v.as_str(), "0" | "false" | "off" | "no")); .is_ok_and(|v| matches!(v.as_str(), "0" | "false" | "off" | "no"));
// SAFETY: per the Vulkan contract above - a read-only query on the live instance/device,
// filling locals returned by value.
let formats = unsafe { surface_i.get_physical_device_surface_formats(pdev, surface) }?; let formats = unsafe { surface_i.get_physical_device_surface_formats(pdev, surface) }?;
let mut sdr = None; let mut sdr = None;
for want in [vk::Format::B8G8R8A8_UNORM, vk::Format::R8G8B8A8_UNORM] { for want in [vk::Format::B8G8R8A8_UNORM, vk::Format::R8G8B8A8_UNORM] {
@@ -673,6 +739,8 @@ fn pick_present_mode(
pdev: vk::PhysicalDevice, pdev: vk::PhysicalDevice,
surface: vk::SurfaceKHR, surface: vk::SurfaceKHR,
) -> Result<vk::PresentModeKHR> { ) -> Result<vk::PresentModeKHR> {
// SAFETY: per the Vulkan contract above - a read-only query on the live instance/device,
// filling locals returned by value.
let modes = unsafe { surface_i.get_physical_device_surface_present_modes(pdev, surface) }?; 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() { let want = match std::env::var("PUNKTFUNK_PRESENT_MODE").ok().as_deref() {
Some("fifo") => vk::PresentModeKHR::FIFO, Some("fifo") => vk::PresentModeKHR::FIFO,
+6
View File
@@ -30,6 +30,9 @@ const APP_USER_MODEL_ID: *const u16 = w!("unom.punktfunk.client");
/// process already carries the MSIX identity, which must win). Call before any window /// process already carries the MSIX identity, which must win). Call before any window
/// exists; later calls don't re-tag existing windows. /// exists; later calls don't re-tag existing windows.
pub(crate) fn set_app_user_model_id() { pub(crate) fn set_app_user_model_id() {
// SAFETY: `GetCurrentPackageFullName` is called with `len = 0` and a NULL buffer, which is the
// documented identity PROBE — it writes nothing and only reports whether the process is
// packaged. `SetCurrentProcessExplicitAppUserModelID` takes a static wide literal.
unsafe { unsafe {
let mut len: u32 = 0; let mut len: u32 = 0;
// No buffer: just probe whether the process has package identity. // No buffer: just probe whether the process has package identity.
@@ -43,6 +46,9 @@ pub(crate) fn set_app_user_model_id() {
/// Stamp the exe-embedded icon (resource ordinal 1) onto the SDL window's title bar, /// Stamp the exe-embedded icon (resource ordinal 1) onto the SDL window's title bar,
/// taskbar and Alt-Tab. A quiet no-op when the exe embeds no icon. /// taskbar and Alt-Tab. A quiet no-op when the exe embeds no icon.
pub(crate) fn stamp_window_icon(window: &sdl3::video::Window) { pub(crate) fn stamp_window_icon(window: &sdl3::video::Window) {
// SAFETY: the SDL property lookups return the `HWND` SDL itself owns for this live `window`
// borrow; the icon calls only pass that handle and a resource ordinal back to Win32, and every
// result is checked before use.
unsafe { unsafe {
// The HWND behind the SDL window, via SDL3's window properties (the same route // The HWND behind the SDL window, via SDL3's window properties (the same route
// the sdl3 crate's raw-window-handle integration takes). // the sdl3 crate's raw-window-handle integration takes).
@@ -38,6 +38,10 @@ fn qos_handle() -> Option<HANDLE> {
// SAFETY: both pointers are valid for the duration of the synchronous call. // SAFETY: both pointers are valid for the duration of the synchronous call.
if unsafe { QOSCreateHandle(&version, &mut handle) } == 0 { if unsafe { QOSCreateHandle(&version, &mut handle) } == 0 {
tracing::debug!( tracing::debug!(
// SAFETY: `GetLastError` takes no arguments and reads this thread's own last-error
// slot; it is called immediately after the failing call, before anything can reset it.
// SAFETY: `GetLastError` takes no arguments and reads this thread's own last-error
// slot; it is called immediately after the failing call, before anything can reset it.
err = unsafe { GetLastError() }, err = unsafe { GetLastError() },
"QOSCreateHandle failed — qWAVE DSCP marking unavailable" "QOSCreateHandle failed — qWAVE DSCP marking unavailable"
); );
@@ -95,6 +99,8 @@ pub(super) fn add_media_flow(socket: &UdpSocket, class: MediaClass) -> Option<Qo
}; };
if ok == 0 { if ok == 0 {
tracing::debug!( tracing::debug!(
// SAFETY: `GetLastError` takes no arguments and reads this thread's own last-error
// slot; it is called immediately after the failing call, before anything can reset it.
err = unsafe { GetLastError() }, err = unsafe { GetLastError() },
?class, ?class,
"QOSAddSocketToFlow failed — DSCP marking skipped" "QOSAddSocketToFlow failed — DSCP marking skipped"
@@ -124,6 +130,8 @@ pub(super) fn add_media_flow(socket: &UdpSocket, class: MediaClass) -> Option<Qo
}; };
if ok == 0 { if ok == 0 {
tracing::debug!( tracing::debug!(
// SAFETY: `GetLastError` takes no arguments and reads this thread's own last-error
// slot; it is called immediately after the failing call, before anything can reset it.
err = unsafe { GetLastError() }, err = unsafe { GetLastError() },
?class, ?class,
"QOSSetFlow(OutgoingDSCPValue) refused — traffic-type default marking stands" "QOSSetFlow(OutgoingDSCPValue) refused — traffic-type default marking stands"
@@ -93,6 +93,10 @@ fn send_one_uso(socket: &std::net::UdpSocket, buf: &[u8], seg_size: u16) -> std:
}; };
let cmsg_len = cmsgdata_align(hdr) + std::mem::size_of::<u32>(); // WSA_CMSG_LEN(4) let cmsg_len = cmsgdata_align(hdr) + std::mem::size_of::<u32>(); // WSA_CMSG_LEN(4)
let space = cmsgdata_align(hdr + cmsghdr_align(std::mem::size_of::<u32>())); // WSA_CMSG_SPACE(4) let space = cmsgdata_align(hdr + cmsghdr_align(std::mem::size_of::<u32>())); // WSA_CMSG_SPACE(4)
// SAFETY: `ctrl` is a local control buffer sized by `WSA_CMSG_SPACE(4)` — computed as `space`
// just above — so the header plus its 4-byte payload fit inside it and neither the field stores
// nor the unaligned data write can run past the end. `write_unaligned` is used because the
// payload sits at a `WSA_CMSG_DATA` offset with no alignment guarantee.
unsafe { unsafe {
let cmsg = ctrl.0.as_mut_ptr() as *mut CMSGHDR; let cmsg = ctrl.0.as_mut_ptr() as *mut CMSGHDR;
(*cmsg).cmsg_len = cmsg_len; (*cmsg).cmsg_len = cmsg_len;