fix(wayland): redraw a subsurface's area when it stops drawing
Partial damage was kept whenever the published tree had no drawn subsurface, so a synced child hidden by a null attach (applied by the parent's commit) left its old pixels on screen. The root now remembers whether its last tree drew children and only keeps partial damage when neither did. This also covers destroying a drawn child, so detach no longer forces a full change for an empty one.
- commit
- 892d98ea798c967714cb42671f612facfe5e934e
- author
- brice · · 2026-10-08 05:12 UTC
- parent
- c0d1352
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +58 -6
M internal/adapters/wayland/damage_test.go +50 -0
| @@ -124,4 +124,54 @@ func TestDamageHistory(t *testing.T) { | |||
| 124 | 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 | 125 | t.Fatalf("empty subsurface: damage %v %v", got, ok) | |
| 126 | 126 | } | |
| 127 | + | // A synced child drawn, then hidden by a null attach the parent's | |
| 128 | + | // commit applies: its old area must be redrawn, both times. | |
| 129 | + | rootCommit := func(what string, wantChildren int) { | |
| 130 | + | t.Helper() | |
| 131 | + | prev := fourth | |
| 132 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 133 | + | fourth = next() | |
| 134 | + | if len(fourth.Children) != wantChildren { | |
| 135 | + | t.Fatalf("%s: %d children, want %d", what, len(fourth.Children), wantChildren) | |
| 136 | + | } | |
| 137 | + | if got, ok := fourth.DamageSince(prev.Seq); ok { | |
| 138 | + | t.Fatalf("%s kept partial damage %v", what, got) | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | requestProtocol(t, c, child, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0)) | |
| 142 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |
| 143 | + | rootCommit("child appearing", 1) | |
| 144 | + | requestProtocol(t, c, child, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0)) | |
| 145 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |
| 146 | + | rootCommit("child hidden", 0) | |
| 147 | + | // Destroying a child that draws nothing changes nothing on screen. | |
| 148 | + | prev := fourth | |
| 149 | + | requestProtocol(t, c, sub, wayland.SubsurfaceRequestDestroy) | |
| 150 | + | fourth = next() | |
| 151 | + | if got, ok := fourth.DamageSince(prev.Seq); !ok || len(got) != 0 { | |
| 152 | + | t.Fatalf("empty child destroyed: damage %v %v", got, ok) | |
| 153 | + | } | |
| 154 | + | prev = fourth | |
| 155 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |
| 156 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(1), int32(1), int32(2), int32(2)) | |
| 157 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 158 | + | fourth = next() | |
| 159 | + | if got, ok := fourth.DamageSince(prev.Seq); !ok || len(got) != 1 { | |
| 160 | + | t.Fatalf("after empty child: damage %v %v", got, ok) | |
| 161 | + | } | |
| 162 | + | // Destroying a drawn child redraws its area. | |
| 163 | + | child2, sub2 := c.AllocateID(), c.AllocateID() | |
| 164 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child2) | |
| 165 | + | registerProtocol(t, c, child2) | |
| 166 | + | registerProtocol(t, c, sub2) | |
| 167 | + | requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub2, child2, surf) | |
| 168 | + | requestProtocol(t, c, child2, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0)) | |
| 169 | + | requestProtocol(t, c, child2, wayland.SurfaceRequestCommit) | |
| 170 | + | rootCommit("second child appearing", 1) | |
| 171 | + | prev = fourth | |
| 172 | + | requestProtocol(t, c, sub2, wayland.SubsurfaceRequestDestroy) | |
| 173 | + | fourth = next() | |
| 174 | + | if got, ok := fourth.DamageSince(prev.Seq); ok || len(fourth.Children) != 0 { | |
| 175 | + | t.Fatalf("drawn child destroyed: damage %v %v, %d children", got, ok, len(fourth.Children)) | |
| 176 | + | } | |
| 127 | 177 | } | |
| @@ -124,4 +124,54 @@ func TestDamageHistory(t *testing.T) { | |||||
| 124 | if got, ok := fourth.DamageSince(third.Seq); !ok || len(got) != 1 || got[0] != (ports.Rect{X: 1, Y: 1, W: 2, H: 2}) { | 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) | 125 | t.Fatalf("empty subsurface: damage %v %v", got, ok) | ||
| 126 | } | 126 | } | ||
| 127 | + | // A synced child drawn, then hidden by a null attach the parent's | |||
| 128 | + | // commit applies: its old area must be redrawn, both times. | |||
| 129 | + | rootCommit := func(what string, wantChildren int) { | |||
| 130 | + | t.Helper() | |||
| 131 | + | prev := fourth | |||
| 132 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 133 | + | fourth = next() | |||
| 134 | + | if len(fourth.Children) != wantChildren { | |||
| 135 | + | t.Fatalf("%s: %d children, want %d", what, len(fourth.Children), wantChildren) | |||
| 136 | + | } | |||
| 137 | + | if got, ok := fourth.DamageSince(prev.Seq); ok { | |||
| 138 | + | t.Fatalf("%s kept partial damage %v", what, got) | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | requestProtocol(t, c, child, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0)) | |||
| 142 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |||
| 143 | + | rootCommit("child appearing", 1) | |||
| 144 | + | requestProtocol(t, c, child, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0)) | |||
| 145 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |||
| 146 | + | rootCommit("child hidden", 0) | |||
| 147 | + | // Destroying a child that draws nothing changes nothing on screen. | |||
| 148 | + | prev := fourth | |||
| 149 | + | requestProtocol(t, c, sub, wayland.SubsurfaceRequestDestroy) | |||
| 150 | + | fourth = next() | |||
| 151 | + | if got, ok := fourth.DamageSince(prev.Seq); !ok || len(got) != 0 { | |||
| 152 | + | t.Fatalf("empty child destroyed: damage %v %v", got, ok) | |||
| 153 | + | } | |||
| 154 | + | prev = fourth | |||
| 155 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |||
| 156 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(1), int32(1), int32(2), int32(2)) | |||
| 157 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 158 | + | fourth = next() | |||
| 159 | + | if got, ok := fourth.DamageSince(prev.Seq); !ok || len(got) != 1 { | |||
| 160 | + | t.Fatalf("after empty child: damage %v %v", got, ok) | |||
| 161 | + | } | |||
| 162 | + | // Destroying a drawn child redraws its area. | |||
| 163 | + | child2, sub2 := c.AllocateID(), c.AllocateID() | |||
| 164 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child2) | |||
| 165 | + | registerProtocol(t, c, child2) | |||
| 166 | + | registerProtocol(t, c, sub2) | |||
| 167 | + | requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub2, child2, surf) | |||
| 168 | + | requestProtocol(t, c, child2, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0)) | |||
| 169 | + | requestProtocol(t, c, child2, wayland.SurfaceRequestCommit) | |||
| 170 | + | rootCommit("second child appearing", 1) | |||
| 171 | + | prev = fourth | |||
| 172 | + | requestProtocol(t, c, sub2, wayland.SubsurfaceRequestDestroy) | |||
| 173 | + | fourth = next() | |||
| 174 | + | if got, ok := fourth.DamageSince(prev.Seq); ok || len(fourth.Children) != 0 { | |||
| 175 | + | t.Fatalf("drawn child destroyed: damage %v %v, %d children", got, ok, len(fourth.Children)) | |||
| 176 | + | } | |||
| 127 | } | 177 | } | ||
M internal/adapters/wayland/surface.go +8 -6
| @@ -98,6 +98,9 @@ type surface struct { | |||
| 98 | 98 | // committed is what the last commit changed, in buffer pixels (full: | |
| 99 | 99 | // everything), read by the window's damage history. | |
| 100 | 100 | committed damage | |
| 101 | + | // drewChildren is whether the root's last published tree drew a | |
| 102 | + | // subsurface: one that stops drawing leaves its area to redraw. | |
| 103 | + | drewChildren bool | |
| 101 | 104 | // Explicit sync (syncobj.go): the surface's syncobj object and the | |
| 102 | 105 | // current buffer's hold. | |
| 103 | 106 | sync *syncState | |
| @@ -252,17 +255,18 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) { | |||
| 252 | 255 | } | |
| 253 | 256 | ||
| 254 | 257 | // 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: | |
| 256 | - | // subsurfaces without a buffer (clients often keep one for later) are not | |
| 257 | - | // drawn, so they do not make it full. | |
| 258 | + | // of the root's own buffer, with no subsurface drawn before or now, keeps | |
| 259 | + | // partial damage: subsurfaces without a buffer (clients often keep one | |
| 260 | + | // for later) are not drawn, so they do not make it full. | |
| 258 | 261 | func (s *surface) redraw() { | |
| 259 | 262 | r := s.root() | |
| 260 | 263 | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | |
| 261 | 264 | c := r.tree(id) | |
| 262 | 265 | d := damage{full: true} | |
| 263 | - | if s == r && len(c.Children) == 0 { | |
| 266 | + | if s == r && len(c.Children) == 0 && !r.drewChildren { | |
| 264 | 267 | d = r.committed | |
| 265 | 268 | } | |
| 269 | + | r.drewChildren = len(c.Children) > 0 | |
| 266 | 270 | s.server.emitContent(c, d) | |
| 267 | 271 | } | |
| 268 | 272 | } | |
| @@ -289,8 +293,6 @@ func (s *surface) detach() { | |||
| 289 | 293 | // Parent updates cannot make a detached child's bound commits visible. | |
| 290 | 294 | s.dropQueue() | |
| 291 | 295 | s.flushDesync() | |
| 292 | - | // The child's area changes, not the parent's buffer: redraw it all. | |
| 293 | - | p.root().committed = damage{full: true} | |
| 294 | 296 | p.redraw() | |
| 295 | 297 | p.emitInput() | |
| 296 | 298 | s.sendScale() | |
| @@ -98,6 +98,9 @@ type surface struct { | |||||
| 98 | // committed is what the last commit changed, in buffer pixels (full: | 98 | // committed is what the last commit changed, in buffer pixels (full: | ||
| 99 | // everything), read by the window's damage history. | 99 | // everything), read by the window's damage history. | ||
| 100 | committed damage | 100 | committed damage | ||
| 101 | + | // drewChildren is whether the root's last published tree drew a | |||
| 102 | + | // subsurface: one that stops drawing leaves its area to redraw. | |||
| 103 | + | drewChildren bool | |||
| 101 | // Explicit sync (syncobj.go): the surface's syncobj object and the | 104 | // Explicit sync (syncobj.go): the surface's syncobj object and the | ||
| 102 | // current buffer's hold. | 105 | // current buffer's hold. | ||
| 103 | sync *syncState | 106 | sync *syncState | ||
| @@ -252,17 +255,18 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) { | |||||
| 252 | } | 255 | } | ||
| 253 | 256 | ||||
| 254 | // redraw sends the tree of a mapped root to the outputs. Only a commit | 257 | // 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: | 258 | + | // of the root's own buffer, with no subsurface drawn before or now, keeps |
| 256 | - | // subsurfaces without a buffer (clients often keep one for later) are not | 259 | + | // partial damage: subsurfaces without a buffer (clients often keep one |
| 257 | - | // drawn, so they do not make it full. | 260 | + | // for later) are not drawn, so they do not make it full. |
| 258 | func (s *surface) redraw() { | 261 | func (s *surface) redraw() { | ||
| 259 | r := s.root() | 262 | r := s.root() | ||
| 260 | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | 263 | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | ||
| 261 | c := r.tree(id) | 264 | c := r.tree(id) | ||
| 262 | d := damage{full: true} | 265 | d := damage{full: true} | ||
| 263 | - | if s == r && len(c.Children) == 0 { | 266 | + | if s == r && len(c.Children) == 0 && !r.drewChildren { |
| 264 | d = r.committed | 267 | d = r.committed | ||
| 265 | } | 268 | } | ||
| 269 | + | r.drewChildren = len(c.Children) > 0 | |||
| 266 | s.server.emitContent(c, d) | 270 | s.server.emitContent(c, d) | ||
| 267 | } | 271 | } | ||
| 268 | } | 272 | } | ||
| @@ -289,8 +293,6 @@ func (s *surface) detach() { | |||||
| 289 | // Parent updates cannot make a detached child's bound commits visible. | 293 | // Parent updates cannot make a detached child's bound commits visible. | ||
| 290 | s.dropQueue() | 294 | s.dropQueue() | ||
| 291 | s.flushDesync() | 295 | s.flushDesync() | ||
| 292 | - | // The child's area changes, not the parent's buffer: redraw it all. | |||
| 293 | - | p.root().committed = damage{full: true} | |||
| 294 | p.redraw() | 296 | p.redraw() | ||
| 295 | p.emitInput() | 297 | p.emitInput() | ||
| 296 | s.sendScale() | 298 | s.sendScale() | ||