Pre-rotate Slint's Skia drawing on the wgpu swapchain on Android
The other half of the wgpu-hal patch: that one lets a caller promise a pre-rotated swapchain, and this is the caller keeping the promise. On configure, `WGPUSurface` reads the surface's `currentTransform`, sizes the swapchain in the panel's orientation (swapped for a quarter turn), tells wgpu-hal to use that transform, and before each frame concatenates the matching rotation onto the Skia canvas. Everything Slint draws goes through that one matrix, so an imported wgpu texture is rotated with the rest of the window. Input is not rotated, and must not be, because Android delivers it in window coordinates. Three details that would each have been a visible bug: - `resize_event` compared the new size against the swapchain's. The swapchain is transposed while a quarter turn is in effect, so the comparison now uses the window's size, kept beside it. - A half turn, landscape to reverse landscape, changes the transform without resizing the window, and wgpu-hal hides the SUBOPTIMAL that would report it. So the transform is re-read before every frame. That costs one query into the native window. - The item renderer snapped the origin to the pixel grid only when the canvas matrix was a pure translation. Under a rotation that is never true, so portrait would have lost pixel alignment everywhere. The check now accepts right-angle rotations and flips without scaling. The direction of each rotation follows the Vulkan spec's reading of preTransform (the image is drawn already rotated clockwise by the transform). It has not been confirmed on the device yet.
This commit is contained in:
Vendored
+19
-1
@@ -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`.
|
||||
|
||||
+21
-2
@@ -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<skia_safe::canvas::AutoRestoredCanvas<'_>> {
|
||||
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,
|
||||
|
||||
+156
-7
@@ -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<wgpu::Surface<'static>>,
|
||||
textures_to_transition_for_sampling: RefCell<Vec<wgpu::Texture>>,
|
||||
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<wgpu::Adapter>,
|
||||
window_size: Cell<PhysicalWindowSize>,
|
||||
rotation: Cell<Rotation>,
|
||||
}
|
||||
|
||||
/// 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::<wgpu::wgc::api::Vulkan>() {
|
||||
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::<wgpu::wgc::api::Vulkan>(),
|
||||
adapter.as_hal::<wgpu::wgc::api::Vulkan>(),
|
||||
) 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<DirtyRegion>,
|
||||
pre_present_callback: &RefCell<Option<Box<dyn FnMut()>>>,
|
||||
) -> Result<DrawOutcome, PlatformError> {
|
||||
// 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);
|
||||
|
||||
Reference in New Issue
Block a user