| 1 | //! Renders small scenes offscreen and checks pixels. Runs on whatever Vulkan |
| 2 | //! driver is available. `nix flake check` forces lavapipe. |
| 3 | |
| 4 | use monolith_render_vk::{Clip, VulkanImage, VulkanRenderer, VulkanTexture}; |
| 5 | use smithay::backend::allocator::Fourcc; |
| 6 | use smithay::backend::renderer::{ |
| 7 | Bind, Color32F, ExportMem, Frame, ImportMem, Offscreen, Renderer, |
| 8 | }; |
| 9 | use smithay::utils::{Physical, Rectangle, Transform}; |
| 10 | |
| 11 | const W: i32 = 64; |
| 12 | const H: i32 = 64; |
| 13 | |
| 14 | type Pixel = [u8; 4]; |
| 15 | |
| 16 | const RED: Pixel = [255, 0, 0, 255]; |
| 17 | const GREEN: Pixel = [0, 255, 0, 255]; |
| 18 | const WHITE: Pixel = [255, 255, 255, 255]; |
| 19 | const BLACK: Pixel = [0, 0, 0, 255]; |
| 20 | |
| 21 | struct Scene { |
| 22 | renderer: VulkanRenderer, |
| 23 | image: VulkanImage, |
| 24 | } |
| 25 | |
| 26 | impl Scene { |
| 27 | fn new() -> Self { |
| 28 | let mut renderer = VulkanRenderer::new().expect("no usable Vulkan device"); |
| 29 | let image = renderer |
| 30 | .create_buffer(Fourcc::Argb8888, (W, H).into()) |
| 31 | .unwrap(); |
| 32 | Self { renderer, image } |
| 33 | } |
| 34 | |
| 35 | fn draw( |
| 36 | &mut self, |
| 37 | transform: Transform, |
| 38 | f: impl FnOnce(&mut monolith_render_vk::VulkanFrame<'_, '_>), |
| 39 | ) { |
| 40 | let mut target = self.renderer.bind(&mut self.image).unwrap(); |
| 41 | let mut frame = self |
| 42 | .renderer |
| 43 | .render(&mut target, (W, H).into(), transform) |
| 44 | .unwrap(); |
| 45 | f(&mut frame); |
| 46 | frame.finish().unwrap().wait().unwrap(); |
| 47 | } |
| 48 | |
| 49 | /// Pixels as RGBA, whatever the image's byte order. |
| 50 | fn pixels(&mut self) -> Vec<Pixel> { |
| 51 | let target = self.renderer.bind(&mut self.image).unwrap(); |
| 52 | let mapping = self |
| 53 | .renderer |
| 54 | .copy_framebuffer( |
| 55 | &target, |
| 56 | Rectangle::from_size((W, H).into()), |
| 57 | Fourcc::Abgr8888, |
| 58 | ) |
| 59 | .unwrap(); |
| 60 | self.renderer |
| 61 | .map_texture(&mapping) |
| 62 | .unwrap() |
| 63 | .as_chunks::<4>() |
| 64 | .0 |
| 65 | .to_vec() |
| 66 | } |
| 67 | |
| 68 | fn texture(&mut self, size: (i32, i32), rgba: &[Pixel]) -> VulkanTexture { |
| 69 | let bytes: Vec<u8> = rgba.iter().flatten().copied().collect(); |
| 70 | self.renderer |
| 71 | .import_memory(&bytes, Fourcc::Abgr8888, size.into(), false) |
| 72 | .unwrap() |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | fn at(pixels: &[Pixel], x: i32, y: i32) -> Pixel { |
| 77 | pixels[(y * W + x) as usize] |
| 78 | } |
| 79 | |
| 80 | fn full() -> Rectangle<i32, Physical> { |
| 81 | Rectangle::from_size((W, H).into()) |
| 82 | } |
| 83 | |
| 84 | fn rgba(p: Pixel) -> Color32F { |
| 85 | p.map(|c| f32::from(c) / 255.0).into() |
| 86 | } |
| 87 | |
| 88 | #[test] |
| 89 | fn partial_clear_keeps_previous_contents() { |
| 90 | let mut s = Scene::new(); |
| 91 | s.draw(Transform::Normal, |f| { |
| 92 | f.clear(rgba(RED), &[full()]).unwrap() |
| 93 | }); |
| 94 | s.draw(Transform::Normal, |f| { |
| 95 | f.clear( |
| 96 | rgba(GREEN), |
| 97 | &[Rectangle::new((0, 0).into(), (32, 64).into())], |
| 98 | ) |
| 99 | .unwrap() |
| 100 | }); |
| 101 | let px = s.pixels(); |
| 102 | assert_eq!(at(&px, 0, 0), GREEN); |
| 103 | assert_eq!(at(&px, 63, 0), RED); |
| 104 | } |
| 105 | |
| 106 | #[test] |
| 107 | fn alpha_blends_premultiplied() { |
| 108 | let mut s = Scene::new(); |
| 109 | let tex = s.texture((1, 1), &[WHITE]); |
| 110 | s.draw(Transform::Normal, |f| { |
| 111 | f.clear(rgba(BLACK), &[full()]).unwrap(); |
| 112 | f.render_texture_from_to( |
| 113 | &tex, |
| 114 | Rectangle::from_size((1.0, 1.0).into()), |
| 115 | full(), |
| 116 | &[full()], |
| 117 | &[], |
| 118 | Transform::Normal, |
| 119 | 0.5, |
| 120 | ) |
| 121 | .unwrap(); |
| 122 | }); |
| 123 | let p = at(&s.pixels(), 32, 32); |
| 124 | assert!( |
| 125 | (127..=129).contains(&p[0]) && p[0] == p[1] && p[1] == p[2], |
| 126 | "{p:?}" |
| 127 | ); |
| 128 | } |
| 129 | |
| 130 | #[test] |
| 131 | fn output_transform_moves_content() { |
| 132 | let mut s = Scene::new(); |
| 133 | s.draw(Transform::_180, |f| { |
| 134 | f.clear(rgba(BLACK), &[full()]).unwrap(); |
| 135 | let dst = Rectangle::new((0, 0).into(), (16, 16).into()); |
| 136 | f.draw_solid(dst, &[Rectangle::from_size(dst.size)], rgba(GREEN)) |
| 137 | .unwrap(); |
| 138 | }); |
| 139 | let px = s.pixels(); |
| 140 | assert_eq!(at(&px, 0, 0), BLACK); |
| 141 | assert_eq!(at(&px, 63, 63), GREEN); |
| 142 | } |
| 143 | |
| 144 | #[test] |
| 145 | fn solid_fill_in_the_corner_keeps_its_colour() { |
| 146 | let mut s = Scene::new(); |
| 147 | s.draw(Transform::Normal, |f| { |
| 148 | f.clear(rgba(BLACK), &[full()]).unwrap(); |
| 149 | let dst = Rectangle::new((48, 48).into(), (16, 16).into()); |
| 150 | f.draw_solid(dst, &[Rectangle::from_size(dst.size)], rgba(RED)) |
| 151 | .unwrap(); |
| 152 | }); |
| 153 | assert_eq!(at(&s.pixels(), 56, 56), RED); |
| 154 | } |
| 155 | |
| 156 | /// The radius follows its corner through an output rotation. |
| 157 | #[test] |
| 158 | fn clip_radius_stays_on_its_corner_when_rotated() { |
| 159 | let corners = [(0, 0), (W - 1, 0), (0, H - 1), (W - 1, H - 1)]; |
| 160 | let mut s = Scene::new(); |
| 161 | s.draw(Transform::_90, |f| { |
| 162 | f.clear(rgba(BLACK), &[full()]).unwrap(); |
| 163 | let dst = Rectangle::new((0, 0).into(), (4, 4).into()); |
| 164 | f.draw_solid(dst, &[Rectangle::from_size(dst.size)], rgba(GREEN)) |
| 165 | .unwrap(); |
| 166 | }); |
| 167 | let px = s.pixels(); |
| 168 | let top_left = corners |
| 169 | .into_iter() |
| 170 | .find(|&(x, y)| at(&px, x, y) == GREEN) |
| 171 | .unwrap(); |
| 172 | s.draw(Transform::_90, |f| { |
| 173 | f.clear(rgba(BLACK), &[full()]).unwrap(); |
| 174 | f.set_clip(Some(Clip { |
| 175 | rect: full(), |
| 176 | radii: [20.0, 0.0, 0.0, 0.0], |
| 177 | ring: 0.0, |
| 178 | })); |
| 179 | f.draw_solid(full(), &[full()], rgba(GREEN)).unwrap(); |
| 180 | }); |
| 181 | let px = s.pixels(); |
| 182 | for (x, y) in corners { |
| 183 | let expected = if (x, y) == top_left { BLACK } else { GREEN }; |
| 184 | assert_eq!(at(&px, x, y), expected, "corner {x},{y}"); |
| 185 | } |
| 186 | assert_eq!(at(&px, W / 2, H / 2), GREEN); |
| 187 | } |
| 188 | |
| 189 | /// Draws arrive in the rotated space, which for a quarter turn on a wide |
| 190 | /// output is tall, so its far corner has to land inside the framebuffer. |
| 191 | #[test] |
| 192 | fn quarter_turn_on_a_wide_output_fills_it() { |
| 193 | let (fw, fh) = (64, 32); |
| 194 | let mut renderer = VulkanRenderer::new().expect("no usable Vulkan device"); |
| 195 | let mut image = renderer |
| 196 | .create_buffer(Fourcc::Argb8888, (fw, fh).into()) |
| 197 | .unwrap(); |
| 198 | for transform in [Transform::_90, Transform::_270, Transform::Flipped90] { |
| 199 | let mut target = renderer.bind(&mut image).unwrap(); |
| 200 | let mut frame = renderer |
| 201 | .render(&mut target, (fw, fh).into(), transform) |
| 202 | .unwrap(); |
| 203 | let area = Rectangle::from_size((fh, fw).into()); |
| 204 | frame.clear(rgba(BLACK), &[area]).unwrap(); |
| 205 | let near = Rectangle::new((0, 0).into(), (8, 8).into()); |
| 206 | let far = Rectangle::new((fh - 8, fw - 8).into(), (8, 8).into()); |
| 207 | frame |
| 208 | .draw_solid(near, &[Rectangle::from_size(near.size)], rgba(GREEN)) |
| 209 | .unwrap(); |
| 210 | frame |
| 211 | .draw_solid(far, &[Rectangle::from_size(far.size)], rgba(RED)) |
| 212 | .unwrap(); |
| 213 | frame.finish().unwrap().wait().unwrap(); |
| 214 | drop(target); |
| 215 | let target = renderer.bind(&mut image).unwrap(); |
| 216 | let mapping = renderer |
| 217 | .copy_framebuffer( |
| 218 | &target, |
| 219 | Rectangle::from_size((fw, fh).into()), |
| 220 | Fourcc::Abgr8888, |
| 221 | ) |
| 222 | .unwrap(); |
| 223 | let px = renderer |
| 224 | .map_texture(&mapping) |
| 225 | .unwrap() |
| 226 | .as_chunks::<4>() |
| 227 | .0 |
| 228 | .to_vec(); |
| 229 | let at = |x: i32, y: i32| px[(y * fw + x) as usize]; |
| 230 | let corners = [(0, 0), (fw - 1, 0), (0, fh - 1), (fw - 1, fh - 1)]; |
| 231 | let green: Vec<_> = corners |
| 232 | .iter() |
| 233 | .filter(|&&(x, y)| at(x, y) == GREEN) |
| 234 | .collect(); |
| 235 | let red: Vec<_> = corners.iter().filter(|&&(x, y)| at(x, y) == RED).collect(); |
| 236 | assert_eq!((green.len(), red.len()), (1, 1), "{transform:?}"); |
| 237 | let (g, r) = (green[0], red[0]); |
| 238 | assert!(g.0 != r.0 && g.1 != r.1, "{transform:?} not diagonal"); |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | #[test] |
| 243 | fn queued_uploads_to_one_texture_land_in_order() { |
| 244 | let mut s = Scene::new(); |
| 245 | let size = (2, 2); |
| 246 | let bytes = |p: Pixel| -> Vec<u8> { [p; 4].iter().flatten().copied().collect() }; |
| 247 | let tex = s.texture(size, &[RED; 4]); |
| 248 | for colour in [WHITE, GREEN] { |
| 249 | s.renderer |
| 250 | .update_memory(&tex, &bytes(colour), Rectangle::from_size(size.into())) |
| 251 | .unwrap(); |
| 252 | } |
| 253 | let read = s |
| 254 | .renderer |
| 255 | .copy_texture(&tex, Rectangle::from_size(size.into()), Fourcc::Abgr8888) |
| 256 | .unwrap(); |
| 257 | assert_eq!(s.renderer.map_texture(&read).unwrap()[..4], GREEN); |
| 258 | s.renderer |
| 259 | .update_memory(&tex, &bytes(WHITE), Rectangle::from_size(size.into())) |
| 260 | .unwrap(); |
| 261 | s.renderer |
| 262 | .update_memory(&tex, &bytes(RED), Rectangle::from_size(size.into())) |
| 263 | .unwrap(); |
| 264 | s.draw(Transform::Normal, |f| { |
| 265 | f.render_texture_from_to( |
| 266 | &tex, |
| 267 | Rectangle::from_size((2.0, 2.0).into()), |
| 268 | full(), |
| 269 | &[full()], |
| 270 | &[], |
| 271 | Transform::Normal, |
| 272 | 1.0, |
| 273 | ) |
| 274 | .unwrap(); |
| 275 | }); |
| 276 | assert_eq!(at(&s.pixels(), 32, 32), RED); |
| 277 | } |