Skip to content

perf(native): record icon and arc paths once and keep them on the GPU - #1008

Merged
omartelo merged 1 commit into
mainfrom
perf/native-retained-paths
Nov 1, 2026
Merged

omartelo merged 1 commit into
mainfrom
perf/native-retained-paths

Conversation

@omartelo

@omartelo omartelo commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator

Why

Gio keys its path cache on where a path was recorded (ops.Key carries the op.Ops and its version, bumped on every Reset). Text escapes this because text.Shaper records glyph outlines in an op.Ops of its own that is never reset. Everything lich recorded straight into the frame's Ops did not: every Lucide icon (icons.Icon.Layout), ring and arc (ui.StrokeArc) was re-stroked on the CPU, re-triangulated and re-uploaded with glBufferData on every frame. With a session busy that is 30 times a second, and in the window's CPU profile stroke.StrokePathCommands alone was 20% of the time.

What

  • icons.Icon records its shapes' paths once per scale in an op.Ops it owns and hands the same clip.PathSpec out on every frame, the way the shaper does for glyphs. LayoutFilled shares the record.
  • ui.StrokeArc keeps a record of the arcs it drew, keyed by geometry. A turning arc is a new path every frame, so the record is bounded: past 1024 paths it starts over in a fresh op.Ops, and the old one lives on while a frame still calls into it.

Icons are package variables drawn from the window's goroutine; the records assume that.

Measured

Rig: nested headless compositor on an RTX 3050 Laptop, 1400x1420 window, 7 session cards, 3 busy rings, a project tab spinner and a terminal full of colored text, one busy session, 30fps. 15s CPU profile:

CPU StrokePathCommands
main 1.61s (10.7% of a core) 0.48s
this PR 0.79s (5.3%) gone
this PR on the sidebar branch 0.84s (5.6%) 0.03s

GPU time per frame is unchanged, measured at 240Hz interleaving the two binaries: it is the painting itself (stencil passes, blit, swap), not the geometry, and it does not depend on what the window shows. Fewer or partial frames are the only lever on it; that is separate work.

Test plan

  • ui/icons: TestIconPathsAreRecordedOncePerScale, the same spec comes back at the same scale, another at another scale
  • ui: TestStrokeArcPathsAreRecordedOnce, same arc same path, record starts over past its bound
  • native/: gofmt, go vet ./..., go test ./...
  • Rig window drawn and looked at, icons and rings intact, numbers above

Gio keys its path cache on where a path was recorded, and a path written
into the frame's op.Ops is new every frame: every Lucide icon, ring and
arc was re-stroked on the CPU, re-triangulated and re-uploaded 30 times a
second while a session was busy. Icons now record their paths once per
scale in an op.Ops of their own, and StrokeArc keeps a bounded record of
the arcs it drew, so the renderer keeps their vertices between frames.

Rig, dense window, one busy session, 30fps, 15s CPU profile: 1.61s to
0.79s, stroke.StrokePathCommands gone from the profile. GPU time per
frame is unchanged: it is the painting itself, not the geometry.
@omartelo
omartelo merged commit 75a3a37 into main Nov 1, 2026
7 checks passed
@omartelo
omartelo deleted the perf/native-retained-paths branch November 1, 2026 20:25
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