Lightningbeam/lightningbeam-ui/gpu-video-encoder/tests/decode_roundtrip.rs

92 lines
3.9 KiB
Rust

//! Round-trip: encode solid frames with the zero-copy encoder, then hardware-decode them back
//! into a wgpu texture and read the Y plane. Verifies the VAAPI decode → DMA-BUF → wgpu import
//! path produces real pixels on the GPU. Skips when VAAPI is unavailable.
#![cfg(target_os = "linux")]
use gpu_video_encoder::decoder::VaapiDecoder;
use gpu_video_encoder::encoder::ZeroCopyEncoder;
#[test]
fn vaapi_decode_roundtrip() {
// 256-wide so the R8 Y readback row (256 B) is already 256-aligned.
let (w, h) = (256u32, 256u32);
let out = std::env::temp_dir().join("gpu_video_encoder_decode_rt.mp4");
let _ = std::fs::remove_file(&out);
// --- Encode 10 frames of solid mid-gray. Full range → Y == luma ≈ 128. ---
{
let mut enc = match ZeroCopyEncoder::new(w, h, 30, 4000, &out, true) {
Ok(e) => e,
Err(e) => {
eprintln!("[decode-rt] encode unavailable, skipping: {e}");
return;
}
};
let device = enc.device();
let src = device.create_texture(&wgpu::TextureDescriptor {
label: Some("gray"),
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let gray = vec![128u8; (w * h * 4) as usize];
enc.queue().write_texture(
wgpu::TexelCopyTextureInfo { texture: &src, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
&gray,
wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(w * 4), rows_per_image: Some(h) },
wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
);
for _ in 0..10 {
enc.encode_rgba(&src).expect("encode_rgba");
}
enc.finish().expect("finish");
}
// --- Decode it back on the GPU. ---
let mut dec = match VaapiDecoder::new(&out) {
Ok(d) => d,
Err(e) => {
eprintln!("[decode-rt] decode unavailable, skipping: {e}");
return;
}
};
let frame = dec.next_frame().expect("next_frame").expect("expected at least one frame");
assert_eq!(frame.y().width(), w, "decoded Y width");
assert_eq!(frame.y().height(), h, "decoded Y height");
// Read back the Y plane (R8) and check it's ≈ the gray we encoded.
let device = dec.device();
let buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("y_readback"),
size: (w * h) as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut cmd = device.create_command_encoder(&Default::default());
cmd.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo { texture: frame.y(), mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
wgpu::TexelCopyBufferInfo {
buffer: &buf,
layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(w), rows_per_image: Some(h) },
},
wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
);
dec.queue().submit(Some(cmd.finish()));
buf.slice(..).map_async(wgpu::MapMode::Read, |_| {});
let _ = device.poll(wgpu::PollType::wait_indefinitely());
let data = buf.slice(..).get_mapped_range();
let mean = data.iter().map(|&b| b as f64).sum::<f64>() / data.len() as f64;
eprintln!("[decode-rt] decoded {w}x{h}, mean Y = {mean:.1}");
assert!(
(mean - 128.0).abs() < 12.0,
"mean Y {mean} not ≈ 128 — decode produced wrong pixels"
);
eprintln!("[decode-rt] ✅ VAAPI decode → wgpu texture verified");
}