From 1c617ea76f961fc81ce1f74ebc150a210e3ef5b9 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 13 Aug 2025 00:27:51 +0200 Subject: [PATCH] Avoid the explicit CATransaction commit This effectively desyncs the surface from the rest of the window, which isn't desirable when resizing. Instead, we now commit in the implicit transaction that happens at the end of the current runloop iteration. --- CHANGELOG.md | 1 + Cargo.toml | 1 + src/backends/cg.rs | 24 ++++++++++++++---------- src/lib.rs | 15 +++++++++++++++ 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e605c92..eb59be2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - **Breaking:** Disable the DRM/KMS backend by default. - **Breaking:** Removed `DamageOutOfRange` error case. If the damage value is greater than the backend supports, it is instead clamped to an appropriate value. - Fixed `present_with_damage` with bounds out of range on Windows, Web and X11. +- Reduced flickering when presenting while resizing on macOS. # 0.4.7 diff --git a/Cargo.toml b/Cargo.toml index 144b6b4a..4844fa46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,6 +114,7 @@ objc2-foundation = { version = "0.3.2", default-features = false, features = [ "NSString", "NSThread", "NSValue", + "NSNull", ] } objc2-quartz-core = { version = "0.3.2", default-features = false, features = [ "std", diff --git a/src/backends/cg.rs b/src/backends/cg.rs index 889fe374..0f1d2ef7 100644 --- a/src/backends/cg.rs +++ b/src/backends/cg.rs @@ -3,7 +3,7 @@ use crate::backend_interface::*; use crate::error::InitError; use crate::{util, Pixel, Rect, SoftBufferError}; use objc2::rc::Retained; -use objc2::runtime::{AnyObject, Bool}; +use objc2::runtime::{AnyObject, Bool, ProtocolObject}; use objc2::{define_class, msg_send, AllocAnyThread, DefinedClass, MainThreadMarker, Message}; use objc2_core_foundation::{CFRetained, CGPoint}; use objc2_core_graphics::{ @@ -12,10 +12,10 @@ use objc2_core_graphics::{ }; use objc2_foundation::{ ns_string, NSDictionary, NSKeyValueChangeKey, NSKeyValueChangeNewKey, - NSKeyValueObservingOptions, NSNumber, NSObject, NSObjectNSKeyValueObserverRegistration, + NSKeyValueObservingOptions, NSNull, NSNumber, NSObject, NSObjectNSKeyValueObserverRegistration, NSString, NSValue, }; -use objc2_quartz_core::{kCAGravityTopLeft, CALayer, CATransaction}; +use objc2_quartz_core::{kCAGravityTopLeft, CALayer}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle}; use std::ffi::c_void; @@ -225,6 +225,17 @@ impl SurfaceInterface for CGImpl< // resized to something that doesn't fit, see #177. layer.setContentsGravity(unsafe { kCAGravityTopLeft }); + // The CALayer has a default action associated with a change in the layer contents, causing + // a quarter second fade transition to happen every time a new buffer is applied. + // + // We avoid this by setting the action for the "contents" key to NULL. + // + // TODO(madsmtm): Do we want to do the same for bounds/contentsScale for smoother resizing? + layer.setActions(Some(&NSDictionary::from_slices( + &[ns_string!("contents")], + &[ProtocolObject::from_ref(&*NSNull::null())], + ))); + // Initialize color space here, to reduce work later on. let color_space = CGColorSpace::new_device_rgb().unwrap(); @@ -354,16 +365,9 @@ impl BufferInterface for BufferImpl<'_> { } .unwrap(); - // The CALayer has a default action associated with a change in the layer contents, causing - // a quarter second fade transition to happen every time a new buffer is applied. This can - // be avoided by wrapping the operation in a transaction and disabling all actions. - CATransaction::begin(); - CATransaction::setDisableActions(true); - // SAFETY: The contents is `CGImage`, which is a valid class for `contents`. unsafe { self.layer.setContents(Some(image.as_ref())) }; - CATransaction::commit(); Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index cdbb7f58..97bd962f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -301,6 +301,21 @@ impl Buffer<'_> { /// /// If the caller wishes to synchronize other surface/window changes, such requests must be sent to the /// Wayland compositor before calling this function. + /// + /// ## macOS / iOS + /// + /// On macOS/iOS/etc., this sets the [contents] of the underlying [`CALayer`], but doesn't yet + /// actually commit those contents to the compositor; that is instead done automatically by + /// QuartzCore at the end of the current iteration of the runloop. This synchronizes the + /// contents with the rest of the window, which is important to avoid flickering when resizing. + /// + /// If you need to send the contents to the compositor immediately (might be useful when + /// rendering from a separate thread or when using Softbuffer without the standard AppKit/UIKit + /// runloop), you'll want to wrap this function in a [`CATransaction`]. + /// + /// [contents]: https://developer.apple.com/documentation/quartzcore/calayer/contents?language=objc + /// [`CALayer`]: https://developer.apple.com/documentation/quartzcore/calayer?language=objc + /// [`CATransaction`]: https://developer.apple.com/documentation/quartzcore/catransaction?language=objc #[inline] pub fn present(self) -> Result<(), SoftBufferError> { // Damage the entire buffer.