824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 1 | //! Renders small scenes offscreen and checks pixels. Runs on whatever Vulkan |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 2 | //! driver is available. `nix flake check` forces lavapipe. |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 3 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 4 | use monolith_render_vk::{VulkanImage, VulkanRenderer, VulkanTexture}; |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 5 | use smithay::backend::allocator::Fourcc; |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 6 | use smithay::backend::renderer::{ |
5a760f013a02 | atagen | 2026-10-16 09:55 UTC | 7 | Bind, Color32F, ExportMem, Frame, ImportMem, Offscreen, Renderer, |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 8 | }; |
5a760f013a02 | atagen | 2026-10-16 09:55 UTC | 9 | use smithay::utils::{Physical, Rectangle, Transform}; |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 10 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 11 | const W: i32 = 64; |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 12 | const H: i32 = 64; |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 13 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 14 | type Pixel = [u8; 4]; |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 15 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 16 | const RED: Pixel = [255, 0, 0, 255]; |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 17 | const GREEN: Pixel = [0, 255, 0, 255]; |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 18 | const WHITE: Pixel = [255, 255, 255, 255]; |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 19 | const BLACK: Pixel = [0, 0, 0, 255]; |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 20 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 21 | struct Scene { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 22 | renderer: VulkanRenderer, |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 23 | image: VulkanImage, |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 24 | } |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 25 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 26 | impl Scene { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 27 | fn new() -> Self { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 28 | let mut renderer = VulkanRenderer::new().expect("no usable Vulkan device"); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 29 | let image = renderer |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 30 | .create_buffer(Fourcc::Argb8888, (W, H).into()) |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 31 | .unwrap(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 32 | Self { renderer, image } |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 33 | } |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 34 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 35 | fn draw( |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 36 | &mut self, |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 37 | transform: Transform, |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 38 | f: impl FnOnce(&mut monolith_render_vk::VulkanFrame<'_, '_>), |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 39 | ) { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 40 | let mut target = self.renderer.bind(&mut self.image).unwrap(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 41 | let mut frame = self |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 42 | .renderer |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 43 | .render(&mut target, (W, H).into(), transform) |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 44 | .unwrap(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 45 | f(&mut frame); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 46 | frame.finish().unwrap().wait().unwrap(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 47 | } |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 48 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 49 | /// Pixels as RGBA, whatever the image's byte order. |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 50 | fn pixels(&mut self) -> Vec<Pixel> { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 51 | let target = self.renderer.bind(&mut self.image).unwrap(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 52 | let mapping = self |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 53 | .renderer |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 54 | .copy_framebuffer( |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 55 | &target, |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 56 | Rectangle::from_size((W, H).into()), |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 57 | Fourcc::Abgr8888, |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 58 | ) |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 59 | .unwrap(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 60 | self.renderer |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 61 | .map_texture(&mapping) |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 62 | .unwrap() |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 63 | .as_chunks::<4>() |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 64 | .0 |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 65 | .to_vec() |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 66 | } |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 67 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 68 | fn texture(&mut self, size: (i32, i32), rgba: &[Pixel]) -> VulkanTexture { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 69 | let bytes: Vec<u8> = rgba.iter().flatten().copied().collect(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 70 | self.renderer |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 71 | .import_memory(&bytes, Fourcc::Abgr8888, size.into(), false) |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 72 | .unwrap() |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 73 | } |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 74 | } |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 75 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 76 | fn at(pixels: &[Pixel], x: i32, y: i32) -> Pixel { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 77 | pixels[(y * W + x) as usize] |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 78 | } |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 79 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 80 | fn full() -> Rectangle<i32, Physical> { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 81 | Rectangle::from_size((W, H).into()) |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 82 | } |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 83 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 84 | fn rgba(p: Pixel) -> Color32F { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 85 | p.map(|c| f32::from(c) / 255.0).into() |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 86 | } |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 87 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 88 | #[test] |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 89 | fn partial_clear_keeps_previous_contents() { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 90 | let mut s = Scene::new(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 91 | s.draw(Transform::Normal, |f| { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 92 | f.clear(rgba(RED), &[full()]).unwrap() |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 93 | }); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 94 | s.draw(Transform::Normal, |f| { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 95 | f.clear( |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 96 | rgba(GREEN), |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 97 | &[Rectangle::new((0, 0).into(), (32, 64).into())], |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 98 | ) |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 99 | .unwrap() |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 100 | }); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 101 | let px = s.pixels(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 102 | assert_eq!(at(&px, 0, 0), GREEN); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 103 | assert_eq!(at(&px, 63, 0), RED); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 104 | } |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 105 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 106 | #[test] |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 107 | fn alpha_blends_premultiplied() { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 108 | let mut s = Scene::new(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 109 | let tex = s.texture((1, 1), &[WHITE]); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 110 | s.draw(Transform::Normal, |f| { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 111 | f.clear(rgba(BLACK), &[full()]).unwrap(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 112 | f.render_texture_from_to( |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 113 | &tex, |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 114 | Rectangle::from_size((1.0, 1.0).into()), |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 115 | full(), |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 116 | &[full()], |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 117 | &[], |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 118 | Transform::Normal, |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 119 | 0.5, |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 120 | ) |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 121 | .unwrap(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 122 | }); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 123 | let p = at(&s.pixels(), 32, 32); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 124 | assert!( |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 125 | (127..=129).contains(&p[0]) && p[0] == p[1] && p[1] == p[2], |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 126 | "{p:?}" |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 127 | ); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 128 | } |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 129 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 130 | #[test] |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 131 | fn output_transform_moves_content() { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 132 | let mut s = Scene::new(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 133 | s.draw(Transform::_180, |f| { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 134 | f.clear(rgba(BLACK), &[full()]).unwrap(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 135 | let dst = Rectangle::new((0, 0).into(), (16, 16).into()); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 136 | f.draw_solid(dst, &[Rectangle::from_size(dst.size)], rgba(GREEN)) |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 137 | .unwrap(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 138 | }); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 139 | let px = s.pixels(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 140 | assert_eq!(at(&px, 0, 0), BLACK); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 141 | assert_eq!(at(&px, 63, 63), GREEN); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 142 | } |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 143 | |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 144 | #[test] |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 145 | fn solid_fill_in_the_corner_keeps_its_colour() { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 146 | let mut s = Scene::new(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 147 | s.draw(Transform::Normal, |f| { |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 148 | f.clear(rgba(BLACK), &[full()]).unwrap(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 149 | let dst = Rectangle::new((48, 48).into(), (16, 16).into()); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 150 | f.draw_solid(dst, &[Rectangle::from_size(dst.size)], rgba(RED)) |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 151 | .unwrap(); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 152 | }); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 153 | assert_eq!(at(&s.pixels(), 56, 56), RED); |
824bf02b6e55 | atagen | 2026-10-16 09:15 UTC | 154 | } |