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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 106 | 106 | if _, ok := third.DamageSince(second.Seq); ok { | |
| 107 | 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 | } | |
| @@ -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) { | |||
| 252 | 252 | } | |
| 253 | 253 | ||
| 254 | 254 | // 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 | 258 | func (s *surface) redraw() { | |
| 257 | 259 | r := s.root() | |
| 258 | 260 | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | |
| 261 | + | c := r.tree(id) | |
| 259 | 262 | d := damage{full: true} | |
| 260 | - | if s == r && len(r.sub.children) == 0 { | |
| 263 | + | if s == r && len(c.Children) == 0 { | |
| 261 | 264 | 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 | 289 | // Parent updates cannot make a detached child's bound commits visible. | |
| 287 | 290 | s.dropQueue() | |
| 288 | 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 | 294 | p.redraw() | |
| 290 | 295 | p.emitInput() | |
| 291 | 296 | 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 commit | 254 | // 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.committed | 264 | 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() | ||