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
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:
@@ -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.
@@ -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)
|
||||||
vk::DescriptorSetLayoutBinding::default()
|
.map(|b| {
|
||||||
.binding(0)
|
vk::DescriptorSetLayoutBinding::default()
|
||||||
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
.binding(b)
|
||||||
.descriptor_count(1)
|
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||||
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
|
.descriptor_count(1)
|
||||||
.immutable_samplers(&samplers),
|
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
|
||||||
vk::DescriptorSetLayoutBinding::default()
|
.immutable_samplers(&samplers)
|
||||||
.binding(1)
|
})
|
||||||
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
.collect();
|
||||||
.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);
|
||||||
|
|||||||
@@ -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())?
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user