diff --git a/third_party/README.md b/third_party/README.md index ba4339b..77da132 100644 --- a/third_party/README.md +++ b/third_party/README.md @@ -49,4 +49,22 @@ Unnecessary once wgpu exposes pre-rotation itself (#3345). ## i-slint-renderer-skia 1.17.1 — rotate the canvas to match -*(patch lands in a later commit)* +`wgpu_29_surface.rs` keeps the promise the wgpu-hal patch lets it make. +On Android it reads the surface's transform whenever it configures, sizes +the swapchain in the panel's orientation (width and height swapped for a +quarter turn), calls `set_pre_transform`, and concatenates the matching +rotation onto the Skia canvas before Slint draws — so the whole UI, +including an imported `wgpu::Texture`, is drawn pre-rotated. It checks the +transform before every frame too, because a half turn (landscape to +reverse landscape) changes it without resizing the window. Touch input is +untouched: only drawing is rotated. + +`itemrenderer.rs` widens the pixel-alignment check from "pure translation" +to "any right-angle rotation or flip without scaling". Without that, a +rotated canvas silently loses pixel snapping everywhere. + +Off Android every path is upstream's: the rotation is always `None`. + +Unnecessary once Slint's Skia wgpu surface pre-rotates on its own — +worth offering upstream, since the linuxkms backend already renders +through the same rotate-and-translate in `render_to_canvas`. diff --git a/third_party/i-slint-renderer-skia-1.17.1/itemrenderer.rs b/third_party/i-slint-renderer-skia-1.17.1/itemrenderer.rs index e8556ca..9b90fd6 100644 --- a/third_party/i-slint-renderer-skia-1.17.1/itemrenderer.rs +++ b/third_party/i-slint-renderer-skia-1.17.1/itemrenderer.rs @@ -509,7 +509,7 @@ impl<'a> SkiaItemRenderer<'a> { // `&self` is needed. Returns true if the caller must call `restore()` on `self.canvas`. fn save_canvas_and_pixel_align_origin(&self) -> bool { let local_to_device = self.canvas.local_to_device_as_3x3(); - if !local_to_device.is_translate() || local_to_device.is_identity() { + if !keeps_pixel_grid(&local_to_device) || local_to_device.is_identity() { return false; } let Some(device_to_local) = local_to_device.invert() else { @@ -529,7 +529,7 @@ impl<'a> SkiaItemRenderer<'a> { fn pixel_align_origin_auto_restore(&self) -> Option> { let local_to_device = self.canvas.local_to_device_as_3x3(); - if !local_to_device.is_translate() || local_to_device.is_identity() { + if !keeps_pixel_grid(&local_to_device) || local_to_device.is_identity() { return None; } let device_to_local = local_to_device.invert()?; @@ -546,6 +546,25 @@ impl<'a> SkiaItemRenderer<'a> { } } +/// DarkRoom patch: whether `m` maps whole pixels onto whole pixels, so that +/// rounding the origin in device space aligns everything drawn after it. +/// +/// Upstream asks `is_translate()`, which is right until the whole canvas is +/// turned a quarter turn for a pre-rotated swapchain (`wgpu_29_surface`): +/// then no matrix is a pure translation and pixel alignment silently stops +/// everywhere. A right-angle rotation or flip without scaling keeps the +/// grid just as well, so it qualifies too. +fn keeps_pixel_grid(m: &skia_safe::Matrix) -> bool { + let unit = |v: f32| v == 0. || v == 1. || v == -1.; + m.is_translate() + || (!m.has_perspective() + && m.rect_stays_rect() + && unit(m.scale_x()) + && unit(m.scale_y()) + && unit(m.skew_x()) + && unit(m.skew_y())) +} + impl ItemRenderer for SkiaItemRenderer<'_> { fn draw_rectangle( &mut self, diff --git a/third_party/i-slint-renderer-skia-1.17.1/wgpu_29_surface.rs b/third_party/i-slint-renderer-skia-1.17.1/wgpu_29_surface.rs index 03aea4b..4b1bb45 100644 --- a/third_party/i-slint-renderer-skia-1.17.1/wgpu_29_surface.rs +++ b/third_party/i-slint-renderer-skia-1.17.1/wgpu_29_surface.rs @@ -9,7 +9,7 @@ use i_slint_core::partial_renderer::DirtyRegion; use i_slint_core::platform::PlatformError; use i_slint_core::renderer::DrawOutcome; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::sync::Arc; use wgpu_29 as wgpu; @@ -34,6 +34,54 @@ pub struct WGPUSurface { surface: Option>, textures_to_transition_for_sampling: RefCell>, pub(crate) backend: Backend, + // DarkRoom patch: pre-rotation. The adapter is kept to ask the surface + // for its `currentTransform`; `window_size` is the size the window + // reported, which differs from the swapchain's when `rotation` is 90° or + // 270°; `rotation` is what the current swapchain was promised. + adapter: Option, + window_size: Cell, + rotation: Cell, +} + +/// DarkRoom patch: the right-angle transforms a swapchain can be pre-rotated +/// by, clockwise, as the presentation engine will apply them. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Rotation { + None, + Cw90, + Cw180, + Cw270, +} + +impl Rotation { + fn swaps_axes(self) -> bool { + matches!(self, Rotation::Cw90 | Rotation::Cw270) + } + + /// Map window coordinates onto a buffer sized for this rotation. + /// + /// The window is `w` × `h`. A clockwise quarter turn sends its top-left + /// corner to the buffer's top-right, so a point (x, y) lands at + /// (h − y, x): Skia's `rotate(90)` gives (−y, x), and translating by the + /// buffer width `h` first brings that back on screen. The other two follow + /// the same way. + fn apply(self, canvas: &skia_safe::Canvas, w: f32, h: f32) { + match self { + Rotation::None => {} + Rotation::Cw90 => { + canvas.translate((h, 0.)); + canvas.rotate(90., None); + } + Rotation::Cw180 => { + canvas.translate((w, h)); + canvas.rotate(180., None); + } + Rotation::Cw270 => { + canvas.translate((0., w)); + canvas.rotate(270., None); + } + } + } } impl WGPUSurface { @@ -56,6 +104,7 @@ impl WGPUSurface { let mut surface_config = surface.get_default_config(&adapter, size.width, size.height).unwrap(); + let rotation = pre_rotate(&surface, &adapter, &mut surface_config, size); let swapchain_capabilities = surface.get_capabilities(&adapter); let swapchain_format = swapchain_capabilities @@ -86,6 +135,9 @@ impl WGPUSurface { surface: Some(surface), textures_to_transition_for_sampling: RefCell::new(Vec::new()), backend, + adapter: Some(adapter), + window_size: Cell::new(size), + rotation: Cell::new(rotation), }) } @@ -105,6 +157,9 @@ impl WGPUSurface { surface: None, textures_to_transition_for_sampling: RefCell::new(Vec::new()), backend, + adapter: None, + window_size: Cell::new(PhysicalWindowSize::default()), + rotation: Cell::new(Rotation::None), } } @@ -130,6 +185,77 @@ impl WGPUSurface { gr_context.submit(None); } + + /// DarkRoom patch: configure the surface for a window of `size`, sized + /// and pre-rotated for the transform the display has right now. + fn configure_for( + &self, + surface_config: &mut wgpu::SurfaceConfiguration, + size: PhysicalWindowSize, + ) { + let (Some(surface), Some(adapter)) = (&self.surface, &self.adapter) else { return }; + let rotation = pre_rotate(surface, adapter, surface_config, size); + surface.configure(&self.device, surface_config); + self.window_size.set(size); + self.rotation.set(rotation); + } +} + +/// DarkRoom patch: size `config` for a window of `size` under the display's +/// current transform, and tell wgpu-hal to promise that transform to the +/// next swapchain. Returns the rotation every frame must then be drawn with. +/// +/// Android only. Everywhere else, and whenever the transform is not a plain +/// rotation (a mirrored one, or a query that failed), this leaves the +/// configuration at the window's size with `IDENTITY` — upstream's +/// behaviour, under which the compositor rotates the buffer itself. +fn pre_rotate( + surface: &wgpu::Surface<'static>, + adapter: &wgpu::Adapter, + config: &mut wgpu::SurfaceConfiguration, + size: PhysicalWindowSize, +) -> Rotation { + let rotation = current_rotation(surface, adapter); + #[cfg(target_os = "android")] + unsafe { + use ash::vk::SurfaceTransformFlagsKHR as T; + if let Some(hal) = surface.as_hal::() { + hal.set_pre_transform(match rotation { + Rotation::None => T::IDENTITY, + Rotation::Cw90 => T::ROTATE_90, + Rotation::Cw180 => T::ROTATE_180, + Rotation::Cw270 => T::ROTATE_270, + }); + } + } + (config.width, config.height) = + if rotation.swaps_axes() { (size.height, size.width) } else { (size.width, size.height) }; + rotation +} + +#[cfg(target_os = "android")] +fn current_rotation(surface: &wgpu::Surface<'static>, adapter: &wgpu::Adapter) -> Rotation { + use ash::vk::SurfaceTransformFlagsKHR as T; + let transform = unsafe { + let (Some(surface), Some(adapter)) = ( + surface.as_hal::(), + adapter.as_hal::(), + ) else { + return Rotation::None; + }; + surface.current_transform(&adapter) + }; + match transform { + Some(T::ROTATE_90) => Rotation::Cw90, + Some(T::ROTATE_180) => Rotation::Cw180, + Some(T::ROTATE_270) => Rotation::Cw270, + _ => Rotation::None, + } +} + +#[cfg(not(target_os = "android"))] +fn current_rotation(_surface: &wgpu::Surface<'static>, _adapter: &wgpu::Adapter) -> Rotation { + Rotation::None } impl crate::Surface for WGPUSurface { @@ -154,14 +280,17 @@ impl crate::Surface for WGPUSurface { fn resize_event(&self, size: PhysicalWindowSize) -> Result<(), PlatformError> { let mut surface_config_opt = self.surface_config.borrow_mut(); - let (Some(surface_config), Some(surface)) = (surface_config_opt.as_mut(), &self.surface) + let (Some(surface_config), Some(_)) = (surface_config_opt.as_mut(), &self.surface) else { return Ok(()); }; // Skip reconfigure if size hasn't changed — DRM/KMS surfaces don't // support being reconfigured. - if surface_config.width == size.width && surface_config.height == size.height { + // + // DarkRoom patch: compared against the window's size rather than the + // swapchain's, which is transposed while pre-rotated a quarter turn. + if self.window_size.get() == size { return Ok(()); } @@ -174,10 +303,7 @@ impl crate::Surface for WGPUSurface { // Prefer FIFO modes over possible Mailbox setting for frame pacing and better energy efficiency. surface_config.present_mode = wgpu::PresentMode::AutoVsync; - surface_config.width = size.width; - surface_config.height = size.height; - - surface.configure(&self.device, surface_config); + self.configure_for(surface_config, size); Ok(()) } @@ -192,6 +318,20 @@ impl crate::Surface for WGPUSurface { ) -> Option, pre_present_callback: &RefCell>>, ) -> Result { + // DarkRoom patch: a turn from landscape to reverse landscape changes + // the display's transform without changing the window's size, so no + // resize arrives to say so — and wgpu-hal hides the + // `VK_SUBOPTIMAL_KHR` that would, on Android. Check before every + // frame and reconfigure when it moved; the query is one call into + // the native window. + if let (Some(surface), Some(adapter)) = (&self.surface, &self.adapter) + && current_rotation(surface, adapter) != self.rotation.get() + && let Some(surface_config) = self.surface_config.borrow_mut().as_mut() + { + self.gr_context.borrow_mut().flush_submit_and_sync_cpu(); + self.configure_for(surface_config, self.window_size.get()); + } + let (Some(surface), Some(surface_config)) = (&self.surface, &*self.surface_config.borrow()) else { return Err("WGPUSurface::render() called on offscreen surface".into()); @@ -228,6 +368,15 @@ impl crate::Surface for WGPUSurface { let mut skia_surface = skia_surface .ok_or_else(|| PlatformError::from("Failed to create Skia surface from WGPU"))?; + // DarkRoom patch: everything Slint draws, in window coordinates, goes + // through this one matrix into the pre-rotated buffer. + let window_size = self.window_size.get(); + self.rotation.get().apply( + skia_surface.canvas(), + window_size.width as f32, + window_size.height as f32, + ); + callback(skia_surface.canvas(), Some(gr_context), 0); self.flush_and_submit(gr_context);