detached b392a0f

perf(wayland): keep partial damage when subsurfaces have no buffer

A window with an empty subsurface (kept for later use, as foot does) sent every commit as a full change, so wl_shm windows were copied whole each frame. Only drawn subsurfaces now make a commit full; detaching one still redraws the parent fully. foot printing a counter: NeferWL CPU 8.1% -> 4.3% of one core, copy share 620 ms -> 180 ms per 12 s.
commit
b392a0f28ab297a32a65e5dc182ad757e268a255
author
brice · · 2026-10-07 21:28 UTC
parent
360492d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +26 -3
M internal/adapters/wayland/damage_test.go +18 -0
@@ -106,4 +106,22 @@ func TestDamageHistory(t *testing.T) {
106106 if _, ok := third.DamageSince(second.Seq); ok {
107107 t.Fatal("undamaged new buffer taken as unchanged")
108108 }
109+ // A subsurface with no buffer draws nothing: once its layout is
110+ // applied (one full change), the root keeps its damage.
111+ subc := bindProtocol(t, c, "wl_subcompositor")
112+ child, sub := c.AllocateID(), c.AllocateID()
113+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
114+ registerProtocol(t, c, child)
115+ registerProtocol(t, c, sub)
116+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, surf)
117+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0))
118+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
119+ third = next()
120+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0))
121+ requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(1), int32(1), int32(2), int32(2))
122+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
123+ fourth := next()
124+ if got, ok := fourth.DamageSince(third.Seq); !ok || len(got) != 1 || got[0] != (ports.Rect{X: 1, Y: 1, W: 2, H: 2}) {
125+ t.Fatalf("empty subsurface: damage %v %v", got, ok)
126+ }
109127 }
@@ -106,4 +106,22 @@ func TestDamageHistory(t *testing.T) {
106 if _, ok := third.DamageSince(second.Seq); ok {106 if _, ok := third.DamageSince(second.Seq); ok {
107 t.Fatal("undamaged new buffer taken as unchanged")107 t.Fatal("undamaged new buffer taken as unchanged")
108 }108 }
109+ // A subsurface with no buffer draws nothing: once its layout is
110+ // applied (one full change), the root keeps its damage.
111+ subc := bindProtocol(t, c, "wl_subcompositor")
112+ child, sub := c.AllocateID(), c.AllocateID()
113+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
114+ registerProtocol(t, c, child)
115+ registerProtocol(t, c, sub)
116+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, surf)
117+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0))
118+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
119+ third = next()
120+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0))
121+ requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(1), int32(1), int32(2), int32(2))
122+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
123+ fourth := next()
124+ if got, ok := fourth.DamageSince(third.Seq); !ok || len(got) != 1 || got[0] != (ports.Rect{X: 1, Y: 1, W: 2, H: 2}) {
125+ t.Fatalf("empty subsurface: damage %v %v", got, ok)
126+ }
109 }127 }
M internal/adapters/wayland/surface.go +8 -3
@@ -252,15 +252,18 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) {
252252 }
253253
254254 // redraw sends the tree of a mapped root to the outputs. Only a commit
255-// of the root's own buffer with no other change keeps partial damage.
255+// of the root's own buffer with no other change keeps partial damage:
256+// subsurfaces without a buffer (clients often keep one for later) are not
257+// drawn, so they do not make it full.
256258 func (s *surface) redraw() {
257259 r := s.root()
258260 if id := r.windowID(); id != 0 && s.server.channels.Contents != nil {
261+ c := r.tree(id)
259262 d := damage{full: true}
260- if s == r && len(r.sub.children) == 0 {
263+ if s == r && len(c.Children) == 0 {
261264 d = r.committed
262265 }
263- s.server.emitContent(r.tree(id), d)
266+ s.server.emitContent(c, d)
264267 }
265268 }
266269
@@ -286,6 +289,8 @@ func (s *surface) detach() {
286289 // Parent updates cannot make a detached child's bound commits visible.
287290 s.dropQueue()
288291 s.flushDesync()
292+ // The child's area changes, not the parent's buffer: redraw it all.
293+ p.root().committed = damage{full: true}
289294 p.redraw()
290295 p.emitInput()
291296 s.sendScale()
@@ -252,15 +252,18 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) {
252 }252 }
253 253
254 // redraw sends the tree of a mapped root to the outputs. Only a commit254 // redraw sends the tree of a mapped root to the outputs. Only a commit
255-// of the root's own buffer with no other change keeps partial damage.255+// of the root's own buffer with no other change keeps partial damage:
256+// subsurfaces without a buffer (clients often keep one for later) are not
257+// drawn, so they do not make it full.
256 func (s *surface) redraw() {258 func (s *surface) redraw() {
257 r := s.root()259 r := s.root()
258 if id := r.windowID(); id != 0 && s.server.channels.Contents != nil {260 if id := r.windowID(); id != 0 && s.server.channels.Contents != nil {
261+ c := r.tree(id)
259 d := damage{full: true}262 d := damage{full: true}
260- if s == r && len(r.sub.children) == 0 {263+ if s == r && len(c.Children) == 0 {
261 d = r.committed264 d = r.committed
262 }265 }
263- s.server.emitContent(r.tree(id), d)266+ s.server.emitContent(c, d)
264 }267 }
265 }268 }
266 269
@@ -286,6 +289,8 @@ func (s *surface) detach() {
286 // Parent updates cannot make a detached child's bound commits visible.289 // Parent updates cannot make a detached child's bound commits visible.
287 s.dropQueue()290 s.dropQueue()
288 s.flushDesync()291 s.flushDesync()
292+ // The child's area changes, not the parent's buffer: redraw it all.
293+ p.root().committed = damage{full: true}
289 p.redraw()294 p.redraw()
290 p.emitInput()295 p.emitInput()
291 s.sendScale()296 s.sendScale()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close