Skip to content

gpu: redraw only what changed into a framebuffer the renderer keeps - #5

Merged
omartelo merged 3 commits into
lichfrom
gpu-retained-partial-redraw
Nov 8, 2026
Merged

omartelo merged 3 commits into
lichfrom
gpu-retained-partial-redraw

Conversation

@omartelo

@omartelo omartelo commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Partial redraw trusted EGL_BUFFER_AGE, and NVIDIA 595 on COSMIC reports an age its buffer does not hold, so the whole window showed stale pixels. This keeps the frame in an sRGB texture the renderer owns, redraws only the damaged rectangles there, and blits it whole to the output with a plain full swap. Buffer age and swap-with-damage are gone. The behaviour stays behind GIO_DAMAGE=on.

Non-OpenGL backends keep drawing whole frames straight to the output (FeatureBlitFramebuffer), as does any frame without a clear or with a translucent clear, whose ground would blend over the kept frame. Paths the rectangles do not reach keep their place in the atlas and only lose their stencil, because a path's place changes its coverage by a rounding.

A second commit logs once when GL_RENDERER names a software renderer.

Pixel test

gpu/headless/retained_test.go draws 12 seeds of 30 frames changing a little at a time into the retained framebuffer and compares each frame with the same frame drawn whole straight to the output, dropping the retained contents every 11 frames (resize) and requiring a whole redraw. Without stencilled paths the frames match byte for byte. With paths a channel may differ by 1: whole frames drawn with their paths at other atlas positions differ from each other by the same one level (measured, 185 of 3,072,000 bytes across 10 seeds, max delta 1), so byte equality against a whole frame is not defined there.

On a real NVIDIA surface, a private headless compositor screenshot of the same window with GIO_DAMAGE off and on has 0 differing pixels.

Measurements

Real NVIDIA GPU, private headless umbriel, one shell session, 1260x700. GL work per frame is GPU and driver time for the passes and the blit, between two glFinish calls, from an uncommitted local instrumentation; render is LICH_NATIVE_FRAME_LOG.

Load Mode GL work (ms, 240-frame buckets) render p50 / p95 (ms)
One line rewritten, 19 fps whole 2.04-3.60 3.6 / 7.0
One line rewritten, 19 fps retained 1.24-2.05 2.6 / 5.2
Scrolling log, 10 fps, 66% of the window dirty whole 4.50-4.69 6.0 / 11
Scrolling log, 10 fps, 66% of the window dirty retained 4.74-4.88 6.4 / 11.1

The retained texture costs width x height x 4 bytes (3.5 MB at 1260x700). The tracker adds about 47 KiB of allocations per frame.

Test plan

  • go test ./gpu/... ./app/... ./internal/..., also TestPartialFramesMatchWhole and TestPartialPathFramesMatchWhole on NVIDIA and on llvmpipe, 5 repeats on NVIDIA
  • Mutations: removing the ground and removing the blit both fail the pixel test
  • GOOS=windows go build ./..., GOOS=js GOARCH=wasm go build ./gpu/... ./internal/gl/...
  • lich native/ go vet ./... and go test ./... with replace gioui.org pointed at this branch

Partial redraw relied on EGL_BUFFER_AGE, which NVIDIA 595 on COSMIC reports
for a buffer that does not hold that frame, so a whole window showed stale
pixels. Keep the frame in an sRGB texture the renderer owns, redraw only the
damaged rectangles there and blit it whole to the output; the swap stays a
plain full swap. The buffer age and swap-with-damage paths go away. It stays
behind GIO_DAMAGE=on.

A frame goes straight to the output and whole on backends without
FeatureBlitFramebuffer, on a frame that did not clear, and on a translucent
clear, whose ground would blend over the kept frame. Paths the rectangles
do not reach keep their place in the atlas and only lose their stencil, as
the place changes a path's coverage by a rounding.

Measured in lich on a real NVIDIA GPU on a private headless compositor, a
terminal line updating at 19 fps: 1.2-2.0 ms of GL work per frame, from 2.0-3.6
ms drawn whole. A scrolling log, two thirds of the window dirty, costs
0.1-0.4 ms more than drawn whole. The texture is width x height x 4 bytes.
The retained framebuffer no longer reads it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant