use crate::error::{Error, Result};
use crate::frame::{Frame, PixelFormat};
use crate::geometry::{Mat3, Quad};
const SHADER: &str = include_str!("render/present.wgsl");
const ROW_ALIGNMENT: u32 = 256;
pub struct Gpu {
pub instance: wgpu::Instance,
pub adapter: wgpu::Adapter,
pub device: wgpu::Device,
pub queue: wgpu::Queue,
}
impl Gpu {
pub fn new() -> Result<Self> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
Self::from_instance(instance, None)
}
pub fn from_instance(
instance: wgpu::Instance,
surface: Option<&wgpu::Surface<'_>>,
) -> Result<Self> {
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: surface,
force_fallback_adapter: false,
..Default::default()
}))
.map_err(|error| Error::Render {
reason: format!("no usable graphics adapter: {error}"),
})?;
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("vtome"),
..Default::default()
}))
.map_err(|error| Error::Render {
reason: format!("the graphics device would not open: {error}"),
})?;
Ok(Gpu {
instance,
adapter,
device,
queue,
})
}
pub fn describe(&self) -> String {
let info = self.adapter.get_info();
format!("{} ({:?}, {:?})", info.name, info.backend, info.device_type)
}
pub fn max_texture_size(&self) -> u32 {
self.device.limits().max_texture_dimension_2d
}
}
#[repr(u32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Mode {
Rgba = 0,
Planar = 1,
BiPlanar = 2,
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
inverse: [[f32; 4]; 3],
color: [[f32; 4]; 3],
target_size: [f32; 2],
opacity: f32,
mode: u32,
}
struct Planes {
textures: Vec<wgpu::Texture>,
uniforms: wgpu::Buffer,
bind_group: wgpu::BindGroup,
width: u32,
height: u32,
format: PixelFormat,
color: crate::color::ColorSpace,
bit_depth: u32,
}
#[derive(Default)]
pub struct Picture {
planes: Option<Planes>,
}
impl Picture {
pub fn new() -> Self {
Picture::default()
}
pub fn is_empty(&self) -> bool {
self.planes.is_none()
}
}
#[derive(Clone, Copy)]
pub struct Layer<'a> {
pub picture: &'a Picture,
pub quad: Quad,
pub opacity: f32,
}
pub struct Renderer {
pipeline: wgpu::RenderPipeline,
layout: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
current: Picture,
format: wgpu::TextureFormat,
}
impl Renderer {
pub fn new(gpu: &Gpu, format: wgpu::TextureFormat) -> Result<Self> {
let device = &gpu.device;
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("vtome present"),
source: wgpu::ShaderSource::Wgsl(SHADER.into()),
});
let mut entries = vec![wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}];
for binding in 1..=3 {
entries.push(wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
});
}
entries.push(wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
});
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("vtome planes"),
entries: &entries,
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("vtome present"),
bind_group_layouts: &[Some(&layout)],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("vtome present"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vertex"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fragment"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("vtome sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
Ok(Renderer {
pipeline,
layout,
sampler,
current: Picture::new(),
format,
})
}
pub fn format(&self) -> wgpu::TextureFormat {
self.format
}
pub fn upload(&mut self, gpu: &Gpu, frame: &Frame) -> Result<()> {
upload_into(&self.layout, &self.sampler, gpu, &mut self.current, frame)
}
pub fn upload_picture(&self, gpu: &Gpu, picture: &mut Picture, frame: &Frame) -> Result<()> {
upload_into(&self.layout, &self.sampler, gpu, picture, frame)
}
pub fn draw(
&self,
gpu: &Gpu,
view: &wgpu::TextureView,
target_width: u32,
target_height: u32,
quad: Quad,
opacity: f32,
) -> Result<()> {
if self.current.is_empty() {
return Err(Error::Render {
reason: "nothing has been uploaded to draw".to_string(),
});
}
self.draw_layers(
gpu,
view,
target_width,
target_height,
wgpu::Color::TRANSPARENT,
&[Layer {
picture: &self.current,
quad,
opacity,
}],
)
}
pub fn draw_layers(
&self,
gpu: &Gpu,
view: &wgpu::TextureView,
target_width: u32,
target_height: u32,
clear: wgpu::Color,
layers: &[Layer<'_>],
) -> Result<()> {
let mut prepared = Vec::with_capacity(layers.len());
for (index, layer) in layers.iter().enumerate() {
let Some(planes) = layer.picture.planes.as_ref() else {
return Err(Error::Render {
reason: format!("layer {index} has nothing uploaded to draw"),
});
};
if let Some(earlier) = layers[..index]
.iter()
.position(|other| std::ptr::eq(other.picture, layer.picture))
{
return Err(Error::Render {
reason: format!(
"layers {earlier} and {index} are the same picture; upload the frame into \
a second one to draw it twice"
),
});
}
let inverse = layer.quad.inverse_homography()?;
prepared.push((
planes,
Uniforms {
inverse: rows_padded(inverse),
color: planes.color.yuv_to_rgb(planes.bit_depth),
target_size: [target_width as f32, target_height as f32],
opacity: layer.opacity.clamp(0.0, 1.0),
mode: mode_for(planes.format)? as u32,
},
));
}
for (planes, uniforms) in &prepared {
gpu.queue
.write_buffer(&planes.uniforms, 0, bytemuck::bytes_of(uniforms));
}
let mut encoder = gpu
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("vtome present"),
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("vtome present"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(clear),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&self.pipeline);
for (planes, _) in &prepared {
pass.set_bind_group(0, &planes.bind_group, &[]);
pass.draw(0..3, 0..1);
}
}
gpu.queue.submit(std::iter::once(encoder.finish()));
Ok(())
}
pub fn render_to_rgba(
&mut self,
gpu: &Gpu,
frame: &Frame,
width: u32,
height: u32,
quad: Quad,
opacity: f32,
) -> Result<Vec<u8>> {
self.require_rgba8()?;
self.upload(gpu, frame)?;
read_back(gpu, self.format, width, height, |view| {
self.draw(gpu, view, width, height, quad, opacity)
})
}
pub fn render_layers_to_rgba(
&self,
gpu: &Gpu,
width: u32,
height: u32,
clear: wgpu::Color,
layers: &[Layer<'_>],
) -> Result<Vec<u8>> {
self.require_rgba8()?;
read_back(gpu, self.format, width, height, |view| {
self.draw_layers(gpu, view, width, height, clear, layers)
})
}
fn require_rgba8(&self) -> Result<()> {
if matches!(
self.format,
wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm
) {
return Ok(());
}
Err(Error::Render {
reason: format!(
"this renderer draws {:?}; reading back needs one built for \
Rgba8Unorm or Bgra8Unorm",
self.format
),
})
}
}
fn upload_into(
layout: &wgpu::BindGroupLayout,
sampler: &wgpu::Sampler,
gpu: &Gpu,
picture: &mut Picture,
frame: &Frame,
) -> Result<()> {
mode_for(frame.format())?;
let limit = gpu.max_texture_size();
if frame.width() > limit || frame.height() > limit {
return Err(Error::Render {
reason: format!(
"{}×{} is past this device's {limit}-pixel texture limit",
frame.width(),
frame.height()
),
});
}
let stale = picture.planes.as_ref().is_none_or(|planes| {
planes.width != frame.width()
|| planes.height != frame.height()
|| planes.format != frame.format()
});
if stale {
picture.planes = Some(allocate(layout, sampler, gpu, frame));
}
let planes = picture.planes.as_mut().expect("just allocated");
planes.color = frame.color();
planes.bit_depth = frame.format().bit_depth();
for (index, texture) in planes.textures.iter().enumerate() {
let (plane_width, plane_height) = frame
.format()
.plane_dimensions(index, frame.width(), frame.height())
.expect("the texture count came from the plane count");
let descriptor = frame.planes()[index];
let row_bytes = plane_width as usize
* frame.format().samples_per_pixel(index)
* frame.format().bytes_per_sample();
let data = frame.plane_data(index).ok_or_else(|| Error::Render {
reason: format!("plane {index} is not where the frame says it is"),
})?;
gpu.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(descriptor.stride.max(row_bytes) as u32),
rows_per_image: Some(plane_height),
},
wgpu::Extent3d {
width: plane_width,
height: plane_height,
depth_or_array_layers: 1,
},
);
}
Ok(())
}
fn allocate(
layout: &wgpu::BindGroupLayout,
sampler: &wgpu::Sampler,
gpu: &Gpu,
frame: &Frame,
) -> Planes {
let format = frame.format();
let mut textures = Vec::new();
let mut views = Vec::new();
for index in 0..format.plane_count() {
let (width, height) = format
.plane_dimensions(index, frame.width(), frame.height())
.expect("index is below the plane count");
let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
label: Some("vtome plane"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: texture_format(format, index),
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
views.push(texture.create_view(&wgpu::TextureViewDescriptor::default()));
textures.push(texture);
}
let uniforms = gpu.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("vtome uniforms"),
size: std::mem::size_of::<Uniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let filler = &views[0];
let bound: Vec<&wgpu::TextureView> = (0..3)
.map(|index| views.get(index).unwrap_or(filler))
.collect();
let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("vtome planes"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniforms.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(bound[0]),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(bound[1]),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(bound[2]),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
});
Planes {
textures,
uniforms,
bind_group,
width: frame.width(),
height: frame.height(),
format,
color: frame.color(),
bit_depth: format.bit_depth(),
}
}
fn read_back(
gpu: &Gpu,
format: wgpu::TextureFormat,
width: u32,
height: u32,
draw: impl FnOnce(&wgpu::TextureView) -> Result<()>,
) -> Result<Vec<u8>> {
let target = gpu.device.create_texture(&wgpu::TextureDescriptor {
label: Some("vtome offscreen"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&wgpu::TextureViewDescriptor::default());
draw(&view)?;
let unpadded = width * 4;
let padded = unpadded.div_ceil(ROW_ALIGNMENT) * ROW_ALIGNMENT;
let readback = gpu.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("vtome readback"),
size: u64::from(padded) * u64::from(height),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = gpu
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("vtome readback"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &target,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded),
rows_per_image: Some(height),
},
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
gpu.queue.submit(std::iter::once(encoder.finish()));
let (sender, receiver) = std::sync::mpsc::channel();
readback
.slice(..)
.map_async(wgpu::MapMode::Read, move |result| {
let _ = sender.send(result);
});
gpu.device
.poll(wgpu::PollType::wait_indefinitely())
.map_err(|error| Error::Render {
reason: format!("waiting for the GPU: {error}"),
})?;
receiver
.recv()
.map_err(|_| Error::Render {
reason: "the readback never completed".to_string(),
})?
.map_err(|error| Error::Render {
reason: format!("mapping the readback buffer: {error}"),
})?;
let mapped = readback
.slice(..)
.get_mapped_range()
.map_err(|error| Error::Render {
reason: format!("reading the mapped buffer: {error}"),
})?;
let mut pixels = Vec::with_capacity((unpadded * height) as usize);
for row in 0..height {
let start = (row * padded) as usize;
pixels.extend_from_slice(&mapped[start..start + unpadded as usize]);
}
drop(mapped);
readback.unmap();
if format == wgpu::TextureFormat::Bgra8Unorm {
for pixel in pixels.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
}
Ok(pixels)
}
#[cfg_attr(not(any(all(feature = "window", not(any(target_os = "ios", target_os = "android"))), feature = "tauri")), allow(dead_code))]
pub(crate) fn surface_format(capabilities: &wgpu::SurfaceCapabilities) -> wgpu::TextureFormat {
capabilities
.formats
.iter()
.copied()
.find(|format| !format.is_srgb())
.unwrap_or(capabilities.formats[0])
}
#[cfg_attr(not(any(all(feature = "window", not(any(target_os = "ios", target_os = "android"))), feature = "tauri")), allow(dead_code))]
pub(crate) fn see_through(capabilities: &wgpu::SurfaceCapabilities) -> wgpu::CompositeAlphaMode {
use wgpu::CompositeAlphaMode::{Inherit, PostMultiplied, PreMultiplied};
[PreMultiplied, PostMultiplied, Inherit]
.into_iter()
.find(|mode| capabilities.alpha_modes.contains(mode))
.unwrap_or(capabilities.alpha_modes[0])
}
fn mode_for(format: PixelFormat) -> Result<Mode> {
Ok(match format {
PixelFormat::Rgba8 | PixelFormat::Bgra8 => Mode::Rgba,
PixelFormat::I420 | PixelFormat::I422 | PixelFormat::I444 => Mode::Planar,
PixelFormat::Nv12 => Mode::BiPlanar,
PixelFormat::P010 => {
return Err(Error::unsupported(
"10-bit P010 needs 16-bit textures and a shifted sample; \
see planning/TODO.md §11",
))
}
})
}
fn texture_format(format: PixelFormat, plane: usize) -> wgpu::TextureFormat {
match (format, plane) {
(PixelFormat::Rgba8, _) => wgpu::TextureFormat::Rgba8Unorm,
(PixelFormat::Bgra8, _) => wgpu::TextureFormat::Bgra8Unorm,
(PixelFormat::Nv12, 1) => wgpu::TextureFormat::Rg8Unorm,
(PixelFormat::P010, 0) => wgpu::TextureFormat::R16Unorm,
(PixelFormat::P010, _) => wgpu::TextureFormat::Rg16Unorm,
_ => wgpu::TextureFormat::R8Unorm,
}
}
fn rows_padded(matrix: Mat3) -> [[f32; 4]; 3] {
let Mat3(m) = matrix;
std::array::from_fn(|row| [m[row][0] as f32, m[row][1] as f32, m[row][2] as f32, 0.0])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::ColorSpace;
use crate::geometry::{Point, Rect};
use std::time::Duration;
fn gpu() -> Option<Gpu> {
match Gpu::new() {
Ok(gpu) => Some(gpu),
Err(error) => {
eprintln!("skipping: no GPU here ({error})");
None
}
}
}
fn quadrants() -> Frame {
let pixels = vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255, ];
Frame::packed(
2,
2,
PixelFormat::Rgba8,
ColorSpace::srgb(),
Duration::ZERO,
pixels,
)
.unwrap()
}
fn pixel(pixels: &[u8], width: u32, x: u32, y: u32) -> [u8; 4] {
let start = ((y * width + x) * 4) as usize;
pixels[start..start + 4].try_into().unwrap()
}
#[test]
fn a_frame_drawn_over_the_whole_target_keeps_its_orientation() {
let Some(gpu) = gpu() else { return };
let mut renderer = Renderer::new(&gpu, wgpu::TextureFormat::Rgba8Unorm).unwrap();
let size = 64;
let quad = Quad::from_rect(Rect::from_size(size as f64, size as f64));
let pixels = renderer
.render_to_rgba(&gpu, &quadrants(), size, size, quad, 1.0)
.unwrap();
assert_eq!(pixel(&pixels, size, 4, 4), [255, 0, 0, 255], "top-left");
assert_eq!(pixel(&pixels, size, 59, 4), [0, 255, 0, 255], "top-right");
assert_eq!(pixel(&pixels, size, 4, 59), [0, 0, 255, 255], "bottom-left");
assert_eq!(
pixel(&pixels, size, 59, 59),
[255, 255, 0, 255],
"bottom-right"
);
}
#[test]
fn outside_the_quad_is_left_transparent() {
let Some(gpu) = gpu() else { return };
let mut renderer = Renderer::new(&gpu, wgpu::TextureFormat::Rgba8Unorm).unwrap();
let size = 64;
let quad = Quad::from_rect(Rect::new(0.0, 0.0, 32.0, 64.0));
let pixels = renderer
.render_to_rgba(&gpu, &quadrants(), size, size, quad, 1.0)
.unwrap();
assert_eq!(pixel(&pixels, size, 8, 32)[3], 255, "inside the quad");
assert_eq!(pixel(&pixels, size, 48, 32)[3], 0, "outside the quad");
}
#[test]
fn a_keystone_is_narrower_at_the_top_than_at_the_bottom() {
let Some(gpu) = gpu() else { return };
let mut renderer = Renderer::new(&gpu, wgpu::TextureFormat::Rgba8Unorm).unwrap();
let size = 128;
let quad = Quad::keystone(Rect::from_size(size as f64, size as f64), 32.0);
let pixels = renderer
.render_to_rgba(&gpu, &quadrants(), size, size, quad, 1.0)
.unwrap();
let opaque_in_row = |row: u32| {
(0..size)
.filter(|column| pixel(&pixels, size, *column, row)[3] > 128)
.count()
};
let top = opaque_in_row(2);
let bottom = opaque_in_row(size - 3);
assert!(top > 0 && bottom > 0, "nothing was drawn at all");
assert!(
bottom > top + 40,
"the keystone is not keystoned: {top} px at the top, {bottom} at the bottom"
);
}
#[test]
fn the_centre_of_the_picture_lands_where_the_homography_says() {
let Some(gpu) = gpu() else { return };
let mut renderer = Renderer::new(&gpu, wgpu::TextureFormat::Rgba8Unorm).unwrap();
let size = 128;
let quad = Quad::keystone(Rect::from_size(size as f64, size as f64), 40.0);
let frame = Frame::packed(
2,
1,
PixelFormat::Rgba8,
ColorSpace::srgb(),
Duration::ZERO,
vec![255, 0, 0, 255, 0, 255, 0, 255],
)
.unwrap();
let pixels = renderer
.render_to_rgba(&gpu, &frame, size, size, quad, 1.0)
.unwrap();
let map = quad.homography().unwrap();
for row in [20_u32, 64, 110] {
let expected = map
.transform(Point::new(0.5, f64::from(row) / f64::from(size)))
.x;
let boundary = (0..size)
.find(|column| {
let texel = pixel(&pixels, size, *column, row);
texel[3] > 128 && texel[1] > texel[0]
})
.map(f64::from);
let Some(boundary) = boundary else {
panic!("row {row} has no green half");
};
assert!(
(boundary - expected).abs() < 3.0,
"row {row}: the picture's midline is at {boundary:.1}, \
the homography says {expected:.1}"
);
}
}
#[test]
fn opacity_reaches_the_alpha_channel() {
let Some(gpu) = gpu() else { return };
let mut renderer = Renderer::new(&gpu, wgpu::TextureFormat::Rgba8Unorm).unwrap();
let size = 32;
let quad = Quad::from_rect(Rect::from_size(size as f64, size as f64));
let pixels = renderer
.render_to_rgba(&gpu, &quadrants(), size, size, quad, 0.5)
.unwrap();
let alpha = pixel(&pixels, size, 16, 16)[3];
assert!((120..=136).contains(&alpha), "alpha came back {alpha}");
}
#[test]
fn a_planar_yuv_frame_is_converted_by_the_shader() {
let Some(gpu) = gpu() else { return };
let mut renderer = Renderer::new(&gpu, wgpu::TextureFormat::Rgba8Unorm).unwrap();
let mut data = vec![235_u8; 4];
data.extend_from_slice(&[128; 1]);
data.extend_from_slice(&[128; 1]);
let frame = Frame::packed(
2,
2,
PixelFormat::I420,
ColorSpace::default(),
Duration::ZERO,
data,
)
.unwrap();
let size = 16;
let quad = Quad::from_rect(Rect::from_size(size as f64, size as f64));
let pixels = renderer
.render_to_rgba(&gpu, &frame, size, size, quad, 1.0)
.unwrap();
let [red, green, blue, alpha] = pixel(&pixels, size, 8, 8);
assert!(
red > 250 && green > 250 && blue > 250,
"{red} {green} {blue}"
);
assert_eq!(alpha, 255);
}
#[test]
fn a_quad_that_folds_over_is_refused_rather_than_drawn() {
let Some(gpu) = gpu() else { return };
let mut renderer = Renderer::new(&gpu, wgpu::TextureFormat::Rgba8Unorm).unwrap();
let bowtie = Quad::new([
Point::new(0.0, 0.0),
Point::new(32.0, 0.0),
Point::new(0.0, 32.0),
Point::new(32.0, 32.0),
]);
assert!(matches!(
renderer.render_to_rgba(&gpu, &quadrants(), 32, 32, bowtie, 1.0),
Err(Error::Placement { .. })
));
}
#[test]
fn drawing_before_uploading_says_so_rather_than_showing_black() {
let Some(gpu) = gpu() else { return };
let renderer = Renderer::new(&gpu, wgpu::TextureFormat::Rgba8Unorm).unwrap();
let target = gpu.device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 16,
height: 16,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = target.create_view(&wgpu::TextureViewDescriptor::default());
let quad = Quad::from_rect(Rect::from_size(16.0, 16.0));
assert!(matches!(
renderer.draw(&gpu, &view, 16, 16, quad, 1.0),
Err(Error::Render { .. })
));
}
#[test]
fn ten_bit_says_what_is_missing_rather_than_drawing_noise() {
assert!(matches!(
mode_for(PixelFormat::P010),
Err(Error::Unsupported { .. })
));
}
#[test]
fn textures_are_reused_when_the_next_frame_is_the_same_shape() {
let Some(gpu) = gpu() else { return };
let mut renderer = Renderer::new(&gpu, wgpu::TextureFormat::Rgba8Unorm).unwrap();
renderer.upload(&gpu, &quadrants()).unwrap();
let first = std::ptr::addr_of!(renderer.current.planes.as_ref().unwrap().textures[0]) as usize;
let first_width = renderer.current.planes.as_ref().unwrap().width;
renderer.upload(&gpu, &quadrants()).unwrap();
let second = std::ptr::addr_of!(renderer.current.planes.as_ref().unwrap().textures[0]) as usize;
assert_eq!(
first, second,
"a same-shaped frame reallocated its textures"
);
assert_eq!(first_width, 2);
let bigger = Frame::packed(
4,
4,
PixelFormat::Rgba8,
ColorSpace::srgb(),
Duration::ZERO,
vec![255; 4 * 4 * 4],
)
.unwrap();
renderer.upload(&gpu, &bigger).unwrap();
assert_eq!(renderer.current.planes.as_ref().unwrap().width, 4);
}
fn solid(rgba: [u8; 4]) -> Frame {
Frame::packed(
2,
2,
PixelFormat::Rgba8,
ColorSpace::srgb(),
Duration::ZERO,
rgba.repeat(4),
)
.unwrap()
}
#[test]
fn layers_stack_in_one_pass_over_the_background() {
let Some(gpu) = gpu() else { return };
let renderer = Renderer::new(&gpu, wgpu::TextureFormat::Rgba8Unorm).unwrap();
let mut red = Picture::new();
let mut blue = Picture::new();
renderer
.upload_picture(&gpu, &mut red, &solid([255, 0, 0, 255]))
.unwrap();
renderer
.upload_picture(&gpu, &mut blue, &solid([0, 0, 255, 255]))
.unwrap();
let size = 64;
let green = wgpu::Color {
r: 0.0,
g: 1.0,
b: 0.0,
a: 1.0,
};
let pixels = renderer
.render_layers_to_rgba(
&gpu,
size,
size,
green,
&[
Layer {
picture: &red,
quad: Quad::from_rect(Rect::new(0.0, 0.0, 40.0, 40.0)),
opacity: 1.0,
},
Layer {
picture: &blue,
quad: Quad::from_rect(Rect::new(24.0, 24.0, 40.0, 40.0)),
opacity: 1.0,
},
],
)
.unwrap();
assert_eq!(pixel(&pixels, size, 8, 8), [255, 0, 0, 255], "red alone");
assert_eq!(pixel(&pixels, size, 32, 32), [0, 0, 255, 255], "blue over red");
assert_eq!(pixel(&pixels, size, 56, 56), [0, 0, 255, 255], "blue alone");
assert_eq!(pixel(&pixels, size, 56, 8), [0, 255, 0, 255], "background");
}
#[test]
fn a_translucent_layer_over_nothing_is_premultiplied() {
let Some(gpu) = gpu() else { return };
let renderer = Renderer::new(&gpu, wgpu::TextureFormat::Rgba8Unorm).unwrap();
let mut white = Picture::new();
renderer
.upload_picture(&gpu, &mut white, &solid([255, 255, 255, 255]))
.unwrap();
let size = 16;
let pixels = renderer
.render_layers_to_rgba(
&gpu,
size,
size,
wgpu::Color::TRANSPARENT,
&[Layer {
picture: &white,
quad: Quad::from_rect(Rect::from_size(16.0, 16.0)),
opacity: 0.5,
}],
)
.unwrap();
let [red, green, blue, alpha] = pixel(&pixels, size, 8, 8);
for channel in [red, green, blue, alpha] {
assert!((120..=136).contains(&channel), "{red} {green} {blue} {alpha}");
}
}
#[test]
fn one_picture_cannot_be_two_layers_in_one_pass() {
let Some(gpu) = gpu() else { return };
let renderer = Renderer::new(&gpu, wgpu::TextureFormat::Rgba8Unorm).unwrap();
let mut red = Picture::new();
renderer
.upload_picture(&gpu, &mut red, &solid([255, 0, 0, 255]))
.unwrap();
let layer = Layer {
picture: &red,
quad: Quad::from_rect(Rect::from_size(8.0, 8.0)),
opacity: 1.0,
};
assert!(matches!(
renderer.render_layers_to_rgba(&gpu, 8, 8, wgpu::Color::BLACK, &[layer, layer]),
Err(Error::Render { .. })
));
}
#[test]
fn an_empty_picture_is_refused_rather_than_drawn_black() {
let Some(gpu) = gpu() else { return };
let renderer = Renderer::new(&gpu, wgpu::TextureFormat::Rgba8Unorm).unwrap();
let empty = Picture::new();
assert!(matches!(
renderer.render_layers_to_rgba(
&gpu,
8,
8,
wgpu::Color::BLACK,
&[Layer {
picture: &empty,
quad: Quad::from_rect(Rect::from_size(8.0, 8.0)),
opacity: 1.0,
}]
),
Err(Error::Render { .. })
));
}
}