#![cfg(all(
feature = "wgpu",
any(target_os = "macos", target_os = "linux", target_os = "windows")
))]
mod common;
use common::engine_with_frame;
use mpv_engine::ExportedFrame;
fn wgpu_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let mut descriptor = wgpu::InstanceDescriptor::new_without_display_handle();
#[cfg(target_os = "macos")]
{
descriptor.backends = wgpu::Backends::METAL;
}
#[cfg(target_os = "linux")]
{
descriptor.backends = wgpu::Backends::VULKAN;
}
#[cfg(target_os = "windows")]
{
descriptor.backends = wgpu::Backends::DX12;
}
let instance = wgpu::Instance::new(descriptor);
let adapter = match pollster::block_on(instance.request_adapter(&Default::default())) {
Ok(adapter) => adapter,
Err(e) => {
eprintln!("skipping: no wgpu adapter for the native backend: {e}");
return None;
}
};
#[cfg(target_os = "linux")]
let device_descriptor = {
let needed = wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF;
if !adapter.features().contains(needed) {
eprintln!("skipping: adapter lacks VULKAN_EXTERNAL_MEMORY_DMA_BUF");
return None;
}
wgpu::DeviceDescriptor {
required_features: needed,
..Default::default()
}
};
#[cfg(not(target_os = "linux"))]
let device_descriptor = wgpu::DeviceDescriptor::default();
match pollster::block_on(adapter.request_device(&device_descriptor)) {
Ok(pair) => Some(pair),
Err(e) => {
eprintln!("skipping: wgpu device unavailable: {e}");
None
}
}
}
fn frame_has_alpha(frame: &ExportedFrame) -> bool {
#[cfg(target_os = "linux")]
{
const FOURCC_XR24: u32 = 0x3432_5258;
frame.fourcc() != FOURCC_XR24
}
#[cfg(not(target_os = "linux"))]
{
let _ = frame;
true
}
}
fn assert_pixels_match(via_wgpu: &[u8], expected: &[u8], has_alpha: bool, what: &str) {
assert_eq!(via_wgpu.len(), expected.len());
if has_alpha {
assert_eq!(via_wgpu, expected, "{what}");
} else {
let mask = |px: &[u8]| {
let mut px = px.to_vec();
px.iter_mut().skip(3).step_by(4).for_each(|a| *a = 0);
px
};
assert_eq!(mask(via_wgpu), mask(expected), "{what} (alpha masked)");
}
}
fn read_back(device: &wgpu::Device, queue: &wgpu::Queue, texture: &wgpu::Texture) -> Vec<u8> {
let size = 64 * 64 * 4;
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&Default::default());
encoder.copy_texture_to_buffer(
texture.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(64 * 4),
rows_per_image: None,
},
},
wgpu::Extent3d {
width: 64,
height: 64,
depth_or_array_layers: 1,
},
);
queue.submit([encoder.finish()]);
let slice = buffer.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result);
});
device
.poll(wgpu::PollType::wait_indefinitely())
.expect("poll");
rx.recv().expect("map callback").expect("map");
let data = slice.get_mapped_range().expect("mapped range").to_vec();
drop(buffer);
data
}
#[test]
fn imported_texture_matches_frame_pixels() {
let Some((engine, frame, _rx)) = engine_with_frame() else {
return;
};
let Some((device, queue)) = wgpu_device() else {
return;
};
let expected = frame.copy_pixels();
let has_alpha = frame_has_alpha(&frame);
let texture = frame.into_wgpu_texture(&device).expect("wgpu import");
assert_eq!(texture.format(), wgpu::TextureFormat::Bgra8Unorm);
assert_eq!((texture.width(), texture.height()), (64, 64));
let via_wgpu = read_back(&device, &queue, &texture);
assert_pixels_match(
&via_wgpu,
&expected,
has_alpha,
"wgpu readback differs from the exported buffer",
);
let top = (8 * 64 + 32) * 4;
assert!(
via_wgpu[top + 2] > 150 && via_wgpu[top] < 100,
"top pixel should be red in BGRA"
);
drop(texture);
let _ = device.poll(wgpu::PollType::wait_indefinitely());
engine.detach_render();
}
#[test]
fn texture_outlives_frame_and_detach() {
let Some((engine, frame, _rx)) = engine_with_frame() else {
return;
};
let Some((device, queue)) = wgpu_device() else {
return;
};
let expected = frame.copy_pixels();
let has_alpha = frame_has_alpha(&frame);
let texture = frame.into_wgpu_texture(&device).expect("wgpu import");
engine.detach_render();
drop(engine);
let via_wgpu = read_back(&device, &queue, &texture);
assert_pixels_match(&via_wgpu, &expected, has_alpha, "readback after detach");
}
fn generate_moving_clip(target: &std::path::Path) -> Option<()> {
let status = std::process::Command::new("ffmpeg")
.args([
"-y",
"-f",
"lavfi",
"-i",
"testsrc=size=64x64:rate=15",
"-t",
"2",
"-c:v",
"rawvideo",
"-pix_fmt",
"yuv420p",
])
.arg(target)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
matches!(status, Ok(s) if s.success()).then_some(())
}
#[test]
fn continuous_import_recycles_buffers_and_stays_correct() {
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::time::{Duration, Instant};
let Some((device, queue)) = wgpu_device() else {
return;
};
let Some(engine) = common::video_engine() else {
return;
};
let dir = tempfile::tempdir().expect("tempdir");
let clip = dir.path().join("moving.nut");
if generate_moving_clip(&clip).is_none() {
return;
}
let (tx, rx) = std::sync::mpsc::channel();
if engine
.attach_exported_render(mpv_engine::ExportOptions::new(64, 64), move || {
let _ = tx.send(());
})
.is_err()
{
eprintln!("skipping: exported render unavailable");
return;
}
engine.set_property("loop-file", "inf").expect("loop-file");
engine
.load_when_ready(clip.to_str().expect("utf-8 path"))
.expect("load");
const WANT: usize = 24;
let deadline = Instant::now() + Duration::from_secs(20);
let mut imported = 0usize;
let mut fingerprints = HashSet::new();
while imported < WANT && Instant::now() < deadline {
let _ = rx.recv_timeout(Duration::from_millis(100));
let Some(frame) = engine.acquire_frame().expect("acquire") else {
continue;
};
let cpu = frame.copy_pixels();
if !cpu.chunks_exact(4).any(|px| px[0] | px[1] | px[2] != 0) {
continue;
}
let has_alpha = frame_has_alpha(&frame);
let texture = frame.into_wgpu_texture(&device).expect("wgpu import");
let via_wgpu = read_back(&device, &queue, &texture);
assert_pixels_match(
&via_wgpu,
&cpu,
has_alpha,
"recycled-buffer readback differs from the frame's own pixels",
);
let mut h = std::collections::hash_map::DefaultHasher::new();
cpu.hash(&mut h);
fingerprints.insert(h.finish());
drop(texture);
let _ = device.poll(wgpu::PollType::wait_indefinitely());
imported += 1;
}
assert!(
imported >= WANT,
"only imported {imported}/{WANT} frames from a looping source"
);
assert!(
fingerprints.len() >= 2,
"every recycled buffer read back identical content — a stuck or aliased buffer"
);
engine.detach_render();
}