Repository navigation
Conversation
…erfaces Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e CPU The atlas mirror keeps a CPU copy of every atlas texture once a renderer with a CPU presenter enables it, logs the rectangles written (capped, then unioned per texture, then "everything"), and serves sprites to the CPU rasterizer. The adaptive policy draws a frame on the CPU, inside damage, stale rectangles left by GPU frames and sprites over written atlas tiles, unless the frame changes more than a sixteenth of the window in a burst, would redraw the canvas whole in a burst, exceeds 8 Mpx, or the burst's CPU frames cost more than a quarter of its time after 250 ms. The canvas is released after a second of GPU frames; presenter errors turn the CPU path off. GPUI_CPU_RENDER=0/always and GPUI_RENDER_STATS=1 control and report it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n X11 Wayland: CPU frames are shown on a synchronized subsurface covering the window surface, because the Vulkan WSI gives the window surface explicit sync (wp_linux_drm_syncobj_surface_v1) and committing a wl_shm buffer to it is a protocol error. Each frame copies into a free wl_shm buffer (up to four, one memfd each, tracked by wl_buffer.release) the regions the buffer misses since it was last attached, attaches it to the subsurface, damages the frame's damage and commits the window surface, which carries the frame callback. gpu_presented() removes the subsurface's buffer so the GPU frame's commit hides it. The subsurface is scaled like the window surface from the size and scale composition's resize hook reports. X11: damage rectangles go into the window with ShmPutImage through an MIT-SHM segment (memfd, ShmAttachFd) when large, else PutImage split under the maximum request length. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CPU frames can now be shown in two ways: with the platform's presenter (native), or by uploading the changed rectangles to a texture that a tiny full-screen pipeline copies to the swapchain image (blit), which needs no presenter and works on every wgpu window. GPUI_CPU_PRESENT=native|blit selects it; unset, default_present_mode uses the presenter where one is installed and blit otherwise. Atlases mirror from their first upload when CPU frames are possible; renderers resolving to none turn the mirror off. The presenter is told the GPU presents (gpu_presented) from gpu_drew, right before the swapchain present, including for the first frame, which now always draws on the GPU. A refused CPU frame draws on the GPU; the CPU path turns off only after MAX_PRESENT_FAILURES refusals in a row. GPUI_RENDER_STATS breaks CPU frames down by presentation mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`gpui_perf --idle` shows the trading workspace in a 1600x1000 window and runs small-update scenarios for a fixed time each (CaretBlink, Clock, Hover, Spinner, Quotes, Scroll, Idle), with the application's own timers off so that only the scenario changes the window. Each reports the frames drawn, the main thread's CPU per frame (mean, p50, p95) and the process's CPU over the run, prints begin/end markers, and writes JSON with --json. With GPUI_RENDER_STATS set, the renderer's `render stats` log records are printed on stderr. `script/measure-adaptive` builds gpui_perf once and runs each scenario with GPUI_CPU_RENDER=0, the default and optionally `always`, alternating the order, sampling /proc CPU and RSS and nvidia-smi power, utilization, P-state, clock and memory, against an idle-desktop baseline. On Hyprland it floats the window at 1600x1000; --x11 runs through XWayland. It writes results.json with every sample and a Markdown summary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
raster::draw draws a scene into a canvas inside regions only, batch by batch in the GPU renderer's order, porting each fragment shader in f32: quads (every background, rounded corners, solid and dashed borders), shadows, underlines, monochrome, subpixel and polychrome sprites (with transformations and bilinear sampling), paths (4x multisampled into an intermediate, then composited through the batch's sprites) and scroll layer tiles (their content drawn over the layer's background). Blending reproduces how the GPU writes an 8-bit target: each fragment channel truncated to 12 bits of fixed point, rounded to a level, and levels blended as integers; texture filtering uses 8-bit fractions and weights. Pixel tests against the GPU renderer, of every primitive and shader branch and of seeded random scenes, in opaque and premultiplied modes, find no channel more than 2 levels apart (3 on one path edge pixel). Large regions are drawn in bands on several threads, exactly as on one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…GPU unless sampled 4x Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`fast::damage::finish_frame` fills `Scene.damage` when a frame is finished: the device-pixel rectangles where the new scene can draw different pixels than the previous one, exactly. Each primitive kind's two vectors, both in drawing order, are compared one draw order at a time. Groups equal byte for byte damage nothing. Otherwise the two are walked side by side, looking up to 8 primitives ahead to agree again; failing that, or for small groups, primitives are matched by value (an open-addressing hash table, exact equality), and matched primitives outside a longest increasing run are damaged only if they overlap a primitive they changed places with. Visible extents follow the shaders: drop shadows by three blur radii, inset shadows by their element, transformed sprites by their transformed bounds, paths padded by a pixel. Surfaces are always damaged, scroll layer tiles by their layer's dirty tiles and generation. Rectangles within 8 px merge, past 16 all merge, and damage over half the window covers it. The first scene and one after a resize have `since == 0`. LayoutStats gains damage counters and time, and gpui_perf's headless report shows the damaged share of the window, small and full damage frames, changed primitives and damage time per frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tead of all Changes spread over the window (quotes ticking in several panels) stay apart: workspace-quotes damage drops from 66% to 20% of the window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A batch of paths that share an order is composited through one sprite per path. Clipped to a band or region beside some of them but level with them, their rectangles came out inverted, and copying a row of the intermediate through one indexed past it: a thread panicked. `GPUI_CPU_VERIFY=1` found it in the trading workspace's charts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The raster reproduced NVIDIA's conversion of a fragment to the 8-bit target: truncated to 12 bits of fixed point, then rounded to a level. Intel (Mesa, measured on Arrow Lake) truncates to 16 bits, reproduced exactly on every level and hundredth of a level. `RasterParams::fragment_bits` comes from the adapter's vendor: 12 on NVIDIA, 16 elsewhere. On Intel two rounding edges remain, which the tests now allow there: a rotated sprite's filtering can land 3 levels apart, and a checkerboard cell's edge exactly on a pixel center can move a pixel (its division comes out just below the whole number), so random scenes draw checkerboards solid on GPUs other than NVIDIA's. `adapter_info` is `pub(crate)` for the vendor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a burst, a frame that would redraw the canvas whole goes to the GPU. An animation that never pauses, such as a 24 px spinner, so stayed on the GPU for good once a large change had left the canvas stale. Once a burst's frames have each changed at most a sixteenth of the window for 250 ms (`CATCH_UP_AFTER`), the CPU redraws the canvas whole once, and the frames after draw only what they change. The Spinner scenario now draws every frame on the CPU, at about 0.035 ms each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…CPU_VERIFY=1 A CPU frame redraws the canvas only inside its region. With `GPUI_CPU_VERIFY=1` each CPU frame's scene is also drawn whole into a second canvas and compared pixel by pixel; the raster's pixels do not depend on how a region is split, so a difference is a change the region missed: missed damage, stale pixels or atlas writes. Mismatches are logged with their bounds and the frame's damage and region, and counted in the render stats (`verify_n`, `verify_bad`, `verify_bad_px`). It costs a whole frame per frame: for checking real applications. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… blit mode Without nvidia-smi the script sampled no GPU. It now also reads an Intel GPU's utilization (time out of RC6) and clock from i915's sysfs, and on any DRM driver that reports it the GPU time of the application and of the compositor (`drm-engine-*` in /proc/<pid>/fdinfo), which other programs on the desktop do not disturb. The `blit` mode runs adaptively with `GPUI_CPU_PRESENT=blit`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…frame CPU frames show on a subsurface over the window surface, which still holds the last GPU frame. Where the compositor makes the window translucent (a window-opacity rule, which Omarchy sets on every window), that frame showed through the CPU frames, and the window came out more opaque than the rule. A CPU frame now sets the window surface's alpha multiplier to 0 with `wp_alpha_modifier_v1`, in the commit that shows the subsurface, and the next GPU frame sets it back in its own commit. At window opacity 0.5 in the Hover scenario, native CPU frames now match blit ones (36 pixels apart by more than 4 levels, against 54,095 before). Without the protocol no presenter is installed, so CPU frames are shown by blit, which has no such effect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd the opacity fix Wayland and X11 measurements of gpu, adaptive (native) and blit on an Intel integrated GPU, with per-process GPU time; the CPU/GPU screenshot comparison; GPUI_CPU_VERIFY; per-vendor fragment conversion; the catch-up rule for long bursts; native presenting as the default now that the window surface is made transparent under Wayland CPU frames. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same or lower CPU, no application GPU time for small updates; the point is an idle GPU (power and heat, mostly on laptops and discrete GPUs), not speed, and power is not measured yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e rendering, Scene damage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… dirty rectangles The renderer draws every frame into a canvas it keeps, copies the canvas into the back buffer and presents. A frame whose damage is relative to the scene the canvas holds clears and draws only its damage rectangles, scissored, and presents with Present1 and the damage as dirty rectangles, so that DWM recomposes only those. Frames are drawn whole without a canvas of the window's size, when the damage is not relative to the canvas, after an atlas write, with surfaces, with damage over half the window, or while a graphics debugger captures. GPUI_PARTIAL_REDRAW=0 keeps the old path; GPUI_RENDER_STATS=1 prints partial and whole frames per second. Hooks: DirectXRenderer::draw forwards to fast::partial::draw, a field, a release on a lost device, an atlas-write call, and pub(crate) on font_info and three DirectXResources fields. Tests draw a scene of every primitive kind, paths included, into the damage of a canvas holding the scene before and compare it with the scene drawn whole, on a composition swap chain's resources (no window); CI runs them on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Metal renderer now keeps the last frame in a canvas texture of its own and draws each window frame into it: whole, as before, or, when the canvas holds the scene the damage compares with, only inside the damage. For each damage rectangle a pass loads the canvas, scissors every main encoder to the rectangle, fills it with the clear color and draws the scene's batches; paths are rasterized into their intermediate whole and composited through the scissor (`fast::paths::encode_pass`). The canvas is then copied into the drawable by a full-screen triangle that reads it texel for texel (drawables are framebuffer-only, so a blit cannot write them), and presented as before. Frames are drawn whole when the canvas is missing, resized or drawn for the other alpha mode, the scene's damage does not compare with the canvas (not numbered, a dropped frame, composed windows' replayed scenes), the atlas was written since the last frame (counted per atlas in `fast_writes`), the scene has surfaces, or the damage covers over half the window. A scene drawn again shows the canvas as it is. Only `draw` goes through the canvas; the headless and image paths draw as upstream does. `GPUI_PARTIAL_REDRAW=0` draws as upstream does. `GPUI_RENDER_STATS=1` prints `partial_frames`, `full_frames`, `partial_px` and `why_<reason>` every second. Pixel tests compare partial frames with the same scene drawn whole, byte for byte, and run in CI on macOS. Hooks: `MetalRenderer::fast_partial` and the forwarding call in `draw`; `MetalAtlasTextures::fast_writes`, noted in `insert`; `render_frame`, `read_texture_to_image`, `MetalAtlas.0` and `MetalAtlasTextures` made `pub(crate)`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The canvas each renderer keeps, when a frame is partial and when whole on each platform, how Metal and Direct3D 11 draw the damage and present it (Present1 dirty rectangles on Windows, a copy pass into the drawable on macOS), the tests that check them, GPUI_PARTIAL_REDRAW and the render stats, and what they save and cost, unmeasured on real hardware yet. The README lists partial redraws among the features. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A partial frame encoded a render pass over the canvas, and its path rasterization, for each damage rectangle. On Apple's GPUs, which render in tiles, every pass loads and stores the whole canvas, so each rectangle cost that again. Now the scene's batches are cut into segments at the path groups rasterized between main passes, as in a whole frame, and each main pass scissors to every rectangle in turn, filling it in the first pass and drawing the segment's batches through it in the scene's order. Overlapping damage rectangles are merged first, so that no pixel is drawn twice. Tests: four separate rectangles drawn in one main pass and equal to the scene drawn whole; overlapping rectangles merged and drawn once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g them Each rectangle draws the whole scene into itself, scissored, so a pixel two rectangles share would have its translucent primitives blended twice. Overlapping rectangles become their union until none overlap, as on Metal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, and walk the batches once for all damage rectangles The canvas was drawn with the previous appearance's clear color, which no scene damage covers: a frame now draws whole (why_appearance) when the clear color differs from the one the canvas was drawn with. Partial frames drew the whole scene once per damage rectangle, so each path batch cleared and resolved the full-window 4x MSAA intermediate once per rectangle. fast::frame::draw_scene_in walks the batches once, drawing each through every rectangle's scissor and rasterizing each path batch once, scissored to the rectangles' union. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y note Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Concurrent draws with different fragment bits (tests on NVIDIA and Intel settings run in parallel) overwrote each other's process-global setting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…faces can_draw looked only at the window scene's paths and surfaces, so paths in scroll layer tiles were drawn with 4x coverage at other sample counts, and surfaces in tiles were skipped. It now builds the tiles the frame draws, checks their content too, and hands them to the draw. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The writes were taken from the atlas at the start of every attempt but only folded into the policy when the attempt presented. A frame that was not presented, or a GPU frame that failed, dropped them, and a scene presented again then left the sprites over them stale on the canvas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WgpuRenderer::recover replaced the renderer whole, dropping the presenter the platform installs only with the window: CPU frames fell back to blit for good, and on Wayland dropping a shown presenter destroyed its subsurface before a GPU frame replaced it. The presenter is now taken out before the renderer is recreated, its memory released, and installed in the new renderer, whose first GPU frame tells it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…osures The swapchain presents in FIFO mode, so a GPU frame's PresentPixmap can land in the window after CPU frames were uploaded over it, and later CPU frames upload only their own damage. The presenter now selects CompleteNotify on the window with its own event context; a GPU present completing after CPU frames were shown refreshes the window, and that frame uploads again everything the CPU frames since the GPU presented uploaded. Without a compositing manager, Expose re-presents the same scene as a CPU frame without damage: the exposed rectangles are now uploaded with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anvas Measured on an M4, a partial frame still costs 75-80% of a whole frame's GPU time (loading, storing and copying the canvas), and the window server composites every presented drawable whole. A frame whose scene was drawn already, or whose damage relative to the canvas is empty, now draws and presents nothing on Metal (decided before taking a drawable) and on Direct3D (no Present1 with a 1x1 rectangle). Layer tiles still rasterize for it. The render stats count these as skipped_frames. Records the M4 measurements in docs/adaptive-rendering.md and README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… only the sprites atlas writes change Measured on an RTX 4080 at 3840x2040, a 3,788-pixel spinner frame and a 1.2 M-pixel scroll frame cost about the same GPU time: every frame copied the whole 31 MB canvas into the back buffer. A flip-sequential swap chain hands its buffers out in turn, so the back buffer holds the frame presented BUFFER_COUNT presents ago; now only the damage of the frames since is copied (CopySubresourceRegion), and the whole canvas whenever that cannot be known: until every buffer holds one of our frames, after a present elsewhere (GetLastPresentCount), a present that did not return S_OK, a whole frame, or a new canvas. A test on a real swap chain checks that the back buffer comes out as the canvas frame after frame. Atlas writes drew the next frame of every window whole (4 of 5 whole frames in Quotes, 36 in Scroll). Writes are now logged with their tile bounds, and the sprites whose tile overlaps one join the damage; past 256 writes the frame is drawn whole. Records the Windows measurements in docs/adaptive-rendering.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The main thread's and the process's CPU time came out as zero on Windows; read them from GetThreadTimes and GetProcessTimes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Partial frames still uploaded and rasterized every scene path before applying the main-pass scissor. Omit paths that cannot reach the damage while preserving batching, draw order, and a conservative filtering margin. Add Metal regression tests for omitted paths and mixed batches. All 25 Metal tests, clippy, and the upstream check pass. Three alternating M3 runs show application GPU time falling 70% for Spinner and 52% for Scroll against the previous partial implementation, at 120 fps.
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.
Summary
Adds scene damage, adaptive CPU rendering on Linux, and partial GPU redraws on macOS and Windows to reduce rendering work for small changes:
crates/gpui/src/fast/damage.rs, all platforms): each finished scene carriesScene::damage, the device-pixel rectangles where it can draw different pixels than the previous scene. The damage is exact. It diffs each draw-order group: byte-equal groups are skipped; otherwise primitives are matched by value and checked for relative order. It also covers primitive extents (shadow blur, transformed sprites, paths), dirty scroll-layer tiles, and path batches that split or join. Nearby rectangles merge (8 px). Past 16 rectangles the cheapest pair merges, and damage over half the window becomes the whole window.crates/gpui_wgpu/src/fast/{cpu,adaptive}/, Linux wgpu): a CPU rasterizer reproduces the WGSL shaders and the GPU's 8-bit blending, and is checked pixel by pixel againstWgpuHeadlessRenderer. How a fragment becomes an 8-bit level depends on the GPU vendor (NVIDIA truncates to 12 bits, Intel to 16), and the rasterizer handles both. The atlas keeps a CPU mirror. A policy draws small damage on the CPU and sends large changes, expensive bursts, surfaces and the first frame to the GPU. Long bursts of small changes, such as a spinner, catch the canvas up once and then stay on the CPU.crates/gpui_linux/src/fast/cpu_present/): Waylandwl_shmon a synchronized subsurface, because the explicit-sync WSI surface forbids shm. While the subsurface shows,wp_alpha_modifier_v1makes the window surface transparent, so translucent windows show no ghosting. Without that protocol, CPU frames go through blit. X11 usesPutImage, with MIT-SHM for large rectangles.blitmode: upload the region and draw one full-screen triangle.crates/gpui_apple/src/fast/partial.rs,crates/gpui_windows/src/fast/partial.rs, macOS and Windows): the GPU renderers keep the last frame in a window-sized canvas and redraw only the damage rectangles into it, scissored, then copy the canvas into the drawable / back buffer. Windows presents withPresent1and the damage as dirty rectangles. A frame with nothing to draw (same scene, or empty damage) is neither drawn nor presented.GPUI_PARTIAL_REDRAW=0turns it off.Upstream files get hooks only.
script/check-upstreamreports 0 violations. The API differences are listed indocs/upstream-sync.md. The reference doc isdocs/adaptive-rendering.mdand the design is indocs/superpowers/specs/2026-10-08-damage-and-adaptive-cpu-design.md.Controls:
GPUI_CPU_RENDER=0|always,GPUI_CPU_PRESENT=native|blit,GPUI_PARTIAL_REDRAW=0,GPUI_RENDER_STATS=1,GPUI_CPU_VERIFY=1.What this means
On Linux, at the same or lower CPU cost, the application stops using the GPU for small updates. In every small-update scenario the process CPU stays flat or drops: caret 0.4% → 0.2%, quotes 1.9% → 1.4%, spinner 8.3% → 4.3%. The application's GPU time drops to 0.
Performance
Measured with
script/measure-adaptive --modes gpu,adaptive,blit --reps 2, 20 s per run. Hardware: Intel Arrow Lake iGPU, 4K at scale 1.5, Hyprland. The GPU columns are the GPU time of the application's and the compositor's own DRM clients, in ms per second.X11 (XWayland) shows the same pattern. For example, Spinner: 8.3% → 4.3% CPU, 320 → 0 ms/s app GPU, and 237 → 138 ms/s compositor GPU. Full tables are in the doc.
Native beats blit on every measure. Blit keeps 5–10% of the GPU time. It also makes the compositor recomposite the whole window, while native recomposites only the damage.
Re-measured after the review fixes (same command, Wayland and X11): every mode keeps 60 fps and the gpu vs adaptive picture is unchanged. Wayland: caret 0.4% → 0.3%, hover 1.4% → 1.1%, quotes 1.9% → 1.6%, spinner 9.1% → 5.0% CPU, and app GPU time 19–358 ms/s → 0 for every small update. X11: spinner 6.9% → 3.5%. The desktop was not in the same state as in the morning run (the idle compositor drew 110 ms/s instead of 28 on Wayland, and nothing on X11, likely a blanked screen), so compare modes within a run, not absolute numbers across runs.
Per-frame CPU cost of this PR (headless
gpui_perf --headless --frames 300, retention on, 1440×900; the PR against its merge base edbd764, median of 3 alternating runs, run-to-run spread mostly 1–4%). Headless frames are not drawn, so this shows the cost only, not the rendering savings. Almost all of it is the damage diff:table-ticks-manylist-selectlist-variable-scroll(worst)At most ~0.1 ms a frame, about 0.6% of a core at 60 fps. In real windows it is more than paid back on small updates (the CPU column above drops), but frames that are redrawn whole anyway (scrolling,
GPUI_PARTIAL_REDRAW=0, headless) pay it for nothing. A follow-up can stop diffing once the damage covers most of the window: 60–91% of the frames inworkspace-scrollandlist-selectend up whole.macOS partial redraws, re-measured after the empty-frame fix (2026-10-08, commit
907863e, MacBook Pro M3 / 24 GB, 1512×836 pt window at 2×). Same release build,gpui_perf --idle --duration 15 --warmup 2,GPUI_RENDER_STATS=1, three rounds alternating whole and partial redraws (full/partial, partial/full, full/partial). Whole frames (GPUI_PARTIAL_REDRAW=0) → partial (GPUI_PARTIAL_REDRAW=1); each value is the median of three runs. Process CPU is a percentage of one core. GPU time is the change in each process'sioregaccumulatedGPUTime, divided by the measured duration, excluding startup and warmup.The application's GPU time drops 24–40%, with update frequency unchanged. Small-update CPU is approximately flat, but continuous animations have a measurable cost: Spinner's CPU median rises from 34.89% to 42.67%, and Scroll's from 40.45% to 45.57%. CPU varies across runs (Spinner: whole 34.18–43.14%, partial 39.58–43.34%; Scroll: whole 40.30–44.14%, partial 41.90–46.39%), so these results do not support a claim of no CPU regression on macOS.
WindowServer GPU time also rises for both continuous animations in every paired round. Its counters include the whole desktop, with about 70 ms/s of background activity even in Idle, so they cannot all be attributed to this application. Lower application GPU time is not evidence of lower total power consumption here; power and battery life were not measured. Apple's partial path still loads/stores the canvas and copies the whole canvas to the drawable; Core Animation composites each presented drawable whole.
Scroll includes expected whole-frame fallbacks after glyph atlas writes. Across each complete partial-mode run (including warmup and scenario transitions), there were 43 whole frames: one initial canvas frame and 42 atlas-write fallbacks. No frame was skipped in these measurements: Quotes had visible changes at this wider viewport, while Idle already requested no ongoing draws in either mode. The empty-frame fix's correctness is covered by the Metal pixel tests, but its performance benefit in a narrower window has not been measured. These M3 results replace the earlier M4 table; hardware and viewport differ, so absolute values should not be compared across the two runs. Windows has not been measured on real hardware.
CPU rasterizer (this machine): 1920×1080 whole frame in 7.7 ms on 1 thread and 2.3 ms on 8 threads. A 200×40 text region takes 55 µs and a caret 14 µs.
Correctness checks
GPUI_CPU_VERIFY=1redraws each CPU frame's scene whole and compares it pixel by pixel with the incrementally drawn canvas, so it catches any missed damage, stale region or atlas write. It was run on everygpui_perf --idlescenario: tiled and 1600×1000 windows, Wayland and X11, native and blit, ~2600 CPU frames. Result:verify_bad=0everywhere.wp_alpha_modifier_v1they differed in 54,095.Review fixes
A five-way review (damage, adaptive policy, CPU raster, Linux presenters, macOS/Windows) found no missed damage in the core diff. Fixed:
PutImagecould land before a queued GPUPresentPixmapand be overwritten at vblank. The presenter now selectsPresentCompleteNotifyand re-uploads what CPU frames drew since the GPU's present. Expose rectangles are uploaded too (no compositing manager: uncovered areas stayed black).can_drawnow checks paths and surfaces inside the scroll-layer tiles it draws.Tests
907863e:cargo test -p gpui_apple --lib23 passed, including 8 partial-redraw tests on the real Metal device;cargo test -p gpui --features test-support --lib fast::tests::damage15 passed;script/check-upstream0 violations. All six performance runs completed successfully with no panic, ERROR or WARN logs. Manual occlusion, window resizing and cross-display checks were not performed.-D warnings) and rustfmt are clean; the Windows and macOS crates are also cross-checked from Linux.Notes
🤖 Generated with Claude Code