From 380946c5b242ff5b5f8ddab5da05861de8d046b3 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 24 Mar 2026 17:14:55 +0100 Subject: [PATCH] Avoid the explicit CATransaction commit Transactions are committed implicitly at the end of each run loop iteration, so if we know we're in a runloop, we'd rather use the later implicit 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 + src/backends/cg.rs | 24 ++++++++++++++++++++---- src/lib.rs | 15 +++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0c9b006..b1abbf25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - **Breaking:** Removed `DamageOutOfRange` error case. If the damage value is greater than the backend supports, it is instead clamped to an appropriate value. - **Breaking:** Removed `SurfaceExtWeb` and the associated `NoDisplayHandle` and `NoWindowHandle` helpers. Use `RawWindowHandle::WebCanvas` or `RawWindowHandle::WebOffscreenCanvas` instead. - Fixed `present_with_damage` with bounds out of range on Windows, Web and X11. +- Reduced flickering when presenting while resizing on macOS. # 0.4.8 diff --git a/src/backends/cg.rs b/src/backends/cg.rs index 789dd442..d1a744bb 100644 --- a/src/backends/cg.rs +++ b/src/backends/cg.rs @@ -418,14 +418,30 @@ impl BufferInterface for BufferImpl<'_> { } .unwrap(); - // Wrap layer modifications in a transaction. Unclear if we should keep doing this, see - // for discussion about this. - CATransaction::begin(); + // Wrap things in a transaction if the event loop isn't currently running. + // + // The event loop implicitly commits pending layer updates at the end of the current run + // loop iteration, as documented in: + // + // + // + // + // This is a bit overly conservative; the full check on macOS could. But it doesn't matter _that_ much, if users decide to render in weird ways, we'll still support it when emitting a transaction, it'll just be slightly less efficient. + let use_transaction = objc2_core_foundation::CFRunLoop::current() + .unwrap() + .current_mode() + .is_none(); + + if use_transaction { + CATransaction::begin(); + } // SAFETY: The contents is `CGImage`, which is a valid class for `contents`. unsafe { layer.setContents(Some(image.as_ref())) }; - CATransaction::commit(); + if use_transaction { + CATransaction::commit(); + } Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 69b4e0e6..2bdc3cf0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -352,6 +352,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.