#![cfg(all(target_os = "linux", feature = "readback-test"))]
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::Arc;
use gpui::{
AnyWindowHandle, AppContext as _, Context, GpuCanvasContext, IntoElement, Render,
SurfaceFormatKind, Window, div, gpu_canvas, prelude::*, px, size,
};
use gpui_interop::{GpuDevice, SharedBuffer, Tiling, attach};
use gpui_wgpu::{CosmicTextSystem, WgpuHeadlessRenderer};
const EDGE: u32 = 64;
const CLEAR: [f32; 4] = [128.0 / 255.0, 128.0 / 255.0, 0.0, 1.0];
const EXPECTED: [u8; 4] = [128, 128, 0, 255];
#[derive(Clone, Copy)]
enum Which {
Linear,
Tiled,
Nv12,
}
struct Producer {
linear: SharedBuffer,
tiled: SharedBuffer,
nv12: SharedBuffer,
_device: GpuDevice,
}
impl Producer {
fn build(gpu: &mut GpuCanvasContext) -> Option<Self> {
let interop = attach(gpu.try_device::<WgpuHeadlessRenderer>()).ok()?;
interop.adapter().gpu_id();
let device = interop.adapter().gpu_device()?;
let linear = cleared(&device, Tiling::Linear)?;
let tiled = cleared(&device, Tiling::Tiled)?;
let nv12 = nv12_filled(&device)?;
Some(Self {
linear,
tiled,
nv12,
_device: device,
})
}
}
fn cleared(device: &GpuDevice, tiling: Tiling) -> Option<SharedBuffer> {
let buffer = SharedBuffer::new(device, EDGE, EDGE, SurfaceFormatKind::rgba8(), tiling).ok()?;
buffer.clear(device, CLEAR).ok()?;
Some(buffer)
}
fn nv12_filled(device: &GpuDevice) -> Option<SharedBuffer> {
let (y, cb, cr) = nv12_from_rgb(EXPECTED);
let luma = vec![y; (EDGE * EDGE) as usize];
let mut chroma = vec![0u8; (EDGE / 2 * EDGE / 2 * 2) as usize];
for pair in chroma.as_chunks_mut::<2>().0 {
pair[0] = cb;
pair[1] = cr;
}
let buffer = SharedBuffer::new(
device,
EDGE,
EDGE,
SurfaceFormatKind::nv12(),
Tiling::Linear,
)
.ok()?;
buffer.fill(device, &[&luma, &chroma]).ok()?;
Some(buffer)
}
struct View {
producer: Rc<RefCell<Option<Producer>>>,
built: Rc<Cell<bool>>,
}
impl Render for View {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_row()
.size_full()
.child(tile(
self.producer.clone(),
self.built.clone(),
Which::Linear,
))
.child(tile(
self.producer.clone(),
self.built.clone(),
Which::Tiled,
))
.child(tile(self.producer.clone(), self.built.clone(), Which::Nv12))
}
}
fn tile(
slot: Rc<RefCell<Option<Producer>>>,
built: Rc<Cell<bool>>,
which: Which,
) -> impl IntoElement {
gpu_canvas(move |gpu| {
let mut slot = slot.borrow_mut();
if slot.is_none() {
match Producer::build(gpu) {
Some(producer) => {
*slot = Some(producer);
built.set(true);
}
None => return,
}
}
let handle = match which {
Which::Linear => slot.as_ref().expect("just built").linear.handle(),
Which::Tiled => slot.as_ref().expect("just built").tiled.handle(),
Which::Nv12 => slot.as_ref().expect("just built").nv12.handle(),
};
gpu.paint_surface(handle);
})
.w(px(EDGE as f32))
.h(px(EDGE as f32))
}
#[test]
fn an_exported_buffer_composites_byte_for_byte() {
if !gpui::current_headless_renderer().is_ok_and(|renderer| renderer.is_some()) {
eprintln!("skipping: no headless renderer (no Vulkan adapter)");
return;
}
let text_system = Arc::new(CosmicTextSystem::new("fallback"));
let mut cx = gpui::HeadlessAppContext::with_platform(text_system, Arc::new(()), || {
gpui::current_headless_renderer()
});
let built = Rc::new(Cell::new(false));
let window = cx
.open_window(
size(px((EDGE * 3) as f32), px(EDGE as f32)),
|_window, cx| {
cx.new(|_| View {
producer: Rc::new(RefCell::new(None)),
built: built.clone(),
})
},
)
.expect("a window to composite into");
let window: AnyWindowHandle = window.into();
cx.run_until_parked();
cx.update_window(window, |_, window, cx| {
let _ = window.draw(cx);
})
.expect("draw a frame");
if !built.get() {
eprintln!("skipping: the renderer's device lent none that exports a dma-buf");
return;
}
assert!(
gpui_interop::gpu_id().is_some(),
"reading the renderer's GPU with Adapter::gpu_id should remember it"
);
let image = cx.capture_screenshot(window).expect("read the frame back");
for (label, x) in [("linear", EDGE / 2), ("tiled", EDGE + EDGE / 2)] {
assert_eq!(
image.get_pixel(x, EDGE / 2).0,
EXPECTED,
"the {label} buffer did not round trip"
);
}
let (y, cb, cr) = nv12_from_rgb(EXPECTED);
let expected = ycbcr_to_rgb(
f32::from(y) / 255.0,
f32::from(cb) / 255.0,
f32::from(cr) / 255.0,
);
let pixel = image.get_pixel(EDGE * 2 + EDGE / 2, EDGE / 2).0;
for channel in 0..3 {
let want = (expected[channel] * 255.0).round() as i32;
let got = i32::from(pixel[channel]);
assert!(
(got - want).abs() <= 2,
"the nv12 buffer's channel {channel} is {got}, but the shader's matrix predicts {want}",
);
}
assert_eq!(pixel[3], 255, "the nv12 surface should be opaque");
}
fn ycbcr_to_rgb(y: f32, cb: f32, cr: f32) -> [f32; 4] {
let columns = [
[1.0, 1.0, 1.0],
[0.0, -0.3441, 1.7720],
[1.4020, -0.7141, 0.0],
[-0.7010, 0.5291, -0.8860],
];
let mut rgb = [0.0f32; 4];
for channel in 0..3 {
rgb[channel] = (columns[0][channel] * y
+ columns[1][channel] * cb
+ columns[2][channel] * cr
+ columns[3][channel])
.clamp(0.0, 1.0);
}
rgb[3] = 1.0;
rgb
}
fn nv12_from_rgb(rgb: [u8; 4]) -> (u8, u8, u8) {
let r = f32::from(rgb[0]) / 255.0;
let g = f32::from(rgb[1]) / 255.0;
let b = f32::from(rgb[2]) / 255.0;
let (r, g, b) = (r + 0.7010, g - 0.5291, b + 0.8860);
let y = 0.299 * r + 0.587 * g + 0.114 * b;
let cb = -0.168736 * r - 0.331264 * g + 0.5 * b;
let cr = 0.5 * r - 0.418688 * g - 0.081312 * b;
(
(y.clamp(0.0, 1.0) * 255.0).round() as u8,
(cb.clamp(0.0, 1.0) * 255.0).round() as u8,
(cr.clamp(0.0, 1.0) * 255.0).round() as u8,
)
}