atagen

monolith

public
Star0
Watch0

Commit 0b68ce424a32

tarball

atagen <boss@atagen.co>2026-10-18 03:18 UTC

2 files changed+61-4
render-vk: map draws through the rotated output size

Draws arrive in the output space after the transform, but VulkanFrame mapped them into the framebuffer against the unrotated size. For 90 and 270 degree transforms on a non-square output that put content at a large offset or skewed it, while 180 hid the bug by keeping the size. Smithay's pixman and GLES renderers both swap the size first.
Modified file: crates/render-vk/src/frame.rs+8 additions−4 deletions
@@ -27,6 +27,9 @@ pub struct VulkanFrame<'frame, 'buffer> {
2727 renderer: &'frame mut VulkanRenderer,
2828 target: &'frame mut VulkanTarget<'buffer>,
2929 output: Size<i32, Physical>,
30+ /// The output rotated into the space draws are given in, which differs
31+ /// from `output` for 90 and 270 degree transforms.
32+ area: Size<i32, Physical>,
3033 transform: Transform,
3134 ops: Vec<Op>,
3235 clip: Option<Clip>,
@@ -63,6 +66,7 @@ impl<'frame, 'buffer> VulkanFrame<'frame, 'buffer> {
6366 renderer,
6467 target,
6568 output,
69+ area: transform.transform_size(output),
6670 transform,
6771 ops: Vec::new(),
6872 clip: None,
@@ -81,7 +85,7 @@ impl<'frame, 'buffer> VulkanFrame<'frame, 'buffer> {
8185 };
8286 let [tl, tr, br, bl] = c.radii;
8387 let corners = [[tl, 0.0], [tr, 0.0], [bl, 0.0], [br, 0.0]];
84- let (fb, r) = to_framebuffer(c.rect, corners, self.output, self.transform);
88+ let (fb, r) = to_framebuffer(c.rect, corners, self.area, self.transform);
8589 let rect = [
8690 fb.loc.x as f32,
8791 fb.loc.y as f32,
@@ -98,7 +102,7 @@ impl<'frame, 'buffer> VulkanFrame<'frame, 'buffer> {
98102 }
99103
100104 fn fb_rect(&self, r: Rectangle<i32, Physical>) -> Option<vk::Rect2D> {
101- let r = rect_to_framebuffer(r, self.output, self.transform)
105+ let r = rect_to_framebuffer(r, self.area, self.transform)
102106 .intersection(Rectangle::from_size(self.fb_size()))?;
103107 Some(vk::Rect2D {
104108 offset: vk::Offset2D {
@@ -175,7 +179,7 @@ impl<'frame, 'buffer> VulkanFrame<'frame, 'buffer> {
175179 return Ok(());
176180 }
177181 let uvs = texture_uvs(src, dst.size, texture.size(), src_transform);
178- let (fb, uv) = to_framebuffer(dst, uvs, self.output, self.transform);
182+ let (fb, uv) = to_framebuffer(dst, uvs, self.area, self.transform);
179183 let (clip, radii, ring) = self.clip_push();
180184 let push = Push {
181185 dst: self.ndc(fb),
@@ -236,7 +240,7 @@ impl Frame for VulkanFrame<'_, '_> {
236240 if scissors.is_empty() {
237241 return Ok(());
238242 }
239- let (fb, _) = to_framebuffer(dst, [[0.0; 2]; 4], self.output, self.transform);
243+ let (fb, _) = to_framebuffer(dst, [[0.0; 2]; 4], self.area, self.transform);
240244 let (clip, radii, ring) = self.clip_push();
241245 let push = Push {
242246 dst: self.ndc(fb),
Modified file: crates/render-vk/tests/golden.rs+53 additions
@@ -185,3 +185,56 @@ fn clip_radius_stays_on_its_corner_when_rotated() {
185185 }
186186 assert_eq!(at(&px, W / 2, H / 2), GREEN);
187187 }
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+}