feat(pyrowave): Linux 4:4:4 encode — per-pixel CSC, full-res chroma, gate open
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 55s
ci / bench (push) Successful in 6m44s
arch / build-publish (push) Successful in 12m8s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
android / android (push) Successful in 13m26s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
deb / build-publish (push) Successful in 12m31s
windows-host / package (push) Successful in 16m21s
deb / build-publish-host (push) Successful in 15m0s
apple / swift (push) Successful in 1m23s
apple / screenshots (push) Successful in 6m25s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m3s
ci / rust (push) Successful in 29m30s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m18s
docker / deploy-docs (push) Successful in 26s

Phase 2 of design/pyrowave-444-hdr.md. can_encode_444(PyroWave) now returns
true on Linux, so a client that advertises VIDEO_CAP_444 (the 4:4:4 setting)
and prefers PyroWave negotiates a full-chroma wavelet session end to end
(the client decoder side landed in 5eb930e7).

- rgb2yuv444.comp: the 4:4:4 twin of rgb2yuv.comp — one invocation per pixel,
  full-res interleaved RG8 CbCr, no box filter/siting, byte-identical BT.709
  limited coefficients; compiled .spv committed (glslangValidator -V, matches
  the existing shader's toolchain).
- Encoder: chroma-conditional pyrowave create (open + reset), full-res chroma
  plane + views, per-pixel dispatch, 4:2:0-only even-dims check.
- Tests: decode oracle grows a 4:4:4 mode (YUV444P CPU readback);
  pyrowave_smoke_444 round-trips plane means AND drives the busy test card at
  the ~2.6 bpp operating point asserting in-budget + run-to-run deterministic
  AU sizes — the exact regime that silently corrupted before the vendored
  payload_data fix (patches/0001), so this doubles as its regression test.

Verified on .21 (RTX 5070 Ti): clippy -D warnings, host tests, and both GPU
smokes (pyrowave_smoke + pyrowave_smoke_444) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 12:43:34 +02:00
parent 5eb930e71d
commit 4861824e7d
4 changed files with 189 additions and 39 deletions
+148 -35
View File
@@ -38,6 +38,9 @@ use std::os::raw::c_char;
/// uses. PyroWave carries no VUI, so the colour contract is fixed by this shader: the Phase-2 /// uses. PyroWave carries no VUI, so the colour contract is fixed by this shader: the Phase-2
/// client CSC must assume BT.709 limited range. /// client CSC must assume BT.709 limited range.
const CSC_SPV: &[u8] = include_bytes!("rgb2yuv.spv"); const CSC_SPV: &[u8] = include_bytes!("rgb2yuv.spv");
/// The 4:4:4 twin (`rgb2yuv444.comp`): one invocation per pixel, full-res interleaved CbCr,
/// same BT.709-limited coefficients byte-for-byte.
const CSC444_SPV: &[u8] = include_bytes!("rgb2yuv444.spv");
/// Fixed cursor-overlay texture size (px) — mirrors `vulkan_video.rs`; the shared CSC shader bounds /// Fixed cursor-overlay texture size (px) — mirrors `vulkan_video.rs`; the shared CSC shader bounds
/// sampling by its push constant, so one allocation fits every pointer bitmap. /// sampling by its push constant, so one allocation fits every pointer bitmap.
const CURSOR_MAX: u32 = 256; const CURSOR_MAX: u32 = 256;
@@ -190,6 +193,9 @@ pub struct PyroWaveEncoder {
width: u32, width: u32,
height: u32, height: u32,
fps: u32, fps: u32,
/// Session-fixed negotiated chroma: 4:4:4 = full-res RG8 chroma plane + per-pixel CSC
/// (`rgb2yuv444.comp`) + `Chroma444` pyrowave objects.
chroma444: bool,
/// Per-frame bitstream budget (hard CBR): `bitrate / (8 * fps)`. /// Per-frame bitstream budget (hard CBR): `bitrate / (8 * fps)`.
frame_budget: usize, frame_budget: usize,
/// Datagram-aligned mode (plan §4.4): packetize at this boundary and pad every codec /// Datagram-aligned mode (plan §4.4): packetize at this boundary and pad every codec
@@ -218,22 +224,24 @@ impl PyroWaveEncoder {
bitrate_bps: u64, bitrate_bps: u64,
chroma: crate::ChromaFormat, chroma: crate::ChromaFormat,
) -> Result<Self> { ) -> Result<Self> {
if chroma.is_444() { if !chroma.is_444() && (width % 2 != 0 || height % 2 != 0) {
// Negotiation can't reach here yet: `can_encode_444` returns false for PyroWave
// until the full-res-chroma rgb2yuv variant lands (design/pyrowave-444-hdr.md
// Phase 2). The parameter is threaded now so that flip is one-file.
bail!("pyrowave 4:4:4 encode not implemented yet (Phase 2)");
}
if width % 2 != 0 || height % 2 != 0 {
bail!("pyrowave 4:2:0 needs even dimensions (got {width}x{height})"); bail!("pyrowave 4:2:0 needs even dimensions (got {width}x{height})");
} }
// SAFETY: `open_inner` only issues Vulkan/pyrowave calls whose preconditions it // SAFETY: `open_inner` only issues Vulkan/pyrowave calls whose preconditions it
// establishes itself (valid instance/device, correctly-chained create-infos that // establishes itself (valid instance/device, correctly-chained create-infos that
// `DeviceHold` keeps alive); all handles are freshly created and owned by the result. // `DeviceHold` keeps alive); all handles are freshly created and owned by the result.
unsafe { Self::open_inner(width, height, fps.max(1), bitrate_bps.max(1_000_000)) } unsafe {
Self::open_inner(
width,
height,
fps.max(1),
bitrate_bps.max(1_000_000),
chroma.is_444(),
)
}
} }
unsafe fn open_inner(w: u32, h: u32, fps: u32, bitrate: u64) -> Result<Self> { unsafe fn open_inner(w: u32, h: u32, fps: u32, bitrate: u64, chroma444: bool) -> Result<Self> {
let entry = ash::Entry::load().context("load vulkan loader")?; let entry = ash::Entry::load().context("load vulkan loader")?;
let mut hold = DeviceHold { let mut hold = DeviceHold {
@@ -386,7 +394,11 @@ impl PyroWaveEncoder {
device: pw_dev, device: pw_dev,
width: w as i32, width: w as i32,
height: h as i32, height: h as i32,
chroma: pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_420, chroma: if chroma444 {
pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_444
} else {
pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_420
},
}; };
let mut pw_enc: pw::pyrowave_encoder = std::ptr::null_mut(); let mut pw_enc: pw::pyrowave_encoder = std::ptr::null_mut();
if let Err(e) = pw_check( if let Err(e) = pw_check(
@@ -397,8 +409,10 @@ impl PyroWaveEncoder {
return Err(e); return Err(e);
} }
// ---- CSC planes: full-res R8 luma + half-res RG8 chroma, storage-written by the CSC // ---- CSC planes: full-res R8 luma + RG8 chroma (half-res for 4:2:0, full-res for
// and sampled directly by pyrowave (R/G view swizzles synthesize Cb/Cr) ---- // 4:4:4), storage-written by the CSC and sampled directly by pyrowave (R/G view
// swizzles synthesize Cb/Cr) ----
let (cw, ch) = if chroma444 { (w, h) } else { (w / 2, h / 2) };
let (y_img, y_mem, y_view) = make_plain_image( let (y_img, y_mem, y_view) = make_plain_image(
&device, &device,
&mem_props, &mem_props,
@@ -411,8 +425,8 @@ impl PyroWaveEncoder {
&device, &device,
&mem_props, &mem_props,
vk::Format::R8G8_UNORM, vk::Format::R8G8_UNORM,
w / 2, cw,
h / 2, ch,
vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::SAMPLED, vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::SAMPLED,
)?; )?;
@@ -425,7 +439,11 @@ impl PyroWaveEncoder {
.address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE), .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE),
None, None,
)?; )?;
let spv = ash::util::read_spv(&mut std::io::Cursor::new(CSC_SPV))?; let spv = ash::util::read_spv(&mut std::io::Cursor::new(if chroma444 {
CSC444_SPV
} else {
CSC_SPV
}))?;
let shader = let shader =
device.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&spv), None)?; device.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&spv), None)?;
let sb = |b: u32, t: vk::DescriptorType| { let sb = |b: u32, t: vk::DescriptorType| {
@@ -571,7 +589,8 @@ impl PyroWaveEncoder {
gpu = %props.device_name_as_c_str().unwrap_or(c"?").to_string_lossy(), gpu = %props.device_name_as_c_str().unwrap_or(c"?").to_string_lossy(),
mode = %format!("{w}x{h}@{fps}"), mode = %format!("{w}x{h}@{fps}"),
budget_kib = frame_budget / 1024, budget_kib = frame_budget / 1024,
"PyroWave encoder open (intra-only wavelet, BT.709 limited 4:2:0)" chroma = if chroma444 { "4:4:4" } else { "4:2:0" },
"PyroWave encoder open (intra-only wavelet, BT.709 limited)"
); );
Ok(Self { Ok(Self {
@@ -613,6 +632,7 @@ impl PyroWaveEncoder {
width: w, width: w,
height: h, height: h,
fps, fps,
chroma444,
frame_budget, frame_budget,
wire_chunk: None, wire_chunk: None,
bitstream: Vec::new(), bitstream: Vec::new(),
@@ -976,7 +996,12 @@ impl PyroWaveEncoder {
0, 0,
&pc_bytes, &pc_bytes,
); );
dev.cmd_dispatch(self.cmd, (w / 2).div_ceil(8), (h / 2).div_ceil(8), 1); // 4:2:0: one invocation per 2x2 luma block (per chroma sample); 4:4:4: per pixel.
if self.chroma444 {
dev.cmd_dispatch(self.cmd, w.div_ceil(8), h.div_ceil(8), 1);
} else {
dev.cmd_dispatch(self.cmd, (w / 2).div_ceil(8), (h / 2).div_ceil(8), 1);
}
// CSC storage writes -> pyrowave's sampled reads (images stay GENERAL — the layout // CSC storage writes -> pyrowave's sampled reads (images stay GENERAL — the layout
// pyrowave's GPU-buffer contract accepts without transitions). // pyrowave's GPU-buffer contract accepts without transitions).
@@ -1029,17 +1054,19 @@ impl PyroWaveEncoder {
), ),
// Two-component chroma image: view swizzles R/G synthesize the Cb/Cr planes // Two-component chroma image: view swizzles R/G synthesize the Cb/Cr planes
// (the documented NV12-style hand-off, pyrowave.h `pyrowave_gpu_buffers`). // (the documented NV12-style hand-off, pyrowave.h `pyrowave_gpu_buffers`).
// The view extent is the chroma IMAGE's own mip0 extent (it's a separate
// image, not a planar aspect): half-res for 4:2:0, full-res for 4:4:4.
plane( plane(
self.uv_img, self.uv_img,
w / 2, if self.chroma444 { w } else { w / 2 },
h / 2, if self.chroma444 { h } else { h / 2 },
rg8, rg8,
pw::VkComponentSwizzle_VK_COMPONENT_SWIZZLE_R, pw::VkComponentSwizzle_VK_COMPONENT_SWIZZLE_R,
), ),
plane( plane(
self.uv_img, self.uv_img,
w / 2, if self.chroma444 { w } else { w / 2 },
h / 2, if self.chroma444 { h } else { h / 2 },
rg8, rg8,
pw::VkComponentSwizzle_VK_COMPONENT_SWIZZLE_G, pw::VkComponentSwizzle_VK_COMPONENT_SWIZZLE_G,
), ),
@@ -1158,7 +1185,11 @@ impl Encoder for PyroWaveEncoder {
device: self.pw_dev, device: self.pw_dev,
width: self.width as i32, width: self.width as i32,
height: self.height as i32, height: self.height as i32,
chroma: pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_420, chroma: if self.chroma444 {
pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_444
} else {
pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_420
},
}; };
let mut enc: pw::pyrowave_encoder = std::ptr::null_mut(); let mut enc: pw::pyrowave_encoder = std::ptr::null_mut();
let r = pw::pyrowave_encoder_create(&einfo, &mut enc); let r = pw::pyrowave_encoder_create(&einfo, &mut enc);
@@ -1280,10 +1311,16 @@ mod tests {
) )
} }
/// Decode an AU with a standalone pyrowave decoder and return the full YUV420P planes. /// Decode an AU with a standalone pyrowave decoder and return the full planar YUV
/// This is the golden oracle for both the Phase-1 smoke check (plane means) and the Apple /// (half-res chroma for 4:2:0, full-res for 4:4:4). This is the golden oracle for the
/// Metal port's committed PSNR fixtures (`pyrowave_dump_golden`). /// smoke checks (plane means) and the Apple Metal port's committed PSNR fixtures
unsafe fn decode_planes(w: u32, h: u32, au: &[u8]) -> (Vec<u8>, Vec<u8>, Vec<u8>) { /// (`pyrowave_dump_golden`).
unsafe fn decode_planes_chroma(
w: u32,
h: u32,
au: &[u8],
chroma444: bool,
) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let mut dev: pw::pyrowave_device = std::ptr::null_mut(); let mut dev: pw::pyrowave_device = std::ptr::null_mut();
assert_eq!( assert_eq!(
pw::pyrowave_create_default_device(&mut dev), pw::pyrowave_create_default_device(&mut dev),
@@ -1293,7 +1330,11 @@ mod tests {
device: dev, device: dev,
width: w as i32, width: w as i32,
height: h as i32, height: h as i32,
chroma: pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_420, chroma: if chroma444 {
pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_444
} else {
pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_420
},
fragment_path: false, fragment_path: false,
}; };
let mut dec: pw::pyrowave_decoder = std::ptr::null_mut(); let mut dec: pw::pyrowave_decoder = std::ptr::null_mut();
@@ -1307,11 +1348,16 @@ mod tests {
); );
assert!(pw::pyrowave_decoder_decode_is_ready(dec, false)); assert!(pw::pyrowave_decoder_decode_is_ready(dec, false));
let (cw, ch) = if chroma444 { (w, h) } else { (w / 2, h / 2) };
let mut y = vec![0u8; (w * h) as usize]; let mut y = vec![0u8; (w * h) as usize];
let mut cb = vec![0u8; (w * h / 4) as usize]; let mut cb = vec![0u8; (cw * ch) as usize];
let mut cr = vec![0u8; (w * h / 4) as usize]; let mut cr = vec![0u8; (cw * ch) as usize];
let mut buf: pw::pyrowave_cpu_buffer = std::mem::zeroed(); let mut buf: pw::pyrowave_cpu_buffer = std::mem::zeroed();
buf.format = pw::pyrowave_cpu_buffer_format_PYROWAVE_CPU_BUFFER_FORMAT_YUV420P; buf.format = if chroma444 {
pw::pyrowave_cpu_buffer_format_PYROWAVE_CPU_BUFFER_FORMAT_YUV444P
} else {
pw::pyrowave_cpu_buffer_format_PYROWAVE_CPU_BUFFER_FORMAT_YUV420P
};
buf.width = w as i32; buf.width = w as i32;
buf.height = h as i32; buf.height = h as i32;
buf.data = [ buf.data = [
@@ -1319,7 +1365,7 @@ mod tests {
cb.as_mut_ptr() as *mut _, cb.as_mut_ptr() as *mut _,
cr.as_mut_ptr() as *mut _, cr.as_mut_ptr() as *mut _,
]; ];
buf.row_stride_in_bytes = [w as usize, (w / 2) as usize, (w / 2) as usize]; buf.row_stride_in_bytes = [w as usize, cw as usize, cw as usize];
buf.plane_size_in_bytes = [y.len(), cb.len(), cr.len()]; buf.plane_size_in_bytes = [y.len(), cb.len(), cr.len()];
assert_eq!( assert_eq!(
pw::pyrowave_decoder_decode_cpu_buffer_synchronous(dec, &buf), pw::pyrowave_decoder_decode_cpu_buffer_synchronous(dec, &buf),
@@ -1330,10 +1376,15 @@ mod tests {
(y, cb, cr) (y, cb, cr)
} }
/// Plane means of an upstream-decoded AU — the Phase-1 smoke assertion. unsafe fn decode_planes(w: u32, h: u32, au: &[u8]) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
unsafe fn decode_plane_means(w: u32, h: u32, au: &[u8]) -> (f64, f64, f64) {
// SAFETY: forwarded — same contract as the caller. // SAFETY: forwarded — same contract as the caller.
let (y, cb, cr) = unsafe { decode_planes(w, h, au) }; unsafe { decode_planes_chroma(w, h, au, false) }
}
/// Plane means of an upstream-decoded AU — the smoke assertion.
unsafe fn decode_plane_means(w: u32, h: u32, au: &[u8], chroma444: bool) -> (f64, f64, f64) {
// SAFETY: forwarded — same contract as the caller.
let (y, cb, cr) = unsafe { decode_planes_chroma(w, h, au, chroma444) };
let mean = |v: &[u8]| v.iter().map(|&x| x as f64).sum::<f64>() / v.len() as f64; let mean = |v: &[u8]| v.iter().map(|&x| x as f64).sum::<f64>() / v.len() as f64;
(mean(&y), mean(&cb), mean(&cr)) (mean(&y), mean(&cb), mean(&cr))
} }
@@ -1368,7 +1419,7 @@ mod tests {
"AU exceeds rate budget" "AU exceeds rate budget"
); );
// SAFETY: test-only FFI into the vendored decoder with locally-owned buffers. // SAFETY: test-only FFI into the vendored decoder with locally-owned buffers.
let (ym, cbm, crm) = unsafe { decode_plane_means(w, h, &au.data) }; let (ym, cbm, crm) = unsafe { decode_plane_means(w, h, &au.data, false) };
let (ye, cbe, cre) = bt709(*c); let (ye, cbe, cre) = bt709(*c);
assert!( assert!(
(ym - ye).abs() < 3.0 && (cbm - cbe).abs() < 3.0 && (crm - cre).abs() < 3.0, (ym - ye).abs() < 3.0 && (cbm - cbe).abs() < 3.0 && (crm - cre).abs() < 3.0,
@@ -1462,6 +1513,68 @@ mod tests {
assert!(enc.poll().expect("poll").is_some()); assert!(enc.poll().expect("poll").is_some());
} }
/// The 4:4:4 twin of `pyrowave_smoke`: per-pixel CSC into full-res RG8 chroma +
/// `Chroma444` pyrowave objects, verified by upstream's own 4:4:4 CPU decode. The
/// busy-card leg then drives the rate controller at the ~2.6 bpp operating point —
/// exactly the regime that overran upstream's 4:2:0-sized payload staging before
/// `patches/0001-payload-data-444-sizing.patch` (the Phase-0 finding): it must stay
/// within budget, decode, and be run-to-run deterministic (the overrun was not).
#[test]
#[ignore = "needs a real Vulkan 1.3 compute device (run on a GPU host, not the build box)"]
fn pyrowave_smoke_444() {
let (w, h) = (256u32, 256u32);
let mut enc =
PyroWaveEncoder::open(w, h, 60, 40_000_000, crate::ChromaFormat::Yuv444).expect("open");
let colors = [
[40u8, 40, 200, 255],
[40, 200, 40, 255],
[200, 40, 40, 255],
[128, 128, 128, 255],
];
for (i, c) in colors.iter().enumerate() {
enc.submit(&cpu_frame(w, h, i as u64 * 16_666_667, *c))
.expect("submit");
let au = enc.poll().expect("poll").expect("one AU per frame");
assert!(au.keyframe);
assert!(
au.data.len() <= enc.frame_budget + BS_SLACK,
"AU exceeds rate budget"
);
// SAFETY: test-only FFI into the vendored decoder with locally-owned buffers.
let (ym, cbm, crm) = unsafe { decode_plane_means(w, h, &au.data, true) };
let (ye, cbe, cre) = bt709(*c);
assert!(
(ym - ye).abs() < 3.0 && (cbm - cbe).abs() < 3.0 && (crm - cre).abs() < 3.0,
"frame {i}: decoded plane means (Y {ym:.1}, Cb {cbm:.1}, Cr {crm:.1}) vs \
expected (Y {ye:.1}, Cb {cbe:.1}, Cr {cre:.1})"
);
}
// Busy content at the 4:4:4 operating point (~2.6 bpp).
let budget_bps = w as u64 * h as u64 * 60 * 26 / 10;
let mut enc =
PyroWaveEncoder::open(w, h, 60, budget_bps, crate::ChromaFormat::Yuv444).expect("open");
let mut sizes = Vec::new();
for _ in 0..3 {
enc.submit(&test_card(w, h, 7)).expect("busy submit");
let au = enc.poll().expect("poll").expect("busy AU");
assert!(
au.data.len() <= enc.frame_budget + BS_SLACK,
"busy 4:4:4 AU exceeds rate budget ({} > {})",
au.data.len(),
enc.frame_budget + BS_SLACK
);
// Upstream's own decoder accepts it (a corrupt stream errors or garbles).
// SAFETY: test-only FFI with locally-owned buffers.
let _ = unsafe { decode_planes_chroma(w, h, &au.data, true) };
sizes.push(au.data.len());
}
assert!(
sizes.windows(2).all(|s| s[0] == s[1]),
"identical input produced varying AU sizes (the Phase-0 overrun signature): {sizes:?}"
);
}
/// A deterministic busy BGRA test card (gradients + checker + LCG noise) — flat fills /// A deterministic busy BGRA test card (gradients + checker + LCG noise) — flat fills
/// exercise almost none of the entropy decoder, this hits every subband. /// exercise almost none of the entropy decoder, this hits every subband.
fn test_card(w: u32, h: u32, seed: u32) -> CapturedFrame { fn test_card(w: u32, h: u32, seed: u32) -> CapturedFrame {
@@ -0,0 +1,37 @@
#version 450
// RGB(A) -> full-res Y + FULL-res interleaved CbCr (BT.709 limited range): the 4:4:4 twin of
// rgb2yuv.comp — one invocation per pixel, no chroma box filter, no siting. Same coefficients
// byte-for-byte (the wavelet clients' planar CSC decodes both layouts identically), same
// cursor-as-metadata blend, same source-edge clamp for the 32-aligned coded extent.
layout(local_size_x = 8, local_size_y = 8) in;
layout(binding = 0) uniform sampler2D rgb; // packed RGB input (sampled; BGRA import ok)
layout(binding = 1, r8) uniform writeonly image2D yImg; // full-res Y
layout(binding = 2, rg8) uniform writeonly image2D uvImg; // full-res UV (interleaved)
layout(binding = 3) uniform sampler2D cursorTex; // straight-alpha RGBA cursor (top-left)
layout(push_constant) uniform Push {
ivec2 curOrigin; // top-left of the cursor in frame pixels (position - hotspot)
ivec2 curSize; // cursor w,h in pixels; x <= 0 => disabled
} pc;
float lumaY(vec3 c) { return 16.0/255.0 + 0.1826*c.r + 0.6142*c.g + 0.0620*c.b; }
vec3 withCursor(ivec2 p, vec3 col) {
if (pc.curSize.x <= 0) return col;
ivec2 cp = p - pc.curOrigin;
if (cp.x < 0 || cp.y < 0 || cp.x >= pc.curSize.x || cp.y >= pc.curSize.y) return col;
vec4 c = texelFetch(cursorTex, cp, 0);
return mix(col, c.rgb, c.a);
}
void main() {
ivec2 sz = imageSize(yImg);
ivec2 rmax = textureSize(rgb, 0) - 1;
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
if (p.x >= sz.x || p.y >= sz.y) return;
vec3 c = withCursor(p, texelFetch(rgb, min(p, rmax), 0).rgb);
imageStore(yImg, p, vec4(lumaY(c), 0, 0, 1));
float U = 128.0/255.0 - 0.1006*c.r - 0.3386*c.g + 0.4392*c.b;
float V = 128.0/255.0 + 0.4392*c.r - 0.3989*c.g - 0.0403*c.b;
imageStore(uvImg, p, vec4(U, V, 0, 1));
}
Binary file not shown.
+4 -4
View File
@@ -871,10 +871,10 @@ pub fn can_encode_444(codec: Codec) -> bool {
use std::sync::{Mutex, OnceLock}; use std::sync::{Mutex, OnceLock};
if codec == Codec::PyroWave { if codec == Codec::PyroWave {
// PyroWave does its own RGB→YCbCr CSC (capture always hands it a full-chroma source), // PyroWave does its own RGB→YCbCr CSC (capture always hands it a full-chroma source),
// so 4:4:4 needs no GPU encode probe — only the full-res-chroma CSC variant, which // so 4:4:4 needs no GPU encode probe — only the full-res-chroma CSC variant:
// hasn't landed yet (design/pyrowave-444-hdr.md: Phase 2 Linux, Phase 3 Windows). // `rgb2yuv444.comp` on Linux (landed, design/pyrowave-444-hdr.md Phase 2); the
// Flip per-OS when it does. // Windows `BgraToYuvPlanes` twin is Phase 3.
return false; return cfg!(target_os = "linux");
} }
if codec != Codec::H265 { if codec != Codec::H265 {
return false; return false;