Skip to content

CoreGraphics: Use double-buffering#343

Open
madsmtm wants to merge 3 commits intomasterfrom
apple-buffering
Open

CoreGraphics: Use double-buffering#343
madsmtm wants to merge 3 commits intomasterfrom
apple-buffering

Conversation

@madsmtm
Copy link
Member

@madsmtm madsmtm commented Mar 5, 2026

We currently create a new Vec each time next_buffer is called on macOS/iOS, which is inefficient, we should instead re-use previous buffers.

Double-buffering seems to be enough here, but in degenerate cases such as the following:

let buffer = surface.next_buffer().unwrap();
buffer.present().unwrap();
// Render again right after!
let buffer = surface.next_buffer().unwrap();
buffer.present().unwrap();

The buffer is still referenced by QuartzCore somewhere, so we need to keep a (potentially infinite) queue of buffers around to be sure that we don't write to a buffer that's in use. Note that there is no way to wait for the buffers to be released since the release happens on the main thread (and besides, we probably don't want to wait either?).

All of this might be overly pedantic, I haven't yet found a case where the buffer is actually read from by QuartzCore while being the back buffer (unlike with IOSurface).

Tested on:

  • macOS 15.7.3 M2
  • macOS 10.12.6 Intel
  • iOS simulator

Should help with #83.
Extracted from #329 (since that one may be more difficult to land).

@madsmtm madsmtm added enhancement New feature or request DS - CoreGraphics macOS/iOS/tvOS/watchOS/visionOS backend labels Mar 5, 2026
@madsmtm madsmtm added this to the Softbuffer v0.5 milestone Mar 5, 2026
self.buffers.push(Buffer::new(self.width, self.height));
// This should have no effect on latency, but it will affect the `buffer.age()` that
// users see, and unbounded allocation is undesirable too, so we should try to avoid it.
tracing::warn!("had to allocate extra buffer in `next_buffer`, you might be rendering faster than the display rate?");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this warning printed a few times when I run the animation example. Presumably it shouldn't be if the example is using things correctly?

And animation is just calling next_buffer() on WindowEvent::RedrawRequested, so that seems correct enough.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WindowEvent::RedrawRequested is actually currently emitted wrong on macOS, when resizing it may be issued twice in a single frame. So getting the warning once after resizing is "expected". We could lower it to a debug! when self.buffers.len() == 3 until the Winit issue is fixed?

Playing around with it a bit I did once manage to get the warning issued a bunch of times in a row, but now I can't reproduce it, so not sure what that's about.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I guess if it's a bug in winit, then it doesn't really make sense to log as a warning since there's no obvious way for the user of the library to actually fix it.

Leaving it as debug! with a comment to change this after wiinit is fixed sounds good.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a debug! comment now, could I get you to try again and see if you get the warning?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to still be producing warnings:

ian@Ians-Mac-mini-5 softbuffer % RUST_LOG=debug cargo run --example animation
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/examples/animation`
2026-03-12T03:26:24.358343Z DEBUG winit::ActiveEventLoop::create_window{window_attributes=WindowAttributes { inner_size: None, min_inner_size: None, max_inner_size: None, position: None, resizable: true, enabled_buttons: WindowButtons(CLOSE | MINIMIZE | MAXIMIZE), title: "winit window", maximized: false, visible: true, transparent: false, blur: false, decorations: true, window_icon: None, preferred_theme: None, resize_increments: None, content_protected: false, window_level: Normal, active: true, cursor: Icon(Default), parent_window: None, fullscreen: None, platform_specific: PlatformSpecificWindowAttributes { movable_by_window_background: false, titlebar_transparent: false, title_hidden: false, titlebar_hidden: false, titlebar_buttons_hidden: false, fullsize_content_view: false, disallow_hidpi: false, has_shadow: true, accepts_first_mouse: true, tabbing_identifier: None, option_as_alt: None, borderless_game: false } }}: winit::platform_impl::macos::app_state: had to queue event since another is currently being handled event=WindowEvent { window_id: WindowId(40792015488), event: Resized(PhysicalSize { width: 800, height: 600 }) }
2026-03-12T03:26:24.359300Z DEBUG winit::ActiveEventLoop::create_window{window_attributes=WindowAttributes { inner_size: None, min_inner_size: None, max_inner_size: None, position: None, resizable: true, enabled_buttons: WindowButtons(CLOSE | MINIMIZE | MAXIMIZE), title: "winit window", maximized: false, visible: true, transparent: false, blur: false, decorations: true, window_icon: None, preferred_theme: None, resize_increments: None, content_protected: false, window_level: Normal, active: true, cursor: Icon(Default), parent_window: None, fullscreen: None, platform_specific: PlatformSpecificWindowAttributes { movable_by_window_background: false, titlebar_transparent: false, title_hidden: false, titlebar_hidden: false, titlebar_buttons_hidden: false, fullsize_content_view: false, disallow_hidpi: false, has_shadow: true, accepts_first_mouse: true, tabbing_identifier: None, option_as_alt: None, borderless_game: false } }}: winit::platform_impl::macos::app_state: had to queue event since another is currently being handled event=WindowEvent { window_id: WindowId(40792015488), event: Focused(false) }
2026-03-12T03:26:27.004819Z DEBUG softbuffer::backends::cg: had to allocate extra buffer in `next_buffer`, this is probably a bug in Winit's RedrawRequested
2026-03-12T03:26:27.011200Z  WARN softbuffer::backends::cg: had to allocate extra buffer in `next_buffer`, you might be rendering faster than the event loop can handle?
2026-03-12T03:26:27.017192Z  WARN softbuffer::backends::cg: had to allocate extra buffer in `next_buffer`, you might be rendering faster than the event loop can handle?
2026-03-12T03:26:27.023131Z  WARN softbuffer::backends::cg: had to allocate extra buffer in `next_buffer`, you might be rendering faster than the event loop can handle?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, are you doing anything with the window? Resizing it? Moving the cursor a lot? Or does it just happen after time?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DEBUG and two WARN lines seems to happen pretty immediately (running the animation example on Tahoe 26.3.1 on an M1 Mac Mini). At least most of the time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DS - CoreGraphics macOS/iOS/tvOS/watchOS/visionOS backend enhancement New feature or request

Development

Successfully merging this pull request may close these issues.

2 participants