Let a wgpu-hal caller choose the Vulkan swapchain's preTransform

wgpu-hal creates every swapchain with `preTransform = IDENTITY` (#3345).
On a tablet whose panel is mounted landscape, a portrait window then
hands Android an unrotated buffer: SurfaceFlinger falls back to rotating
it on the GPU (composition CLIENT), and on this device those frames tear.
That is why Android draws with Skia over OpenGL today, and why the
develop view pays a readback (TD-1).

The field cannot just be set to `currentTransform` inside wgpu. It is a
promise that the image is already drawn rotated and sized in the panel's
orientation, and only the renderer above wgpu can keep it. So the patch
is the smallest thing that lets that renderer ask:
`vulkan::Surface::current_transform` reads the surface's transform, and
`set_pre_transform` makes the next swapchain use it. The default stays
IDENTITY, so desktop and any caller that does not opt in behave exactly
as upstream.
This commit is contained in:
2026-09-25 04:18:39 -04:00
parent dc1add9dbb
commit dc9da52651
3 changed files with 83 additions and 2 deletions
+18 -1
View File
@@ -28,7 +28,24 @@ upstream change would make it unnecessary.
## wgpu-hal 29.0.4 — Vulkan pre-rotation on Android
*(patch lands in the next commit)*
Upstream creates every Vulkan swapchain with `preTransform = IDENTITY`
(`src/vulkan/swapchain/native.rs`, gfx-rs/wgpu#3345). On Android, a window
whose orientation differs from the panel's is then rotated by the
compositor on the GPU (`composition=CLIENT`), and on this tablet in
portrait those frames tear.
The patch adds two methods to `wgpu_hal::vulkan::Surface`:
- `current_transform(&Adapter)` — the surface's `currentTransform`.
- `set_pre_transform(transform)` — the `preTransform` for the next
swapchain. Opt-in: nothing calls it but the Skia patch below, and the
default is still `IDENTITY`, so desktop and every other caller behave
exactly as upstream.
Setting it is a promise that the caller draws rotated into a swapchain
sized in the panel's orientation; wgpu itself rotates nothing.
Unnecessary once wgpu exposes pre-rotation itself (#3345).
## i-slint-renderer-skia 1.17.1 — rotate the canvas to match
+35
View File
@@ -208,6 +208,41 @@ impl Surface {
)
}
/// DarkRoom patch: the surface's `currentTransform` — how the presentation
/// engine will rotate what is presented to it — or `None` if the surface
/// is not a native Vulkan surface or the query failed.
///
/// On Android this is how a caller learns the display's rotation relative
/// to the panel, so it can draw pre-rotated (see [`Self::set_pre_transform`]).
pub fn current_transform(&self, adapter: &Adapter) -> Option<vk::SurfaceTransformFlagsKHR> {
self.inner
.as_any()
.downcast_ref::<swapchain::NativeSurface>()?
.current_transform(adapter)
}
/// DarkRoom patch: create the next swapchain with this `preTransform`
/// instead of `IDENTITY`. Takes effect on the next `configure`.
///
/// This is a promise that every frame presented afterwards is already
/// rotated by `transform`, *and* that the configured extent is the
/// unrotated one — width and height swapped from the window's for a
/// 90° or 270° rotation. Keeping the promise is entirely the caller's
/// job: wgpu does not rotate anything. The payoff on Android is that the
/// compositor scans the buffer out directly instead of rotating it on the
/// GPU, and `vkQueuePresentKHR` stops returning `VK_SUBOPTIMAL_KHR`.
///
/// Does nothing on a surface that is not a native Vulkan surface.
pub fn set_pre_transform(&self, transform: vk::SurfaceTransformFlagsKHR) {
if let Some(native) = self
.inner
.as_any()
.downcast_ref::<swapchain::NativeSurface>()
{
native.set_pre_transform(transform);
}
}
/// Get the raw Vulkan swapchain associated with this surface.
///
/// Returns [`None`] if the surface is not configured or if the swapchain
+30 -1
View File
@@ -17,6 +17,10 @@ pub(crate) struct NativeSurface {
raw: vk::SurfaceKHR,
functor: khr::surface::Instance,
instance: Arc<InstanceShared>,
// DarkRoom patch: the `preTransform` the next swapchain is created with.
// `IDENTITY` unless a caller that rotates its own drawing asks otherwise
// through `vulkan::Surface::set_pre_transform`.
pre_transform: Mutex<vk::SurfaceTransformFlagsKHR>,
}
impl NativeSurface {
@@ -26,12 +30,33 @@ impl NativeSurface {
raw,
functor,
instance: Arc::clone(&instance.shared),
pre_transform: Mutex::new(vk::SurfaceTransformFlagsKHR::IDENTITY),
}
}
pub fn as_raw(&self) -> vk::SurfaceKHR {
self.raw
}
/// DarkRoom patch: the transform the presentation engine will apply to
/// this surface right now, as `vkGetPhysicalDeviceSurfaceCapabilitiesKHR`
/// reports it.
pub fn current_transform(
&self,
adapter: &crate::vulkan::Adapter,
) -> Option<vk::SurfaceTransformFlagsKHR> {
let caps = unsafe {
self.functor
.get_physical_device_surface_capabilities(adapter.raw, self.raw)
}
.inspect_err(|e| log::error!("get_physical_device_surface_capabilities: {e}"))
.ok()?;
Some(caps.current_transform)
}
pub fn set_pre_transform(&self, transform: vk::SurfaceTransformFlagsKHR) {
*self.pre_transform.lock() = transform;
}
}
impl Surface for NativeSurface {
@@ -199,7 +224,11 @@ impl Surface for NativeSurface {
.image_array_layers(config.extent.depth_or_array_layers)
.image_usage(conv::map_texture_usage(config.usage))
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
.pre_transform(vk::SurfaceTransformFlagsKHR::IDENTITY)
// DarkRoom patch: upstream hardcodes `IDENTITY` here
// (gfx-rs/wgpu#3345). The value is a promise that the image is
// already drawn rotated, so it is only ever something other than
// `IDENTITY` when the caller has said it draws that way.
.pre_transform(*self.pre_transform.lock())
.composite_alpha(conv::map_composite_alpha_mode(config.composite_alpha_mode))
.present_mode(conv::map_present_mode(config.present_mode))
.clipped(true)