Fix DirectX atlas panic after GPU device recovery (#55878) (cherry-pick to preview) - #56035
Merged
Merged
Conversation
## Problem A Sentry-reported crash on Windows (Intel Iris Xe Graphics, v1.0.1): ``` index out of bounds: the len is 1 but the index is 1 ``` panicking at `DirectXAtlasState::texture` in [`crates/gpui_windows/src/directx_atlas.rs`](https://github.com/zed-industries/zed/blob/main/crates/gpui_windows/src/directx_atlas.rs): ```rust AtlasTextureKind::Subpixel => { &self.subpixel_textures[id.index as usize].as_ref().unwrap() } ``` ## Root cause After a GPU device-lost recovery, GPUI's view cache replays stale `AtlasTile` references from the previous frame's `paint_operations` via `Scene::replay`. 1. **Atlas grows past one texture.** A long enough session pushes `subpixel_textures.textures.len() ≥ 2` (easy on Iris Xe at the default 1024×1024 atlas size). Top-level views in Zed use `cached(...)`, so their `AnyViewState.paint_range` records into `rendered_frame.scene.paint_operations`, referencing both index `0` and index `1`. 2. **Device lost.** `handle_device_lost` clears every `AtlasTextureList` (`textures.len() == 0`) and `tiles_by_key`, then sets `skip_draws = true`. 3. **`WM_GPUI_FORCE_UPDATE_WINDOW` arrives.** `mark_drawable()` flips `skip_draws` back to `false` and `request_frame` runs with `force_render: true`. 4. **The cache hit.** Inside `Window::draw`, `AnyView::prepaint`'s cache check (`!dirty_views.contains(...) && !window.refreshing`) succeeds for every cached view because the recovery doesn't touch invalidator state and `force_render` doesn't propagate into `Window`. `AnyView::paint` calls `window.reuse_paint` → `Scene::replay` → `primitive.clone()`, which (since `SubpixelSprite`/`AtlasTile` are `Copy`) verbatim copies a `Primitive::SubpixelSprite { tile: { texture_id: { index: 1, ... }, ... } }` into `next_frame.scene`. 5. **Atlas regrows to one.** Dirty/uncached parts of the same frame (caret, animations, anything that called `cx.notify`) fall through to `paint_glyph` → `get_or_insert_with` → `push_texture`, growing `subpixel_textures.textures` from `0` to **`1`** with index `0` valid. 6. **Panic.** After `mem::swap`, `rendered_frame.scene` contains a mix of fresh `index = 0` and replayed `index = 1` sprites. `Scene::batches` emits separate batches per `texture_id`; the `index = 1` batch reaches `atlas.get_texture_view` → `subpixel_textures[1]` → panic with `len = 1, index = 1`. The two earlier related fixes do not catch this: - **#52389 / dbd95ea** (`if force_render { mark_drawable }`) protects the 200 ms recovery sleep — pending `WM_PAINT`s carry `force_render = false` and so do not clear `skip_draws`. But `WM_GPUI_FORCE_UPDATE_WINDOW` carries `force_render = true`, so `mark_drawable` runs, then `Window::draw`'s `reuse_paint` still reproduces stale tiles. - The unmerged Windows draft `2e5d890e37` (`force_render_after_recovery`) similarly only forces the forced-render branch — it doesn't bypass the view cache. ## Fix Two parts: **1. Bypass the view cache on a forced draw (cross-platform).** In the platform-agnostic `request_frame` closure in `Window::new`, call `window.refresh()` whenever `RequestFrameOptions::force_render` is `true`. `Window::refresh` is the documented escape hatch for cached views (per the `AnyView::cached` docs: *"The one exception is when [Window::refresh] is called, in which case caching is ignored."*). With `refreshing = true` every `AnyView::prepaint` cache check fails, every cached view fully repaints, and `paint_glyph` allocates fresh tiles for every glyph, so `rendered_frame.scene` ends up free of stale `AtlasTile`s. **2. Add the `force_render_after_recovery` flag on Windows.** Mirror the Linux fix from #52389: a per-window `Cell<bool>` set after `WindowsWindowInner::handle_device_lost` succeeds and consumed at the top of `draw_window`. Together with the GPUI change above, the first frame after recovery (whether a stray `WM_PAINT` during the 200 ms recovery sleep or the explicit `WM_GPUI_FORCE_UPDATE_WINDOW`) is treated as a forced render that both clears `skip_draws` and bypasses the view cache. ## Testing - `script/clippy -p gpui` is clean. - I do not have a Windows toolchain available locally, so I have not cross-compiled `gpui_windows`. Reviewers with Windows access — please smoke-test on a machine where the device-lost path can be exercised (Intel iGPU, suspend/resume, or running a TDR-inducing test on a GPU driver). ## Related - Sentry issue ID 7457971403 (DirectX subpixel atlas crash, Intel Iris Xe). - Builds on / fixes the residual gap in #52389 (`gpui_linux: Force scene rebuild after GPU device recovery"). The GPUI change here also hardens the corresponding Linux path against the same `reuse_paint` mechanism. Release Notes: - Fixed a crash on Windows when the GPU device is lost and recovered during use (typically driver crash, suspend/resume, or display reconfiguration, most commonly on Intel iGPUs)
This was referenced May 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cherry-pick of #55878 to preview
Problem
A Sentry-reported crash on Windows (Intel Iris Xe Graphics, v1.0.1):
panicking at
DirectXAtlasState::textureincrates/gpui_windows/src/directx_atlas.rs:Root cause
After a GPU device-lost recovery, GPUI's view cache replays stale
AtlasTilereferences from the previous frame's
paint_operationsviaScene::replay.subpixel_textures.textures.len() ≥ 2(easy on Iris Xe at the default1024×1024 atlas size). Top-level views in Zed use
cached(...), sotheir
AnyViewState.paint_rangerecords intorendered_frame.scene.paint_operations,referencing both index
0and index1.handle_device_lostclears everyAtlasTextureList(
textures.len() == 0)and
tiles_by_key, then setsskip_draws = true.WM_GPUI_FORCE_UPDATE_WINDOWarrives.mark_drawable()flipsskip_drawsback tofalseandrequest_frameruns withforce_render: true.Window::draw,AnyView::prepaint's cachecheck (
!dirty_views.contains(...) && !window.refreshing) succeeds forevery cached view because the recovery doesn't touch invalidator state
and
force_renderdoesn't propagate intoWindow.AnyView::paintcallswindow.reuse_paint→Scene::replay→primitive.clone(), which(since
SubpixelSprite/AtlasTileareCopy) verbatim copies aPrimitive::SubpixelSprite { tile: { texture_id: { index: 1, ... }, ... } }into
next_frame.scene.(caret, animations, anything that called
cx.notify) fall through topaint_glyph→get_or_insert_with→push_texture, growingsubpixel_textures.texturesfrom0to1with index0valid.mem::swap,rendered_frame.scenecontains a mixof
fresh
index = 0and replayedindex = 1sprites.Scene::batchesemits separate batches per
texture_id; theindex = 1batch reachesatlas.get_texture_view→subpixel_textures[1]→ panic withlen = 1, index = 1.The two earlier related fixes do not catch this:
if force_render { mark_drawable }) protectsthe
200 ms recovery sleep — pending
WM_PAINTs carryforce_render = falseand so do not clear
skip_draws. ButWM_GPUI_FORCE_UPDATE_WINDOWcarries
force_render = true, somark_drawableruns, thenWindow::draw'sreuse_paintstill reproduces stale tiles.2e5d890e37(
force_render_after_recovery)similarly only forces the forced-render branch — it doesn't bypass the
view cache.
Fix
Two parts:
1. Bypass the view cache on a forced draw (cross-platform).
In the platform-agnostic
request_frameclosure inWindow::new, callwindow.refresh()wheneverRequestFrameOptions::force_renderistrue.Window::refreshis the documented escape hatch for cached views (perthe
AnyView::cacheddocs: "The one exception is when [Window::refresh] iscalled, in which case caching is ignored."). With
refreshing = trueevery
AnyView::prepaintcache check fails, every cached view fullyrepaints, and
paint_glyphallocates fresh tiles for every glyph, sorendered_frame.sceneends up free of staleAtlasTiles.2. Add the
force_render_after_recoveryflag on Windows.Mirror the Linux fix from #52389: a per-window
Cell<bool>set afterWindowsWindowInner::handle_device_lostsucceeds and consumed at thetop
of
draw_window. Together with the GPUI change above, the first frameafter recovery (whether a stray
WM_PAINTduring the 200 ms recoverysleep or the explicit
WM_GPUI_FORCE_UPDATE_WINDOW) is treated as aforced render that both clears
skip_drawsand bypasses the view cache.Testing
script/clippy -p gpuiis clean.cross-compiled
gpui_windows. Reviewers with Windows access — pleasesmoke-test on a machine where the device-lost path can be exercised
(Intel iGPU, suspend/resume, or running a TDR-inducing test on a GPU
driver).
Related
Xe).
gpui_linux: Force scene rebuild after GPU device recovery"). The GPUI change here also hardens the corresponding Linux path against the samereuse_paint` mechanism.Release Notes:
during use (typically driver crash, suspend/resume, or display
reconfiguration, most commonly on Intel iGPUs)