feat(client): PyroWave planar present path + Linux NVENC match-arm fix (Phase 2b, part 2)
android / android (push) Failing after 45s
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 56s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
arch / build-publish (push) Failing after 4m15s
ci / rust (push) Failing after 3m59s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 59s
deb / build-publish (push) Failing after 4m7s
ci / bench (push) Successful in 5m28s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 3m46s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 3m58s
flatpak / build-publish (push) Failing after 8m3s
docker / deploy-docs (push) Successful in 22s
windows-host / package (push) Successful in 14m21s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m31s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m35s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m57s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m46s
apple / swift (push) Successful in 4m59s
apple / screenshots (push) Successful in 21m40s

The arch package job (--features nvenc) tripped the same class of
Codec::PyroWave non-exhaustive matches as windows-host had, in
nvenc_cuda.rs (6 sites) — dispatch-guarded unreachable!() arms, plus
the vk_util-extraction leftover unused imports in vulkan_video.rs.
All Linux host feature combos (none / pyrowave / nvenc,vulkan-encode /
all three) now compile clean on .21.

Presenter: planar_csc.frag (+ committed .spv) — the 3-plane variant of
nv12_csc.frag (separate Cb/Cr R8 planes, same push-constant CSC-row
contract, siting correction self-disables at full-res chroma).
CscPass grows a shared builder + new_planar()/bind_planes_planar()
(GENERAL-layout descriptors — pyrowave planes stay GENERAL); the Vk
presenter builds the planar pass when the device passed the pyrowave
probe, FrameInput::PyroWave rides present_frame (no acquire barrier
needed: the decoder fence-completed and barriered the planes on the
same queue), and run.rs presents it with no demote rung (only device
loss ends the session).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 01:42:15 +02:00
parent 575975687c
commit f77eec1299
8 changed files with 318 additions and 21 deletions
+5
View File
@@ -43,3 +43,8 @@ windows-sys = { version = "0.61", features = [
"Win32_UI_Shell", "Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging", "Win32_UI_WindowsAndMessaging",
] } ] }
[features]
# PyroWave planar present path (the wired-LAN wavelet codec) — forwards to the decode
# backend in pf-client-core; OFF by default.
pyrowave = ["pf-client-core/pyrowave"]
@@ -0,0 +1,84 @@
// Planar 3-plane YCbCr → RGBA — the PyroWave variant of nv12_csc.frag (separate Cb and
// Cr R8 planes instead of an interleaved CbCr plane; design/pyrowave-codec-plan.md §4.5).
// Same push-constant contract (csc_rows precomputes the matrix + range expansion), same
// output modes — though PyroWave itself is 8-bit SDR BT.709 limited, keeping parity means
// one less divergence if the codec ever signals more. 4:4:4 needs no shader change: the
// chroma planes arrive full-res and the siting correction self-disables.
//
// Regenerate: shaders/build.sh (committed .spv, no build-time toolchain).
#version 450
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 frag;
layout(set = 0, binding = 0) uniform sampler2D u_y;
layout(set = 0, binding = 1) uniform sampler2D u_cb;
layout(set = 0, binding = 2) uniform sampler2D u_cr;
layout(push_constant) uniform Csc {
vec4 r0;
vec4 r1;
vec4 r2;
vec4 params; // x: mode, y: tonemap peak, z/w: reserved
} pc;
// SMPTE ST.2084 (PQ) EOTF: code value → display-referred linear, normalized to 1.0 =
// 10000 nits.
vec3 pq_eotf(vec3 e) {
const float m1 = 0.1593017578125; // 2610/16384
const float m2 = 78.84375; // 2523/4096 * 128
const float c1 = 0.8359375; // 3424/4096
const float c2 = 18.8515625; // 2413/4096 * 32
const float c3 = 18.6875; // 2392/4096 * 32
vec3 p = pow(max(e, vec3(0.0)), vec3(1.0 / m2));
return pow(max(p - c1, vec3(0.0)) / (c2 - c3 * p), vec3(1.0 / m1));
}
// BT.2020 → BT.709 primaries (linear light).
vec3 bt2020_to_709(vec3 c) {
return mat3(
1.6605, -0.1246, -0.0182,
-0.5876, 1.1329, -0.1006,
-0.0728, -0.0083, 1.1187
) * c;
}
// Linear → sRGB OETF.
vec3 srgb_oetf(vec3 c) {
c = clamp(c, 0.0, 1.0);
bvec3 lo = lessThanEqual(c, vec3(0.0031308));
vec3 hi = 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055;
return mix(hi, c * 12.92, vec3(lo));
}
void main() {
// Left-cosited 4:2:0 chroma sampled at luma UV assumes CENTER siting — offset +0.25
// chroma texels to re-align (same correction as nv12_csc.frag; self-disables when the
// chroma plane is full-res).
vec2 cuv = v_uv;
int cw = textureSize(u_cb, 0).x;
if (cw < textureSize(u_y, 0).x) {
cuv.x += 0.25 / float(cw);
}
vec3 yuv = vec3(texture(u_y, v_uv).r, texture(u_cb, cuv).r, texture(u_cr, cuv).r);
vec3 rgb = vec3(
dot(pc.r0.xyz, yuv) + pc.r0.w,
dot(pc.r1.xyz, yuv) + pc.r1.w,
dot(pc.r2.xyz, yuv) + pc.r2.w
);
if (pc.params.x > 0.5) {
vec3 lin = pq_eotf(clamp(rgb, 0.0, 1.0)) * (10000.0 / 203.0);
lin = max(bt2020_to_709(lin), vec3(0.0));
float peak = max(pc.params.y, 1.0001);
float l = max(lin.r, max(lin.g, lin.b));
if (l > 1.0) {
float mapped = 1.0 + (l - 1.0) / (1.0 + (l - 1.0) / (peak - 1.0));
lin *= mapped / l;
}
rgb = srgb_oetf(lin);
} else {
rgb = clamp(rgb, 0.0, 1.0);
}
frag = vec4(rgb, 1.0);
}
Binary file not shown.
+54 -12
View File
@@ -32,6 +32,32 @@ impl CscPass {
/// `attachment_format` = the video image's format: R8G8B8A8 for SDR, a 10-bit /// `attachment_format` = the video image's format: R8G8B8A8 for SDR, a 10-bit
/// format when the pass writes PQ (8 bits would band the PQ curve visibly). /// format when the pass writes PQ (8 bits would band the PQ curve visibly).
pub fn new(device: &ash::Device, attachment_format: vk::Format) -> Result<CscPass> { pub fn new(device: &ash::Device, attachment_format: vk::Format) -> Result<CscPass> {
Self::build(
device,
attachment_format,
2,
include_bytes!("../shaders/nv12_csc.frag.spv"),
)
}
/// The planar 3-plane variant (separate Cb/Cr R8 planes — the PyroWave decode
/// output, design/pyrowave-codec-plan.md §4.5). Same push-constant contract.
#[cfg(feature = "pyrowave")]
pub fn new_planar(device: &ash::Device, attachment_format: vk::Format) -> Result<CscPass> {
Self::build(
device,
attachment_format,
3,
include_bytes!("../shaders/planar_csc.frag.spv"),
)
}
fn build(
device: &ash::Device,
attachment_format: vk::Format,
plane_bindings: u32,
frag_spv: &[u8],
) -> Result<CscPass> {
// One color attachment: the presenter's video image. Content is fully // One color attachment: the presenter's video image. Content is fully
// overwritten (DONT_CARE load), and the pass ends in TRANSFER_SRC so the // overwritten (DONT_CARE load), and the pass ends in TRANSFER_SRC so the
// existing letterbox blit consumes it with no extra barrier. // existing letterbox blit consumes it with no extra barrier.
@@ -89,20 +115,16 @@ impl CscPass {
}?; }?;
let samplers = [sampler]; let samplers = [sampler];
let bindings = [ let bindings: Vec<vk::DescriptorSetLayoutBinding> = (0..plane_bindings)
.map(|b| {
vk::DescriptorSetLayoutBinding::default() vk::DescriptorSetLayoutBinding::default()
.binding(0) .binding(b)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1) .descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT) .stage_flags(vk::ShaderStageFlags::FRAGMENT)
.immutable_samplers(&samplers), .immutable_samplers(&samplers)
vk::DescriptorSetLayoutBinding::default() })
.binding(1) .collect();
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.immutable_samplers(&samplers),
];
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),
@@ -124,7 +146,7 @@ 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(2)]; .descriptor_count(plane_bindings)];
let desc_pool = unsafe { let desc_pool = unsafe {
device.create_descriptor_pool( device.create_descriptor_pool(
&vk::DescriptorPoolCreateInfo::default() &vk::DescriptorPoolCreateInfo::default()
@@ -145,7 +167,7 @@ impl CscPass {
device, device,
render_pass, render_pass,
pipeline_layout, pipeline_layout,
include_bytes!("../shaders/nv12_csc.frag.spv"), frag_spv,
false, // opaque — the CSC output IS the video false, // opaque — the CSC output IS the video
)?; )?;
@@ -184,6 +206,26 @@ impl CscPass {
unsafe { device.update_descriptor_sets(&writes, &[]) }; unsafe { device.update_descriptor_sets(&writes, &[]) };
} }
/// Planar variant of [`bind_planes`](Self::bind_planes): three single-component
/// plane views in GENERAL layout (the pyrowave decode leaves them there; same
/// fence-wait safety contract).
#[cfg(feature = "pyrowave")]
pub fn bind_planes_planar(&self, device: &ash::Device, planes: [vk::ImageView; 3]) {
let infos = planes.map(|view| {
[vk::DescriptorImageInfo::default()
.image_view(view)
.image_layout(vk::ImageLayout::GENERAL)]
});
let writes = [0u32, 1, 2].map(|b| {
vk::WriteDescriptorSet::default()
.dst_set(self.desc_set)
.dst_binding(b)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(&infos[b as usize])
});
unsafe { device.update_descriptor_sets(&writes, &[]) };
}
pub fn destroy(&self, device: &ash::Device) { pub fn destroy(&self, device: &ash::Device) {
unsafe { unsafe {
device.destroy_pipeline(self.pipeline, None); device.destroy_pipeline(self.pipeline, None);
+22
View File
@@ -968,6 +968,28 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
image, image,
} = f; } = f;
let did_present = match image { let did_present = match image {
// PyroWave planar frames: already on the presenter's device and
// fence-complete — a present failure has no demote rung (nothing
// else decodes the codec); only device loss ends the session.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
DecodedImage::PyroWave(f) => {
st.hdr = false; // 8-bit SDR codec
match presenter.present(
&window,
FrameInput::PyroWave(f),
overlay_frame.as_ref(),
) {
Ok(p) => p,
Err(e) => {
if device_lost(&e) {
return Err(e)
.context("GPU device lost — the session cannot continue");
}
tracing::warn!(error = %format!("{e:#}"), "pyrowave present failed");
false
}
}
}
DecodedImage::Cpu(c) => { DecodedImage::Cpu(c) => {
st.hdr = c.color.is_pq(); st.hdr = c.color.is_pq();
presenter.present(&window, FrameInput::Cpu(&c), overlay_frame.as_ref())? presenter.present(&window, FrameInput::Cpu(&c), overlay_frame.as_ref())?
+137
View File
@@ -40,6 +40,10 @@ pub enum FrameInput<'a> {
/// D3D11VA hand-off — a shareable NT-handle texture to import (`d3d11.rs`). /// D3D11VA hand-off — a shareable NT-handle texture to import (`d3d11.rs`).
#[cfg(windows)] #[cfg(windows)]
D3d11(pf_client_core::video::D3d11Frame), D3d11(pf_client_core::video::D3d11Frame),
/// PyroWave planar output — three R8 plane views already on THIS device, decode
/// fence-complete, GENERAL layout (`pf_client_core::video_pyrowave`).
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
PyroWave(pf_client_core::video_pyrowave::PyroWavePlanarFrame),
} }
/// The dmabuf/CSC machinery, present only when the device carries the import extensions. /// The dmabuf/CSC machinery, present only when the device carries the import extensions.
@@ -321,6 +325,10 @@ pub struct Presenter {
#[cfg(windows)] #[cfg(windows)]
hw_win: Option<HwCtxWin>, hw_win: Option<HwCtxWin>,
csc: CscPass, csc: CscPass,
/// The planar (3-plane) CSC variant for PyroWave frames; built only when the device
/// passed the pyrowave probe.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
csc_planar: Option<CscPass>,
/// FFmpeg Vulkan Video decode handles — `None` when the stack can't do it. /// FFmpeg Vulkan Video decode handles — `None` when the stack can't do it.
video_export: Option<pf_client_core::video::VulkanDecodeDevice>, video_export: Option<pf_client_core::video::VulkanDecodeDevice>,
/// The console-UI composite quad (§6.1's presenter half). /// The console-UI composite quad (§6.1's presenter half).
@@ -641,6 +649,13 @@ impl Presenter {
ext_mem_win32: ash::khr::external_memory_win32::Device::new(&instance, &device), ext_mem_win32: ash::khr::external_memory_win32::Device::new(&instance, &device),
}); });
let csc = CscPass::new(&device, vk::Format::R8G8B8A8_UNORM)?; let csc = CscPass::new(&device, vk::Format::R8G8B8A8_UNORM)?;
// PyroWave is 8-bit SDR only, so the planar pass never needs the HDR10 rebuild.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
let csc_planar = if pyrowave_ok {
Some(CscPass::new_planar(&device, vk::Format::R8G8B8A8_UNORM)?)
} else {
None
};
// The exported handle bundle: FFmpeg Vulkan Video handles when the device can // The exported handle bundle: FFmpeg Vulkan Video handles when the device can
// decode, AND (Windows) the D3D11-interop facts — so it's built whenever EITHER // decode, AND (Windows) the D3D11-interop facts — so it's built whenever EITHER
@@ -769,6 +784,8 @@ impl Presenter {
#[cfg(windows)] #[cfg(windows)]
hw_win, hw_win,
csc, csc,
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
csc_planar,
video_export, video_export,
overlay_pipe, overlay_pipe,
retired_hw: None, retired_hw: None,
@@ -1044,6 +1061,10 @@ impl Presenter {
vk::Format::R8G8B8A8_UNORM vk::Format::R8G8B8A8_UNORM
}; };
self.csc.destroy(&self.device); // fence-safe: only our cmd bufs reference it self.csc.destroy(&self.device); // fence-safe: only our cmd bufs reference it
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
if let Some(p) = &self.csc_planar {
p.destroy(&self.device);
}
self.csc = CscPass::new(&self.device, self.video_format)?; self.csc = CscPass::new(&self.device, self.video_format)?;
if let Some(v) = self.video.take() { if let Some(v) = self.video.take() {
unsafe { unsafe {
@@ -1102,6 +1123,8 @@ impl Presenter {
FrameInput::VkFrame(v) => Some(v.color.is_pq()), FrameInput::VkFrame(v) => Some(v.color.is_pq()),
#[cfg(windows)] #[cfg(windows)]
FrameInput::D3d11(d) => Some(d.color.is_pq()), FrameInput::D3d11(d) => Some(d.color.is_pq()),
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
FrameInput::PyroWave(f) => Some(f.color.is_pq()), // always SDR today
}; };
if let Some(pq) = frame_pq { if let Some(pq) = frame_pq {
// A PQ stream we can only tone-map (no HDR10 surface) is the silent failure behind // A PQ stream we can only tone-map (no HDR10 surface) is the silent failure behind
@@ -1130,6 +1153,8 @@ impl Presenter {
#[cfg(windows)] #[cfg(windows)]
let mut win_frame: Option<crate::d3d11::HwFrame> = None; let mut win_frame: Option<crate::d3d11::HwFrame> = None;
let mut vk_frame: Option<(VkVideoFrame, [vk::ImageView; 2])> = None; let mut vk_frame: Option<(VkVideoFrame, [vk::ImageView; 2])> = None;
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
let mut pyro_frame: Option<pf_client_core::video_pyrowave::PyroWavePlanarFrame> = None;
let cpu_frame = match input { let cpu_frame = match input {
FrameInput::Redraw => None, FrameInput::Redraw => None,
FrameInput::Cpu(f) => Some(f), FrameInput::Cpu(f) => Some(f),
@@ -1156,6 +1181,11 @@ impl Presenter {
vk_frame = Some((v, views)); vk_frame = Some((v, views));
None None
} }
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
FrameInput::PyroWave(f) => {
pyro_frame = Some(f);
None
}
}; };
// 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
@@ -1210,6 +1240,22 @@ impl Presenter {
} }
self.csc.bind_planes(&self.device, views[0], views[1]); self.csc.bind_planes(&self.device, views[0], views[1]);
} }
#[cfg(all(target_os = "linux", 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(|v| vk::ImageView::from_raw(v)));
}
if let Some(o) = overlay { if let Some(o) = overlay {
// Point the composite at this overlay image (same fence-wait safety). // Point the composite at this overlay image (same fence-wait safety).
let infos = [vk::DescriptorImageInfo::default() let infos = [vk::DescriptorImageInfo::default()
@@ -1352,6 +1398,18 @@ impl Presenter {
vk_sync = Some(sync); 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(target_os = "linux", 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). // 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) { if let (Some(f), Some(v), Some(s)) = (cpu_frame, &self.video, &self.staging) {
barrier( barrier(
@@ -1708,6 +1766,81 @@ impl Presenter {
} }
} }
/// [`record_csc`] over the planar (PyroWave) pass — always 8-bit, no MSB packing.
#[cfg(all(target_os = "linux", 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],
&[],
);
let rows = csc_rows(color, 8, false);
let mut pc = [0f32; 16];
pc[..12].copy_from_slice(bytemuck_rows(&rows));
pc[12] = 0.0; // SDR passthrough — PyroWave has no PQ path
pc[13] = 0.0;
let bytes = std::slice::from_raw_parts(pc.as_ptr().cast::<u8>(), 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 /// 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). /// exact sampling contract (the frames pool was created MUTABLE_FORMAT for this).
/// 8-bit NV12 (R8 + R8G8) and 10-bit P010/X6 (R10X6 + R10X6G10X6). /// 8-bit NV12 (R8 + R8G8) and 10-bit P010/X6 (R10X6 + R10X6G10X6).
@@ -1956,6 +2089,10 @@ impl Drop for Presenter {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
self.hw.take(); self.hw.take();
self.csc.destroy(&self.device); self.csc.destroy(&self.device);
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
if let Some(p) = &self.csc_planar {
p.destroy(&self.device);
}
self.overlay_pipe.destroy(&self.device); self.overlay_pipe.destroy(&self.device);
for s in self.render_sems.drain(..) { for s in self.render_sems.drain(..) {
self.device.destroy_semaphore(s, None); self.device.destroy_semaphore(s, None);
@@ -224,6 +224,8 @@ fn codec_guid(codec: Codec) -> nv::GUID {
Codec::H264 => nv::NV_ENC_CODEC_H264_GUID, Codec::H264 => nv::NV_ENC_CODEC_H264_GUID,
Codec::H265 => nv::NV_ENC_CODEC_HEVC_GUID, Codec::H265 => nv::NV_ENC_CODEC_HEVC_GUID,
Codec::Av1 => nv::NV_ENC_CODEC_AV1_GUID, Codec::Av1 => nv::NV_ENC_CODEC_AV1_GUID,
// Guarded by the open_video dispatch: a PyroWave session never reaches NVENC.
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
} }
} }
@@ -522,6 +524,7 @@ impl NvencCudaEncoder {
} }
Codec::Av1 => {} Codec::Av1 => {}
Codec::H264 => {} Codec::H264 => {}
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
} }
// Chroma + bit depth. 4:4:4 (HEVC Range Extensions, chromaFormatIDC=3) engages on a YUV444 // Chroma + bit depth. 4:4:4 (HEVC Range Extensions, chromaFormatIDC=3) engages on a YUV444
@@ -549,6 +552,7 @@ impl NvencCudaEncoder {
.set_inputPixelBitDepthMinus8(0); .set_inputPixelBitDepthMinus8(0);
} }
Codec::H264 => {} Codec::H264 => {}
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
} }
} }
@@ -596,6 +600,7 @@ impl NvencCudaEncoder {
av1.matrixCoefficients = mat; av1.matrixCoefficients = mat;
av1.colorRange = 0; av1.colorRange = 0;
} }
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
} }
} }
@@ -616,6 +621,7 @@ impl NvencCudaEncoder {
Codec::Av1 => { Codec::Av1 => {
cfg.encodeCodecConfig.av1Config.maxNumRefFramesInDPB = RFI_DPB; cfg.encodeCodecConfig.av1Config.maxNumRefFramesInDPB = RFI_DPB;
} }
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
} }
} }
Ok(cfg) Ok(cfg)
@@ -1014,6 +1020,9 @@ impl Encoder for NvencCudaEncoder {
pic.codecPicParams.h264PicParams.seiPayloadArrayCnt = sei.len() as u32; pic.codecPicParams.h264PicParams.seiPayloadArrayCnt = sei.len() as u32;
} }
Codec::Av1 => {} Codec::Av1 => {}
Codec::PyroWave => {
unreachable!("PyroWave never opens the direct-NVENC backend")
}
} }
} }
(api().encode_picture)(self.encoder, &mut pic) (api().encode_picture)(self.encoder, &mut pic)
@@ -10,16 +10,14 @@
//! The AV1 encode structs our pinned `ash 0.38` predates are vendored in `vk_av1_encode.rs`. //! The AV1 encode structs our pinned `ash 0.38` predates are vendored in `vk_av1_encode.rs`.
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
use super::vk_util::{ use super::vk_util::{color_range, find_mem, make_plain_image, make_view, pixel_to_vk};
color_range, find_mem, fourcc_to_vk, make_plain_image, make_view, pixel_to_vk, use crate::capture::{CapturedFrame, FramePayload};
};
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
use crate::encode::{Codec, EncodedFrame, Encoder, EncoderCaps}; use crate::encode::{Codec, EncodedFrame, Encoder, EncoderCaps};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use ash::vk; use ash::vk;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::ffi::c_void; use std::ffi::c_void;
use std::os::fd::{AsRawFd, IntoRawFd}; use std::os::fd::AsRawFd;
const NV12: vk::Format = vk::Format::G8_B8R8_2PLANE_420_UNORM; const NV12: vk::Format = vk::Format::G8_B8R8_2PLANE_420_UNORM;
/// Max resident dmabuf imports (comfortably above any PipeWire pool depth; imports alias existing /// Max resident dmabuf imports (comfortably above any PipeWire pool depth; imports alias existing