| 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::{VulkanImage, VulkanRenderer, VulkanTexture}; |
| 5 | use smithay::backend::allocator::Fourcc; |
| 6 | use smithay::backend::renderer::{ |
| 7 | Bind, Color32F, ExportMem, Frame, ImportMem, Offscreen, Renderer, TextureFilter, |
| 8 | }; |
| 9 | use smithay::utils::{Physical, Rectangle, Size, 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 BLUE: Pixel = [0, 0, 255, 255]; |
| 19 | const WHITE: Pixel = [255, 255, 255, 255]; |
| 20 | const BLACK: Pixel = [0, 0, 0, 255]; |
| 21 | |
| 22 | struct Scene { |
| 23 | renderer: VulkanRenderer, |
| 24 | image: VulkanImage, |
| 25 | } |
| 26 | |
| 27 | impl Scene { |
| 28 | fn new() -> Self { |
| 29 | let mut renderer = VulkanRenderer::new().expect("no usable Vulkan device"); |
| 30 | let image = renderer |
| 31 | .create_buffer(Fourcc::Argb8888, (W, H).into()) |
| 32 | .unwrap(); |
| 33 | Self { renderer, image } |
| 34 | } |
| 35 | |
| 36 | fn draw( |
| 37 | &mut self, |
| 38 | transform: Transform, |
| 39 | f: impl FnOnce(&mut monolith_render_vk::VulkanFrame<'_, '_>), |
| 40 | ) { |
| 41 | let mut target = self.renderer.bind(&mut self.image).unwrap(); |
| 42 | let mut frame = self |
| 43 | .renderer |
| 44 | .render(&mut target, (W, H).into(), transform) |
| 45 | .unwrap(); |
| 46 | f(&mut frame); |
| 47 | frame.finish().unwrap().wait().unwrap(); |
| 48 | } |
| 49 | |
| 50 | /// Pixels as RGBA, whatever the image's byte order. |
| 51 | fn pixels(&mut self) -> Vec<Pixel> { |
| 52 | let target = self.renderer.bind(&mut self.image).unwrap(); |
| 53 | let mapping = self |
| 54 | .renderer |
| 55 | .copy_framebuffer( |
| 56 | &target, |
| 57 | Rectangle::from_size((W, H).into()), |
| 58 | Fourcc::Abgr8888, |
| 59 | ) |
| 60 | .unwrap(); |
| 61 | self.renderer |
| 62 | .map_texture(&mapping) |
| 63 | .unwrap() |
| 64 | .as_chunks::<4>() |
| 65 | .0 |
| 66 | .to_vec() |
| 67 | } |
| 68 | |
| 69 | fn texture(&mut self, size: (i32, i32), rgba: &[Pixel]) -> VulkanTexture { |
| 70 | let bytes: Vec<u8> = rgba.iter().flatten().copied().collect(); |
| 71 | self.renderer |
| 72 | .import_memory(&bytes, Fourcc::Abgr8888, size.into(), false) |
| 73 | .unwrap() |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | fn at(pixels: &[Pixel], x: i32, y: i32) -> Pixel { |
| 78 | pixels[(y * W + x) as usize] |
| 79 | } |
| 80 | |
| 81 | fn full() -> Rectangle<i32, Physical> { |
| 82 | Rectangle::from_size((W, H).into()) |
| 83 | } |
| 84 | |
| 85 | fn rgba(p: Pixel) -> Color32F { |
| 86 | p.map(|c| f32::from(c) / 255.0).into() |
| 87 | } |
| 88 | |
| 89 | #[test] |
| 90 | fn clears_to_a_colour() { |
| 91 | let mut s = Scene::new(); |
| 92 | s.draw(Transform::Normal, |f| { |
| 93 | f.clear(rgba(RED), &[full()]).unwrap() |
| 94 | }); |
| 95 | let px = s.pixels(); |
| 96 | assert!(px.iter().all(|&p| p == RED)); |
| 97 | } |
| 98 | |
| 99 | #[test] |
| 100 | fn solid_fill_respects_its_rectangle() { |
| 101 | let mut s = Scene::new(); |
| 102 | s.draw(Transform::Normal, |f| { |
| 103 | f.clear(rgba(BLACK), &[full()]).unwrap(); |
| 104 | let dst = Rectangle::new((16, 16).into(), (32, 32).into()); |
| 105 | f.draw_solid(dst, &[Rectangle::from_size(dst.size)], rgba(GREEN)) |
| 106 | .unwrap(); |
| 107 | }); |
| 108 | let px = s.pixels(); |
| 109 | assert_eq!(at(&px, 16, 16), GREEN); |
| 110 | assert_eq!(at(&px, 47, 47), GREEN); |
| 111 | assert_eq!(at(&px, 15, 16), BLACK); |
| 112 | assert_eq!(at(&px, 48, 47), BLACK); |
| 113 | } |
| 114 | |
| 115 | #[test] |
| 116 | fn damage_limits_drawing() { |
| 117 | let mut s = Scene::new(); |
| 118 | s.draw(Transform::Normal, |f| { |
| 119 | f.clear(rgba(BLACK), &[full()]).unwrap(); |
| 120 | let damage = Rectangle::new((0, 0).into(), (8, 8).into()); |
| 121 | f.draw_solid(full(), &[damage], rgba(BLUE)).unwrap(); |
| 122 | }); |
| 123 | let px = s.pixels(); |
| 124 | assert_eq!(at(&px, 7, 7), BLUE); |
| 125 | assert_eq!(at(&px, 8, 8), BLACK); |
| 126 | } |
| 127 | |
| 128 | #[test] |
| 129 | fn partial_clear_keeps_previous_contents() { |
| 130 | let mut s = Scene::new(); |
| 131 | s.draw(Transform::Normal, |f| { |
| 132 | f.clear(rgba(RED), &[full()]).unwrap() |
| 133 | }); |
| 134 | s.draw(Transform::Normal, |f| { |
| 135 | f.clear( |
| 136 | rgba(GREEN), |
| 137 | &[Rectangle::new((0, 0).into(), (32, 64).into())], |
| 138 | ) |
| 139 | .unwrap() |
| 140 | }); |
| 141 | let px = s.pixels(); |
| 142 | assert_eq!(at(&px, 0, 0), GREEN); |
| 143 | assert_eq!(at(&px, 63, 0), RED); |
| 144 | } |
| 145 | |
| 146 | #[test] |
| 147 | fn textures_sample_with_nearest_filtering() { |
| 148 | let mut s = Scene::new(); |
| 149 | s.renderer.upscale_filter(TextureFilter::Nearest).unwrap(); |
| 150 | let tex = s.texture((2, 2), &[RED, GREEN, BLUE, WHITE]); |
| 151 | s.draw(Transform::Normal, |f| { |
| 152 | f.render_texture_from_to( |
| 153 | &tex, |
| 154 | Rectangle::from_size((2.0, 2.0).into()), |
| 155 | full(), |
| 156 | &[full()], |
| 157 | &[], |
| 158 | Transform::Normal, |
| 159 | 1.0, |
| 160 | ) |
| 161 | .unwrap(); |
| 162 | }); |
| 163 | let px = s.pixels(); |
| 164 | assert_eq!(at(&px, 8, 8), RED); |
| 165 | assert_eq!(at(&px, 56, 8), GREEN); |
| 166 | assert_eq!(at(&px, 8, 56), BLUE); |
| 167 | assert_eq!(at(&px, 56, 56), WHITE); |
| 168 | } |
| 169 | |
| 170 | #[test] |
| 171 | fn alpha_blends_premultiplied() { |
| 172 | let mut s = Scene::new(); |
| 173 | let tex = s.texture((1, 1), &[WHITE]); |
| 174 | s.draw(Transform::Normal, |f| { |
| 175 | f.clear(rgba(BLACK), &[full()]).unwrap(); |
| 176 | f.render_texture_from_to( |
| 177 | &tex, |
| 178 | Rectangle::from_size((1.0, 1.0).into()), |
| 179 | full(), |
| 180 | &[full()], |
| 181 | &[], |
| 182 | Transform::Normal, |
| 183 | 0.5, |
| 184 | ) |
| 185 | .unwrap(); |
| 186 | }); |
| 187 | let p = at(&s.pixels(), 32, 32); |
| 188 | assert!( |
| 189 | (127..=129).contains(&p[0]) && p[0] == p[1] && p[1] == p[2], |
| 190 | "{p:?}" |
| 191 | ); |
| 192 | } |
| 193 | |
| 194 | #[test] |
| 195 | fn output_transform_moves_content() { |
| 196 | let mut s = Scene::new(); |
| 197 | s.draw(Transform::_180, |f| { |
| 198 | f.clear(rgba(BLACK), &[full()]).unwrap(); |
| 199 | let dst = Rectangle::new((0, 0).into(), (16, 16).into()); |
| 200 | f.draw_solid(dst, &[Rectangle::from_size(dst.size)], rgba(GREEN)) |
| 201 | .unwrap(); |
| 202 | }); |
| 203 | let px = s.pixels(); |
| 204 | assert_eq!(at(&px, 0, 0), BLACK); |
| 205 | assert_eq!(at(&px, 63, 63), GREEN); |
| 206 | } |
| 207 | |
| 208 | #[test] |
| 209 | fn offscreen_images_can_be_sampled() { |
| 210 | let mut s = Scene::new(); |
| 211 | let mut inner = s |
| 212 | .renderer |
| 213 | .create_buffer(Fourcc::Argb8888, Size::from((4, 4))) |
| 214 | .unwrap(); |
| 215 | { |
| 216 | let mut t = s.renderer.bind(&mut inner).unwrap(); |
| 217 | let mut f = s |
| 218 | .renderer |
| 219 | .render(&mut t, (4, 4).into(), Transform::Normal) |
| 220 | .unwrap(); |
| 221 | f.clear(rgba(BLUE), &[Rectangle::from_size((4, 4).into())]) |
| 222 | .unwrap(); |
| 223 | f.finish().unwrap().wait().unwrap(); |
| 224 | } |
| 225 | let tex = inner.texture(); |
| 226 | s.draw(Transform::Normal, |f| { |
| 227 | f.render_texture_from_to( |
| 228 | &tex, |
| 229 | Rectangle::from_size((4.0, 4.0).into()), |
| 230 | full(), |
| 231 | &[full()], |
| 232 | &[], |
| 233 | Transform::Normal, |
| 234 | 1.0, |
| 235 | ) |
| 236 | .unwrap(); |
| 237 | }); |
| 238 | assert_eq!(at(&s.pixels(), 32, 32), BLUE); |
| 239 | } |
| 240 | |
| 241 | #[test] |
| 242 | fn reports_the_device() { |
| 243 | let r = VulkanRenderer::new().unwrap(); |
| 244 | eprintln!("rendering on {}", r.device_name()); |
| 245 | assert!(!r.device_name().is_empty()); |
| 246 | } |
| 247 | |
| 248 | #[test] |
| 249 | fn solid_fill_in_the_corner_keeps_its_colour() { |
| 250 | let mut s = Scene::new(); |
| 251 | s.draw(Transform::Normal, |f| { |
| 252 | f.clear(rgba(BLACK), &[full()]).unwrap(); |
| 253 | let dst = Rectangle::new((48, 48).into(), (16, 16).into()); |
| 254 | f.draw_solid(dst, &[Rectangle::from_size(dst.size)], rgba(RED)) |
| 255 | .unwrap(); |
| 256 | }); |
| 257 | assert_eq!(at(&s.pixels(), 56, 56), RED); |
| 258 | } |