## Summary
This fixes macOS live-resize behavior for the `eframe`/`egui-wgpu` path
when using the low-latency wgpu surface configuration.
The problem I was seeing is that native window resize can look visibly
below the baseline expected from a desktop GUI: stale or stretched
frames (manifesting as wobble/jitter), or severe lag while dragging a
window edge.
The fix has three parts:
- use `CAMetalLayer.presentsWithTransaction` during live resize to avoid
stale/stretched frames
- temporarily use at least `desired_maximum_frame_latency = 2` while
live resize is active, so transaction presentation does not stall when
the app normally uses `SurfaceConfig::LOW_LATENCY`
- treat macOS `WindowEvent::Moved` as part of the live-resize event
stream, since resizing from the top or left edge changes the window
origin
This PR depends on the winit-side AppKit live-resize timing fix in
[rust-windowing/winit#4588](https://github.com/rust-windowing/winit/pull/4588)
A renderer-only frame-latency change is not enough by itself. The
temporary latency bump only solves the drawable starvation caused by
combining `presentsWithTransaction` with `SurfaceConfig::LOW_LATENCY`.
It does not change when winit emits resize/redraw events, whether
redraws are delivered during AppKit's live-resize event-tracking loop,
or whether the surface size is derived from the current backing rect.
That is why the winit fix is needed first: it makes the windowing layer
report the current AppKit backing size and request redraws from the
live-resize/display callbacks. egui-wgpu still needs this PR on top
because winit does not own the wgpu `Surface` or the underlying
`CAMetalLayer` presentation policy.
In other words: winit fixes when the windowing layer reports
resize/redraw work, while this PR fixes how egui-wgpu presents
Metal-backed wgpu frames during that resize.
## Why change the existing feature?
The existing `macos-window-resize-jitter-fix` feature addresses one
symptom by enabling transaction presentation during resize, but it is
not enough for the low-latency wgpu path.
In particular, `presentsWithTransaction` and
`SurfaceConfig::LOW_LATENCY` interact poorly during AppKit live resize.
The old code avoids that by [skipping transaction presentation when
latency is
`1`](71c4ff3c33/crates/egui-wgpu/src/winit.rs (L417)),
but that means low-latency users get the resize jitter/wobble back.
This PR keeps the low-latency path normally, but temporarily bumps frame
latency only while live resize is active. That gives the resize path
enough drawable slack without changing normal interaction latency.
I removed the `macos-window-resize-jitter-fix` feature because this
seems like the behavior the macOS wgpu path should have by default, not
a separate opt-in. If keeping the feature as a no-op compatibility alias
is preferred, I can adjust the PR.
## Validation
I created a small demo app that somewhat resembles the layout of my
actual app and highlights both horizontal and vertical resize jitter:
- a borderless macOS window
- a simple toolbar
- a scrolling side list
- `SurfaceConfig::LOW_LATENCY`
The toolbar and list make stale or stretched frames easy to see during
native resize. The jitter is visible even on the traffic light buttons.
Recordings:
### Before 1: no transaction presentation, low latency
Shows jitter/wobble and stale/stretched frames during live resize.
https://github.com/user-attachments/assets/2cf4467b-e14c-4f41-8021-0b8c23f41004
### Before 2: transaction presentation with low latency
Shows the other failure mode: live resize can become severely laggy when
transaction presentation is used while keeping
`SurfaceConfig::LOW_LATENCY`.
https://github.com/user-attachments/assets/2f866790-f472-4ede-a3c0-480e8f0f041a
### After: patched egui-wgpu + patched winit, low latency
No visible wobble/jitter and no severe live-resize lag.
https://github.com/user-attachments/assets/59e46e9f-7906-4b5c-a6c7-1d09eae644cd
---------
Co-authored-by: lucasmerlin <hi@lucasmerlin.me>
## Symptom
Fix this long-standing, occasional bug, that can cause text to look
compressed and "drunk":
<img width="552" height="226" alt="Screenshot 2026-06-22 at 13 12 56"
src="https://github.com/user-attachments/assets/9b1abad4-5ef6-4771-8168-f201afc341ab"
/>
## Root cause
`epaint::TextureAtlas::take_delta` is fire-and-forget: it resets the
dirty region as soon as it hands out a delta, assuming the delta will be
uploaded. Atlas growth always emits a **full** `ImageDelta` (`pos:
None`) which recreates the GPU texture at the new size — *as long as it
is applied*. But both native integrations applied `textures_delta`
inside skippable code paths:
- **wgpu** (`egui-wgpu/src/winit.rs`): textures were uploaded only
*after* surface-dependent early-returns (`render_state` /
`surfaces.get_mut(viewport_id)` missing). Texture uploads are
device-level and don't need a surface.
- **glow** (`eframe/src/native/glow_integration.rs`): textures were
uploaded only inside `if is_visible { … }` (and after a viewport-missing
early-return), while `integration.update` still ran and grew the atlas.
The root window even starts hidden on purpose (`with_visible(false)`, to
avoid a startup white flash), so the very first frames hit this.
When the delta was dropped, the GPU font texture stayed smaller than the
CPU-side atlas; every glyph UV (normalized by the CPU atlas size) then
sampled the wrong rows until the next full atlas recreation. wgpu/Metal
can't detect this — the read is in-bounds, just the wrong row.
## Fixes
- **wgpu**: apply `textures_delta.set` right after `render_state` is
obtained, **before** any surface-dependent early-return. `free` still
runs after submit (unchanged).
- **glow**: apply `textures_delta.set` (and `free`) regardless of
`is_visible`, making the GL context current when there's anything to
upload; only tessellation/paint/swap stay gated on visibility.
- **debug assert** in `egui-wgpu`'s `Renderer::update_texture`: a full
delta must (re)create the GPU texture at exactly the delta size —
catches any future CPU/GPU size desync at the source.
## wgpu ruled out
Confirmed the desync is **not** inside wgpu: Metal `create_texture` uses
the exact descriptor size, and `queue.write_texture` validates against
the texture's own live `desc` — a single texture can't have CPU/GPU
sizes disagree. The mismatch is born at the egui boundary (atlas size
for UVs vs. last-applied upload), which wgpu cannot see.
## Testing note
A headless regression test of `paint_and_update_textures` isn't
practical (it needs a real winit window; `render_state` is private with
no surface-less setter). I verified the failure *mechanism* separately
on macOS/Metal (texture lagging the atlas → silent wrong-row sampling,
no wgpu error), but that demo did not exercise the fixed code path, so
it's not included. The fixes rest on the reasoning above.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
### Related
* Closes#8134.
* Related to #5136.
Possibly fixes:
* #8123
* #5145
### What
We did not properly handle the variants of
[`CurrentSurfaceTexture`](https://docs.rs/wgpu/latest/wgpu/enum.CurrentSurfaceTexture.html)
and always returned `SkipFrame`.
Because of this `egui` could end up in a state where frames are always
skipped after observing `Outdated`, without the chance to recover
(unless an event arrives from the outside).
> [!NOTE]
> This is not Wayland-specific, but could happen on all platforms. It
just happens frequently for Wayland compositors that directly resize a
window after creation (such as tiling/scrolling compositors like
`hyprland` and `niri`).
This PR improves this by separating the code paths for `Outdated` and
`Lost`, to help recover from those events.
## Related
* https://github.com/emilk/egui/issues/8043
* Introduced in #8103
## What
I noticed resizing the native winit window was really choppy and bad on
macOS, and this was the readon
Let apps change present_mode and desired_maximum_frame_latency at
runtime instead of only at startup.
API changes (egui-wgpu):
- New SurfaceConfig { present_mode, desired_maximum_frame_latency }.
- WgpuConfiguration now nests these as pub surface: SurfaceConfig (was
two top-level fields).
- RenderState gains pub surface_config: SurfaceConfig — the
currently-requested value.
API additions (eframe):
- Frame::wgpu_surface_config() / Frame::set_wgpu_surface_config(...) for
get/set.
- SurfaceConfig re-exported as eframe::SurfaceConfig.
How it works:
The wgpu painter compares render_state.surface_config to its
currently-applied values each paint. If they differ it updates its
config and flips
needs_reconfigure on every surface, piggybacking on the existing
deferred-reconfigure pathway.
Demo:
The backend panel (egui_demo_app) gets dropdowns for present mode and
desired max frame latency, wired through the new Frame accessors.
<img width="282" height="172" alt="image"
src="https://github.com/user-attachments/assets/0b1274b2-7e4e-4413-969b-0a014c415f79"
/>
Enable these new clippy lints and fix all warnings:
* `format_push_string` — use `write!` instead of `s += &format!(…)` to
avoid extra allocations
* `ignored_unit_patterns` — use `()` instead of `_` when matching unit
* `missing_fields_in_debug` — ensure manual `Debug` impls account for
all fields
* `needless_raw_string_hashes` — remove unnecessary `r#` on string
literals
* `ref_option` — prefer `Option<&T>` over `&Option<T>` in function
signatures
* Fix for https://github.com/emilk/eframe_template/issues/223
* Related: https://github.com/gfx-rs/wgpu/pull/9319
By default, we would only turn on the WebGPU backend on web, which means
browsers without WebGPU support would just crash.
You can still opt-out of all the default `wgpu` features by enabling
`eframe/wgpu_no_default_features` instead of `eframe/wgpu`
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md)
before opening a Pull Request!
* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.
Please be patient! I will review your PR, but my time is limited!
-->
* Closes <https://github.com/emilk/egui/issues/THE_RELEVANT_ISSUE>
* [x] I have followed the instructions in the PR template
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
* [x] I have followed the instructions in the PR template
This updates wgpu to v29 across the egui crate stack.
There a a few API changes due to the requirement to provide a display
handle up front to properly support GLES on linux. I have done my best
to make the api changes as reasonable as possible, but I don't have all
the greater project context, so lmk if things should be done a bit
differently.
I've also updated glow to 0.17 to make cargo deny happy, there are no
source changes. I'm not sure how you want to land these.
---------
Co-authored-by: lucasmerlin <hi@lucasmerlin.me>
This PR enables users of `egui-wgpu` to render `epaint` primitives
without having to bring in the complete `egui` crate and all it's
dependencies.
---------
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
## Short bluesky announcement:
We just released egui 0.33.0!
Highlights:
- `egui::Plugin` a improved way to create and access egui plugins
- [kitdiff](https://github.com/rerun-io/kitdiff), a viewer for
egui_kittest image snapshots (and a general image diff tool)
- better kerning (check the diff on
[kitdiff](https://rerun-io.github.io/kitdiff/?url=https://github.com/emilk/egui/pull/7431))
https://github.com/user-attachments/assets/971f0493-6dae-42e5-8019-58b74cf5d203
## Relaese Changelog:
egui is an easy-to-use immediate mode GUI for Rust that runs on both web
and native.
Try it now: <https://www.egui.rs/>
egui development is sponsored by [Rerun](https://www.rerun.io/), a
startup building an SDK for visualizing streams of multimodal data.
# egui 0.33.0 changelog
Highlights from this release:
- `egui::Plugin` a improved way to create and access egui plugins
- [kitdiff](https://github.com/rerun-io/kitdiff), a viewer for
egui_kittest image snapshots (and a general image diff tool)
- better kerning
### Improved kerning
As a step towards using [parley](https://github.com/linebender/parley)
for font rendering, @valadaptive has refactored the font loading and
rendering code. A result of this (next to the font rendering code being
much nicer now) is improved kerning.
Notice how the c moved away from the k:

### `egui::Plugin` trait
We've added a new trait-based plugin api, meant to replace
`Context::on_begin_pass` and `Context::on_end_pass`.
This makes it a lot easier to handle state in your plugins. Instead of
having to write to egui memory it can live right on your plugin struct.
The trait based api also makes easier to add new hooks that plugins can
use. In addition to `on_begin_pass` and `on_end_pass`, the `Plugin`
trait now has a `input_hook` and `output_hook` which you can use to
inspect / modify the `RawInput` / `FullOutput`.
### kitdiff, a image diff viewer
At rerun we have a ton of snapshots. Some PRs will change most of them
(e.g. [the](https://github.com/rerun-io/rerun/pull/11253/files)
[one](https://rerun-io.github.io/kitdiff/?url=https://github.com/rerun-io/rerun/pull/11253/files)
that updated egui and introduced the kerning improvements, ~500
snapshots changed!).
If you really want to look at every changed snapshot it better be as
efficient as possible, and the experience on github, fiddeling with the
sliders, is kind of frustrating.
In order to fix this, we've made
[kitdiff](https://rerun-io.github.io/kitdiff/).
You can use it locally via
- `kitdiff files .` will search for .new.png and .diff.png files
- `kitdiff git` will compare the current files to the default branch
(main/master)
Or in the browser via
- going to https://rerun-io.github.io/kitdiff/ and pasting a PR or
github artifact url
- linking to kitdiff via e.g. a github workflow
`https://rerun-io.github.io/kitdiff/?url=<link_to_pr_or_artefact>`
To install kitdiff run `cargo install --git
https://github.com/rerun-io/kitdiff`
Here is a video showing the kerning changes in kitdiff ([try it
yourself](https://rerun-io.github.io/kitdiff/?url=https://github.com/rerun-io/rerun/pull/11253/files)):
https://github.com/user-attachments/assets/74640af1-09ba-435a-9d0c-2cbeee140c8f
### Migration guide
- `egui::Mutex` now has a timeout as a simple deadlock detection
- If you use a `egui::Mutex` in some place where it's held for longer
than a single frame, you should switch to the std mutex or parking_lot
instead (egui mutexes are wrappers around parking lot)
- `screen_rect` is deprecated
- In order to support safe areas, egui now has `viewport_rect` and
`content_rect`.
- Update all usages of `screen_rect` to `content_rect`, unless you are
sure that you want to draw outside the `safe area` (which would mean
your Ui may be covered by notches, system ui, etc.)
* [x] I have followed the instructions in the PR template
Splitting this out from the Parley work as requested. This removes
`FontImage` and makes the font atlas use a `ColorImage`. It converts
alpha to coverage at glyph-drawing time, not at delta-upload time.
This doesn't do much now, but will allow for color emoji rendering once
we start using Parley.
I've changed things around so that we pass in `text_alpha_to_coverage`
to the `Fonts` the same way we do with `pixels_per_point` and
`max_texture_side`, reusing the existing code to check if the setting
differs and recreating the font atlas if so. I'm not quite sure why this
wasn't done in the first place.
I've left `ImageData` as an enum for now, in case we want to add support
for more texture pixel formats in the future (which I personally think
would be worthwhile). If you'd like, I can just remove that enum
entirely.