//! The per-frame present path (route input → video image → CSC → blit → present). HOT PATH. use super::gpu::*; use super::{FrameInput, Presenter, Retired}; use crate::csc::csc_rows; #[cfg(target_os = "linux")] use crate::dmabuf::{self, HwFrame}; use crate::overlay::OverlayFrame; use anyhow::{bail, Context as _, Result}; use ash::vk; use ash::vk::Handle as _; use pf_client_core::video::VkVideoFrame; impl Presenter { /// Present one frame: route `input` into the video image (staging upload or dmabuf /// import + CSC pass; `Redraw` re-blits what's retained), clear, letterbox-blit, /// blend the console-UI `overlay` quad if one arrived, present. Returns false when /// the swapchain was out of date — the caller recreates (with current window state) /// and may retry. pub fn present( &mut self, window: &sdl3::video::Window, input: FrameInput, overlay: Option<&OverlayFrame>, ) -> Result { if self.extent.width == 0 || self.extent.height == 0 { return Ok(true); // minimized — nothing to do } // SDR↔HDR follows the FRAMES' own signaling (the host flips PQ in-band): // switch modes before anything touches this frame. Only where the surface // offers HDR10 — otherwise PQ stays on the SDR swapchain and the CSC shader // tonemaps (mode 1). let frame_pq = match &input { FrameInput::Redraw => None, FrameInput::Cpu(f) => Some(f.color.is_pq()), #[cfg(target_os = "linux")] FrameInput::Dmabuf(d) => Some(d.color.is_pq()), FrameInput::VkFrame(v) => Some(v.color.is_pq()), #[cfg(windows)] FrameInput::D3d11(d) => Some(d.color.is_pq()), #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] FrameInput::PyroWave(f) => Some(f.color.is_pq()), }; if let Some(pq) = frame_pq { // A PQ stream we can only tone-map (no HDR10 surface) is the silent failure behind // "HDR isn't advertised": the compositor never sees an HDR-committing app. Say so // once — its presence proves PQ IS arriving and the surface/compositor is the // blocker (on the Deck: gamescope's WSI layer not visible in the flatpak sandbox); // its absence, with a plain SDR stream, points back at the host not sending PQ. if pq && self.hdr10_format.is_none() && !self.hdr_downgrade_warned { self.hdr_downgrade_warned = true; tracing::warn!( "PQ (HDR10) stream tone-mapped to SDR — the surface offers no HDR10 \ colorspace, so no HDR is committed to the compositor. Under gamescope this \ usually means the gamescope Vulkan WSI layer is not visible in the sandbox." ); } let want = pq && self.hdr10_format.is_some(); if want != self.hdr_active { self.set_hdr_mode(window, want)?; } } // Hardware frames prepare before anything touches the queue: an import/view the // driver rejects must fail out here, before this present consumed the acquire // semaphore. #[cfg(target_os = "linux")] let mut hw_frame: Option = None; #[cfg(windows)] let mut win_frame: Option = None; let mut vk_frame: Option<(VkVideoFrame, [vk::ImageView; 2])> = None; #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] let mut pyro_frame: Option = None; let cpu_frame = match input { FrameInput::Redraw => None, FrameInput::Cpu(f) => Some(f), #[cfg(target_os = "linux")] FrameInput::Dmabuf(d) => { let hw = self .hw .as_ref() .context("hardware frame without dmabuf support")?; hw_frame = Some(dmabuf::import(&self.device, &hw.ext_mem_fd, d)?); None } #[cfg(windows)] FrameInput::D3d11(d) => { let hw = self .hw_win .as_ref() .context("D3D11 frame without win32 import support")?; win_frame = Some(crate::d3d11::import(&self.device, &hw.ext_mem_win32, &d)?); None } FrameInput::VkFrame(v) => { let views = self.vkframe_plane_views(&v)?; vk_frame = Some((v, views)); None } #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] FrameInput::PyroWave(f) => { pyro_frame = Some(f); None } }; // One frame in flight: the fence covers the command buffer, the staging buffer // AND the previously submitted hw frame — waiting makes all three reusable. unsafe { if self.submitted { self.device.wait_for_fences(&[self.fence], true, u64::MAX)?; self.submitted = false; } self.device.reset_fences(&[self.fence])?; } if let Some(old) = self.retired_hw.take() { old.destroy(&self.device); } if let Some(f) = cpu_frame { self.stage_frame(f)?; } #[cfg(target_os = "linux")] if let Some(f) = &hw_frame { 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"); } // Safe while nothing in flight references the set — the fence wait above. self.csc .bind_planes(&self.device, f.luma_view, f.chroma_view); } #[cfg(windows)] if let Some(f) = &win_frame { 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"); } } if let Some((f, views)) = &vk_frame { 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"); } self.csc.bind_planes(&self.device, views[0], views[1]); } #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] if let Some(f) = &pyro_frame { 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 planar = self .csc_planar .as_ref() .context("PyroWave frame but the device failed the pyrowave probe")?; planar.bind_planes_planar(&self.device, f.views.map(vk::ImageView::from_raw)); } if let Some(o) = overlay { // Point the composite at this overlay image (same fence-wait safety). let infos = [vk::DescriptorImageInfo::default() .image_view(o.view) .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)]; let writes = [vk::WriteDescriptorSet::default() .dst_set(self.overlay_pipe.desc_set) .dst_binding(0) .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) .image_info(&infos)]; unsafe { self.device.update_descriptor_sets(&writes, &[]) }; } let (index, _suboptimal) = match unsafe { self.swap_d.acquire_next_image( self.swapchain, u64::MAX, self.acquire_sem, vk::Fence::null(), ) } { Ok(r) => r, Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { // Never submitted — the import (if any) dies here, GPU never saw it. #[cfg(target_os = "linux")] if let Some(f) = hw_frame { f.destroy(&self.device); } #[cfg(windows)] if let Some(f) = win_frame { f.destroy(&self.device); } self.recreate_swapchain(window)?; return Ok(false); } Err(e) => return Err(e).context("vkAcquireNextImageKHR"), }; let swap_image = self.images[index as usize]; unsafe { self.device.begin_command_buffer( self.cmd_buf, &vk::CommandBufferBeginInfo::default() .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), )?; // Dmabuf frame: acquire the foreign planes, then the CSC pass renders // NV12→RGBA into the video image (render pass ends it in TRANSFER_SRC for // the blit below). #[cfg(target_os = "linux")] if let (Some(f), Some(v)) = (&hw_frame, &self.video) { for view_image in [f.luma_image(), f.chroma_image()] { foreign_acquire_barrier(&self.device, self.cmd_buf, view_image, self.qfi); } let extent = vk::Extent2D { width: v.width, height: v.height, }; let ten_bit = f.is_p010(); self.record_csc( v.framebuffer, extent, f.color, if ten_bit { 10 } else { 8 }, ten_bit, ); } // D3D11 frame: acquire the imported RGB texture from the external "queue // family" (the keyed mutex on the submit is the actual cross-API sync) and // blit it into the video image — the frame arrives as ready RGB from the // decoder's VideoProcessor (sRGB BGRA8, or PQ RGB10A2 on the HDR ring — // matching the HDR-mode video image), so there is no CSC pass; the blit // converts component order. Same layout dance as the CPU staging path. #[cfg(windows)] if let (Some(f), Some(v)) = (&win_frame, &self.video) { external_acquire_barrier(&self.device, self.cmd_buf, f.image(), self.qfi); barrier( &self.device, self.cmd_buf, v.image, vk::ImageLayout::UNDEFINED, vk::ImageLayout::TRANSFER_DST_OPTIMAL, ); let extent = vk::Offset3D { x: v.width as i32, y: v.height as i32, z: 1, }; let blit = vk::ImageBlit::default() .src_subresource(subresource_layers()) .src_offsets([vk::Offset3D::default(), extent]) .dst_subresource(subresource_layers()) .dst_offsets([vk::Offset3D::default(), extent]); self.device.cmd_blit_image( self.cmd_buf, f.image(), vk::ImageLayout::TRANSFER_SRC_OPTIMAL, v.image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, &[blit], vk::Filter::NEAREST, // 1:1 — the composite blit below does the scaling ); barrier( &self.device, self.cmd_buf, v.image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, vk::ImageLayout::TRANSFER_SRC_OPTIMAL, ); } // Vulkan-Video frame: the decoded image is already on THIS device. Read the // live sync state under the frames lock (held through submission — the // AVVulkanFramesContext contract), acquire from the decode queue family, // then the same CSC pass. let mut vk_sync: Option = None; if let (Some((f, _)), Some(v)) = (&vk_frame, &self.video) { let sync = lock_vkframe(f); vkframe_acquire_barrier( &self.device, self.cmd_buf, vk::Image::from_raw(sync.image), vk::ImageLayout::from_raw(sync.layout), sync.queue_family, self.qfi, ); let extent = vk::Extent2D { width: v.width, height: v.height, }; let ten_bit = f.vk_format == vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16.as_raw(); self.record_csc( v.framebuffer, extent, f.color, if ten_bit { 10 } else { 8 }, ten_bit, ); vk_sync = Some(sync); } // PyroWave frame: the planes are already on THIS device, decode // fence-complete and barriered to fragment sampling (GENERAL) by the // decoder — no acquire needed, just the planar CSC pass. #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] if let (Some(f), Some(v)) = (&pyro_frame, &self.video) { let extent = vk::Extent2D { width: v.width, height: v.height, }; self.record_csc_planar(v.framebuffer, extent, f.color); } // New frame: staging → video image (stride carried by buffer_row_length). if let (Some(f), Some(v), Some(s)) = (cpu_frame, &self.video, &self.staging) { barrier( &self.device, self.cmd_buf, v.image, vk::ImageLayout::UNDEFINED, vk::ImageLayout::TRANSFER_DST_OPTIMAL, ); let region = vk::BufferImageCopy::default() .buffer_row_length((f.stride / 4) as u32) .image_subresource(subresource_layers()) .image_extent(vk::Extent3D { width: v.width, height: v.height, depth: 1, }); self.device.cmd_copy_buffer_to_image( self.cmd_buf, s.buffer, v.image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, &[region], ); barrier( &self.device, self.cmd_buf, v.image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, vk::ImageLayout::TRANSFER_SRC_OPTIMAL, ); } // Swapchain image: discard old content, clear to black (the letterbox bars), // blit the video in, hand to present. barrier( &self.device, self.cmd_buf, swap_image, vk::ImageLayout::UNDEFINED, vk::ImageLayout::TRANSFER_DST_OPTIMAL, ); self.device.cmd_clear_color_image( self.cmd_buf, swap_image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, &vk::ClearColorValue { float32: [0.0, 0.0, 0.0, 1.0], }, &[subresource_range()], ); if let Some(v) = &self.video { let (dst0, dst1) = letterbox(self.extent, v.width, v.height); let blit = vk::ImageBlit::default() .src_subresource(subresource_layers()) .src_offsets([ vk::Offset3D { x: 0, y: 0, z: 0 }, vk::Offset3D { x: v.width as i32, y: v.height as i32, z: 1, }, ]) .dst_subresource(subresource_layers()) .dst_offsets([dst0, dst1]); self.device.cmd_blit_image( self.cmd_buf, v.image, vk::ImageLayout::TRANSFER_SRC_OPTIMAL, swap_image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, &[blit], vk::Filter::LINEAR, ); } if let Some(o) = overlay { // Cross-submit visibility for the overlay image (Skia flushed it on this // queue): same-layout barrier = execution + memory dependency only. barrier( &self.device, self.cmd_buf, o.image, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, ); barrier( &self.device, self.cmd_buf, swap_image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL, ); // The composite pass blends the quad and ends the image PRESENT-ready. self.device.cmd_begin_render_pass( self.cmd_buf, &vk::RenderPassBeginInfo::default() .render_pass(self.overlay_pipe.render_pass) .framebuffer(self.overlay_pipe.framebuffers[index as usize]) .render_area(vk::Rect2D { offset: vk::Offset2D { x: 0, y: 0 }, extent: self.extent, }), vk::SubpassContents::INLINE, ); self.device.cmd_bind_pipeline( self.cmd_buf, vk::PipelineBindPoint::GRAPHICS, self.overlay_pipe.pipeline, ); self.device.cmd_set_viewport( self.cmd_buf, 0, &[vk::Viewport { x: 0.0, y: 0.0, width: self.extent.width as f32, height: self.extent.height as f32, min_depth: 0.0, max_depth: 1.0, }], ); self.device.cmd_set_scissor( self.cmd_buf, 0, &[vk::Rect2D { offset: vk::Offset2D { x: 0, y: 0 }, extent: self.extent, }], ); self.device.cmd_bind_descriptor_sets( self.cmd_buf, vk::PipelineBindPoint::GRAPHICS, self.overlay_pipe.pipeline_layout, 0, &[self.overlay_pipe.desc_set], &[], ); self.device.cmd_draw(self.cmd_buf, 3, 1, 0, 0); self.device.cmd_end_render_pass(self.cmd_buf); } else { barrier( &self.device, self.cmd_buf, swap_image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, vk::ImageLayout::PRESENT_SRC_KHR, ); } self.device.end_command_buffer(self.cmd_buf)?; let render_sem = self.render_sems[index as usize]; let cmd_bufs = [self.cmd_buf]; let mut wait_sems = vec![self.acquire_sem]; let mut wait_stages = vec![vk::PipelineStageFlags::TRANSFER]; let mut signal_sems = vec![render_sem]; // The Vulkan-Video frame's timeline semaphore: wait for the decoder's value, // signal value+1 when our reads are done (FFmpeg's per-submission contract). let mut wait_values = vec![0u64]; let mut signal_values = vec![0u64]; if let Some(sync) = &vk_sync { let sem = vk::Semaphore::from_raw(sync.semaphore); wait_sems.push(sem); wait_stages.push(vk::PipelineStageFlags::FRAGMENT_SHADER); wait_values.push(sync.sem_value); signal_sems.push(sem); signal_values.push(sync.sem_value + 1); } let mut timeline = vk::TimelineSemaphoreSubmitInfo::default() .wait_semaphore_values(&wait_values) .signal_semaphore_values(&signal_values); let mut submit = vk::SubmitInfo::default() .wait_semaphores(&wait_sems) .wait_dst_stage_mask(&wait_stages) .command_buffers(&cmd_bufs) .signal_semaphores(&signal_sems); if vk_sync.is_some() { submit = submit.push_next(&mut timeline); } // D3D11 frame: bracket the submit in the shared texture's keyed mutex, key 0 // both ways (the decode side copies under acquire(0)/release(0) too) — the // GPU-side acquire is what orders our sampling after the decoder's copy, and // our completion release is what unblocks the ring slot's reuse. #[cfg(windows)] let keyed_mem; #[cfg(windows)] let keyed_keys = [0u64]; #[cfg(windows)] let keyed_timeouts = [2000u32]; #[cfg(windows)] let mut keyed_info; #[cfg(windows)] if let Some(f) = &win_frame { // Bisect knob: PUNKTFUNK_D3D11_NO_MUTEX=1 skips the acquire/release pair // (torn frames possible — debugging only). if std::env::var_os("PUNKTFUNK_D3D11_NO_MUTEX").is_none() { keyed_mem = [f.memory()]; keyed_info = vk::Win32KeyedMutexAcquireReleaseInfoKHR::default() .acquire_syncs(&keyed_mem) .acquire_keys(&keyed_keys) .acquire_timeouts(&keyed_timeouts) .release_syncs(&keyed_mem) .release_keys(&keyed_keys); submit = submit.push_next(&mut keyed_info); } } let submitted = { // Queue external sync vs the pump's FFmpeg submits (see `queue_lock`). let _q = self.queue_lock.guard(); self.device.queue_submit(self.queue, &[submit], self.fence) }; // Write the new sync state back and release the frames lock REGARDLESS of // the submit outcome (an abandoned lock would wedge the decoder). if let Some(sync) = vk_sync.take() { let ok = submitted.is_ok(); unlock_vkframe( vk_frame .as_ref() .map(|(f, _)| f) .expect("vk_sync implies vk_frame"), &sync, ok, self.qfi, ); } submitted?; self.submitted = true; // The hw frame is on the GPU now — park it until the fence proves the reads // done (destroyed at the next present's fence wait, or in Drop). At most one // of hw_frame/vk_frame is set (they route from the same `input`). self.retired_hw = vk_frame .take() .map(|(frame, views)| Retired::Vk { frame, views }); #[cfg(target_os = "linux")] if let Some(f) = hw_frame.take() { self.retired_hw = Some(Retired::Dmabuf(f)); } #[cfg(windows)] if let Some(f) = win_frame.take() { self.retired_hw = Some(Retired::D3d11(f)); } let swapchains = [self.swapchain]; let indices = [index]; let present_sems = [render_sem]; // On-glass timing (T0.2): attach a monotonically increasing present id the // PresentTimer's `vkWaitForPresentKHR` resolves to real visibility. let ids = [self.next_present_id + 1]; let mut pid_info = vk::PresentIdKHR::default().present_ids(&ids); let mut present_info = vk::PresentInfoKHR::default() .wait_semaphores(&present_sems) .swapchains(&swapchains) .image_indices(&indices); if self.present_timer.is_some() { self.next_present_id += 1; present_info = present_info.push_next(&mut pid_info); } // Same queue external-sync rule as the submit above. Scoped tightly: the // OUT_OF_DATE arm re-enters the lock via recreate_swapchain's queue drain. let present_res = { let _q = self.queue_lock.guard(); self.swap_d.queue_present(self.queue, &present_info) }; match present_res { Ok(_) => { // A failed present's id may never signal — claimable only on Ok. if self.present_timer.is_some() { self.last_presented = Some((self.swapchain, self.next_present_id)); } Ok(true) } Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { self.recreate_swapchain(window)?; Ok(false) } Err(e) => Err(e).context("vkQueuePresentKHR"), } } } /// Record the NV12→RGBA CSC pass into the video image (framebuffer): fullscreen /// triangle, CICP-driven push-constant rows. Shared by the dmabuf and Vulkan-Video /// paths — only the plane views bound beforehand differ. /// /// # Safety /// `self.cmd_buf` must be in the recording state; the CSC descriptor set must point /// at live plane views. unsafe fn record_csc( &self, framebuffer: vk::Framebuffer, extent: vk::Extent2D, color: pf_client_core::video::ColorDesc, depth: u8, msb_packed: bool, ) { unsafe { self.device.cmd_begin_render_pass( self.cmd_buf, &vk::RenderPassBeginInfo::default() .render_pass(self.csc.render_pass) .framebuffer(framebuffer) .render_area(vk::Rect2D { offset: vk::Offset2D { x: 0, y: 0 }, extent, }), vk::SubpassContents::INLINE, ); self.device.cmd_bind_pipeline( self.cmd_buf, vk::PipelineBindPoint::GRAPHICS, self.csc.pipeline, ); self.device.cmd_set_viewport( self.cmd_buf, 0, &[vk::Viewport { x: 0.0, y: 0.0, width: extent.width as f32, height: extent.height as f32, min_depth: 0.0, max_depth: 1.0, }], ); self.device.cmd_set_scissor( self.cmd_buf, 0, &[vk::Rect2D { offset: vk::Offset2D { x: 0, y: 0 }, extent, }], ); self.device.cmd_bind_descriptor_sets( self.cmd_buf, vk::PipelineBindPoint::GRAPHICS, self.csc.pipeline_layout, 0, &[self.csc.desc_set], &[], ); let rows = csc_rows(color, depth, msb_packed); // Mode 1 = PQ→SDR tonemap (a PQ stream without an HDR10 surface); mode 0 // passes the transfer through (SDR as-is, or PQ onto the HDR10 swapchain). let mode = if color.is_pq() && !self.hdr_active { 1.0f32 } else { 0.0 }; let peak = std::env::var("PUNKTFUNK_TONEMAP_PEAK") .ok() .and_then(|v| v.parse::().ok()) .unwrap_or(4.9); // ≈1000 nits over the 203-nit reference let mut pc = [0f32; 16]; pc[..12].copy_from_slice(bytemuck_rows(&rows)); pc[12] = mode; pc[13] = peak; let bytes = std::slice::from_raw_parts(pc.as_ptr().cast::(), 64); self.device.cmd_push_constants( self.cmd_buf, self.csc.pipeline_layout, vk::ShaderStageFlags::FRAGMENT, 0, bytes, ); self.device.cmd_draw(self.cmd_buf, 3, 1, 0, 0); self.device.cmd_end_render_pass(self.cmd_buf); } } /// [`record_csc`] over the planar (PyroWave) pass — always 8-bit, no MSB packing. #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] unsafe fn record_csc_planar( &self, framebuffer: vk::Framebuffer, extent: vk::Extent2D, color: pf_client_core::video::ColorDesc, ) { // The planar pass exists whenever a PyroWave frame reached us (checked at bind). let Some(planar) = self.csc_planar.as_ref() else { return; }; unsafe { self.device.cmd_begin_render_pass( self.cmd_buf, &vk::RenderPassBeginInfo::default() .render_pass(planar.render_pass) .framebuffer(framebuffer) .render_area(vk::Rect2D { offset: vk::Offset2D { x: 0, y: 0 }, extent, }), vk::SubpassContents::INLINE, ); self.device.cmd_bind_pipeline( self.cmd_buf, vk::PipelineBindPoint::GRAPHICS, planar.pipeline, ); self.device.cmd_set_viewport( self.cmd_buf, 0, &[vk::Viewport { x: 0.0, y: 0.0, width: extent.width as f32, height: extent.height as f32, min_depth: 0.0, max_depth: 1.0, }], ); self.device.cmd_set_scissor( self.cmd_buf, 0, &[vk::Rect2D { offset: vk::Offset2D { x: 0, y: 0 }, extent, }], ); self.device.cmd_bind_descriptor_sets( self.cmd_buf, vk::PipelineBindPoint::GRAPHICS, planar.pipeline_layout, 0, &[planar.desc_set], &[], ); // An HDR (PQ) pyrowave session carries P010-style 10-bit studio codes MSB-packed // into 16-bit planes (design/pyrowave-444-hdr.md §2.2) — same sampling scale as // the P010 path; SDR sessions are plain 8-bit BT.709 limited. Depth follows the // colour contract (negotiation couples 10-bit ⟺ PQ for this codec). let (depth, msb_packed) = if color.is_pq() { (10, true) } else { (8, false) }; let rows = csc_rows(color, depth, msb_packed); // Mode 1 = PQ→SDR tonemap (PQ stream without an HDR10 surface); mode 0 passes // the transfer through — identical to the NV12 arm above. let mode = if color.is_pq() && !self.hdr_active { 1.0f32 } else { 0.0 }; let peak = std::env::var("PUNKTFUNK_TONEMAP_PEAK") .ok() .and_then(|v| v.parse::().ok()) .unwrap_or(4.9); // ≈1000 nits over the 203-nit reference let mut pc = [0f32; 16]; pc[..12].copy_from_slice(bytemuck_rows(&rows)); pc[12] = mode; pc[13] = peak; let bytes = std::slice::from_raw_parts(pc.as_ptr().cast::(), 64); self.device.cmd_push_constants( self.cmd_buf, planar.pipeline_layout, vk::ShaderStageFlags::FRAGMENT, 0, bytes, ); self.device.cmd_draw(self.cmd_buf, 3, 1, 0, 0); self.device.cmd_end_render_pass(self.cmd_buf); } } /// Per-plane views over a Vulkan-Video frame's multiplanar image — the CSC pass's /// exact sampling contract (the frames pool was created MUTABLE_FORMAT for this). /// 8-bit NV12 (R8 + R8G8) and 10-bit P010/X6 (R10X6 + R10X6G10X6). fn vkframe_plane_views(&self, f: &VkVideoFrame) -> Result<[vk::ImageView; 2]> { let (luma_fmt, chroma_fmt) = if f.vk_format == vk::Format::G8_B8R8_2PLANE_420_UNORM.as_raw() { (vk::Format::R8_UNORM, vk::Format::R8G8_UNORM) } else if f.vk_format == vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16.as_raw() { ( vk::Format::R10X6_UNORM_PACK16, vk::Format::R10X6G10X6_UNORM_2PACK16, ) } else { bail!( "Vulkan-Video pool format {} unsupported (expected 2-plane 4:2:0, 8/10-bit)", f.vk_format ); }; // img[0] is creation-constant (only the sync fields need the frames lock). let image = vk::Image::from_raw( unsafe { (*(f.vkframe as *const pf_ffvk::AVVkFrame)).img[0] } as u64, ); let make = |aspect: vk::ImageAspectFlags, format: vk::Format| { unsafe { self.device.create_image_view( &vk::ImageViewCreateInfo::default() .image(image) .view_type(vk::ImageViewType::TYPE_2D) .format(format) .subresource_range( vk::ImageSubresourceRange::default() .aspect_mask(aspect) .level_count(1) .layer_count(1), ), None, ) } .context("vk-frame plane view") }; let luma = make(vk::ImageAspectFlags::PLANE_0, luma_fmt)?; let chroma = match make(vk::ImageAspectFlags::PLANE_1, chroma_fmt) { Ok(v) => v, Err(e) => { unsafe { self.device.destroy_image_view(luma, None) }; return Err(e); } }; Ok([luma, chroma]) } } /// Flatten the 3×vec4 rows for the push-constant block. fn bytemuck_rows(rows: &[[f32; 4]; 3]) -> &[f32] { // SAFETY: [[f32;4];3] is 12 contiguous f32s. unsafe { std::slice::from_raw_parts(rows.as_ptr().cast::(), 12) } } /// The live sync state of an `AVVkFrame`, snapshotted under the frames lock. struct VkFrameSync { image: u64, semaphore: u64, sem_value: u64, layout: i32, queue_family: u32, } /// Lock the frame and read its live sync state (the presenter's submit must wait /// `sem_value` and signal `sem_value + 1`). The lock is held until [`unlock_vkframe`]. // bindgen's enum repr is target-dependent (u32 Linux/clang, i32 MSVC) — the layout cast // is required on one platform and a no-op on the other. #[allow(clippy::unnecessary_cast)] fn lock_vkframe(f: &VkVideoFrame) -> VkFrameSync { unsafe { let lock: unsafe extern "C" fn(*mut pf_ffvk::AVHWFramesContext, *mut pf_ffvk::AVVkFrame) = std::mem::transmute(f.lock_frame); let fc = f.frames_ctx as *mut pf_ffvk::AVHWFramesContext; let vkf = f.vkframe as *mut pf_ffvk::AVVkFrame; lock(fc, vkf); VkFrameSync { image: (*vkf).img[0] as u64, semaphore: (*vkf).sem[0] as u64, sem_value: (*vkf).sem_value[0], layout: (*vkf).layout[0] as i32, queue_family: (*vkf).queue_family[0], } } } /// 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. fn unlock_vkframe(f: &VkVideoFrame, sync: &VkFrameSync, submitted: bool, graphics_qf: u32) { unsafe { let vkf = f.vkframe as *mut pf_ffvk::AVVkFrame; if submitted { (*vkf).sem_value[0] = sync.sem_value + 1; (*vkf).layout[0] = vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL.as_raw() as pf_ffvk::VkImageLayout; if sync.queue_family != vk::QUEUE_FAMILY_IGNORED { (*vkf).queue_family[0] = graphics_qf; } } let unlock: unsafe extern "C" fn(*mut pf_ffvk::AVHWFramesContext, *mut pf_ffvk::AVVkFrame) = std::mem::transmute(f.unlock_frame); unlock(f.frames_ctx as *mut pf_ffvk::AVHWFramesContext, vkf); } }