fix(vulkan): draw a kept shm window from its GPU copy when its pool is closed
- commit
- 44787184df64dcfca467aa220eee03f7652658f6
- author
- brice · · 2026-10-05 08:32 UTC
- committer
- brice · · 2026-10-05 08:32 UTC
- parent
- 5eede4a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email - refs
- feat/animations
3 files · +81 -21
M internal/adapters/vulkan/renderer_test.go +39 -0
| @@ -561,3 +561,42 @@ func TestRendererAlphaBlend(t *testing.T) { | |||
| 561 | 561 | } | |
| 562 | 562 | } | |
| 563 | 563 | } | |
| 564 | + | ||
| 565 | + | // A window kept after its client closed (a closing window fading out) still | |
| 566 | + | // draws from the GPU copy of its content: the pool file is closed and its | |
| 567 | + | // mapping gone, and the copy is found without reading the pool. | |
| 568 | + | func TestRendererSHMKeptWindowDrawsFromCopy(t *testing.T) { | |
| 569 | + | r, err := New(32, 32) | |
| 570 | + | if err != nil { | |
| 571 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 572 | + | } | |
| 573 | + | defer r.Close() | |
| 574 | + | scene := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}} | |
| 575 | + | c := solidContent(t, 16, 16, color.RGBA{R: 255, A: 255}) | |
| 576 | + | c.ID, c.Surface, c.Seq, c.Version = 1, 1, 1, 1 | |
| 577 | + | if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil { | |
| 578 | + | t.Fatal(err) | |
| 579 | + | } | |
| 580 | + | // Wayland closed the pool; the mapping was dropped meanwhile. | |
| 581 | + | if err := c.SHM.File.Close(); err != nil { | |
| 582 | + | t.Fatal(err) | |
| 583 | + | } | |
| 584 | + | r.dropPools() | |
| 585 | + | for pool, m := range r.pools { | |
| 586 | + | _ = unix.Munmap(m.data) | |
| 587 | + | delete(r.pools, pool) | |
| 588 | + | } | |
| 589 | + | // The window moves (a fade-out), so it draws again. | |
| 590 | + | scene.Seq = 2 | |
| 591 | + | scene.Windows[0].Rect.X = 4 | |
| 592 | + | if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil { | |
| 593 | + | t.Fatal(err) | |
| 594 | + | } | |
| 595 | + | got := readPixels(t, r) | |
| 596 | + | if g, want := got.At(18, 8), (color.RGBA{R: 255, A: 255}); g != want { | |
| 597 | + | t.Fatalf("kept window pixel %v, want %v", g, want) | |
| 598 | + | } | |
| 599 | + | if g, want := got.At(1, 8), (color.RGBA{A: 255}); g != want { | |
| 600 | + | t.Fatalf("background pixel %v, want %v", g, want) | |
| 601 | + | } | |
| 602 | + | } | |
| @@ -561,3 +561,42 @@ func TestRendererAlphaBlend(t *testing.T) { | |||||
| 561 | } | 561 | } | ||
| 562 | } | 562 | } | ||
| 563 | } | 563 | } | ||
| 564 | + | ||||
| 565 | + | // A window kept after its client closed (a closing window fading out) still | |||
| 566 | + | // draws from the GPU copy of its content: the pool file is closed and its | |||
| 567 | + | // mapping gone, and the copy is found without reading the pool. | |||
| 568 | + | func TestRendererSHMKeptWindowDrawsFromCopy(t *testing.T) { | |||
| 569 | + | r, err := New(32, 32) | |||
| 570 | + | if err != nil { | |||
| 571 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 572 | + | } | |||
| 573 | + | defer r.Close() | |||
| 574 | + | scene := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}} | |||
| 575 | + | c := solidContent(t, 16, 16, color.RGBA{R: 255, A: 255}) | |||
| 576 | + | c.ID, c.Surface, c.Seq, c.Version = 1, 1, 1, 1 | |||
| 577 | + | if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil { | |||
| 578 | + | t.Fatal(err) | |||
| 579 | + | } | |||
| 580 | + | // Wayland closed the pool; the mapping was dropped meanwhile. | |||
| 581 | + | if err := c.SHM.File.Close(); err != nil { | |||
| 582 | + | t.Fatal(err) | |||
| 583 | + | } | |||
| 584 | + | r.dropPools() | |||
| 585 | + | for pool, m := range r.pools { | |||
| 586 | + | _ = unix.Munmap(m.data) | |||
| 587 | + | delete(r.pools, pool) | |||
| 588 | + | } | |||
| 589 | + | // The window moves (a fade-out), so it draws again. | |||
| 590 | + | scene.Seq = 2 | |||
| 591 | + | scene.Windows[0].Rect.X = 4 | |||
| 592 | + | if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil { | |||
| 593 | + | t.Fatal(err) | |||
| 594 | + | } | |||
| 595 | + | got := readPixels(t, r) | |||
| 596 | + | if g, want := got.At(18, 8), (color.RGBA{R: 255, A: 255}); g != want { | |||
| 597 | + | t.Fatalf("kept window pixel %v, want %v", g, want) | |||
| 598 | + | } | |||
| 599 | + | if g, want := got.At(1, 8), (color.RGBA{A: 255}); g != want { | |||
| 600 | + | t.Fatalf("background pixel %v, want %v", g, want) | |||
| 601 | + | } | |||
| 602 | + | } | |||
M internal/adapters/vulkan/scene.go +21 -12
| @@ -543,19 +543,28 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon | |||
| 543 | 543 | return | |
| 544 | 544 | } | |
| 545 | 545 | b := content.SHM | |
| 546 | - | pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4) | |
| 547 | - | if err != nil { | |
| 548 | - | return | |
| 549 | - | } | |
| 550 | 546 | st := shmState{w: content.Width, h: content.Height, seq: seq, windowSeq: content.Seq} | |
| 551 | - | // Damage history is the root surface's: children copy in full. | |
| 552 | - | var damage func(uint64) ([]ports.Rect, bool) | |
| 553 | - | if root { | |
| 554 | - | damage = content.DamageSince | |
| 555 | - | } | |
| 556 | - | c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage) | |
| 557 | - | if err != nil { | |
| 558 | - | return | |
| 547 | + | // The copy of this content, if any, needs no pool: a kept (closing) | |
| 548 | + | // window whose pool file is closed still draws. | |
| 549 | + | c := r.shmCached(key, st) | |
| 550 | + | if c != nil { | |
| 551 | + | // As a drawn pool, for Trim; nothing is mapped for it. | |
| 552 | + | if m := r.pools[b.Pool]; m != nil { | |
| 553 | + | m.last = r.frame | |
| 554 | + | } | |
| 555 | + | } else { | |
| 556 | + | pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4) | |
| 557 | + | if err != nil { | |
| 558 | + | return | |
| 559 | + | } | |
| 560 | + | // Damage history is the root surface's: children copy in full. | |
| 561 | + | var damage func(uint64) ([]ports.Rect, bool) | |
| 562 | + | if root { | |
| 563 | + | damage = content.DamageSince | |
| 564 | + | } | |
| 565 | + | if c, err = r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage); err != nil { | |
| 566 | + | return | |
| 567 | + | } | |
| 559 | 568 | } | |
| 560 | 569 | dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeBuffer, content.Opaque) | |
| 561 | 570 | r.setContentColor(&dr, content) | |
| @@ -543,19 +543,28 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon | |||||
| 543 | return | 543 | return | ||
| 544 | } | 544 | } | ||
| 545 | b := content.SHM | 545 | b := content.SHM | ||
| 546 | - | pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4) | |||
| 547 | - | if err != nil { | |||
| 548 | - | return | |||
| 549 | - | } | |||
| 550 | st := shmState{w: content.Width, h: content.Height, seq: seq, windowSeq: content.Seq} | 546 | st := shmState{w: content.Width, h: content.Height, seq: seq, windowSeq: content.Seq} | ||
| 551 | - | // Damage history is the root surface's: children copy in full. | 547 | + | // The copy of this content, if any, needs no pool: a kept (closing) |
| 552 | - | var damage func(uint64) ([]ports.Rect, bool) | 548 | + | // window whose pool file is closed still draws. |
| 553 | - | if root { | 549 | + | c := r.shmCached(key, st) |
| 554 | - | damage = content.DamageSince | 550 | + | if c != nil { |
| 555 | - | } | 551 | + | // As a drawn pool, for Trim; nothing is mapped for it. |
| 556 | - | c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage) | 552 | + | if m := r.pools[b.Pool]; m != nil { |
| 557 | - | if err != nil { | 553 | + | m.last = r.frame |
| 558 | - | return | 554 | + | } |
| 555 | + | } else { | |||
| 556 | + | pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4) | |||
| 557 | + | if err != nil { | |||
| 558 | + | return | |||
| 559 | + | } | |||
| 560 | + | // Damage history is the root surface's: children copy in full. | |||
| 561 | + | var damage func(uint64) ([]ports.Rect, bool) | |||
| 562 | + | if root { | |||
| 563 | + | damage = content.DamageSince | |||
| 564 | + | } | |||
| 565 | + | if c, err = r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage); err != nil { | |||
| 566 | + | return | |||
| 567 | + | } | |||
| 559 | } | 568 | } | ||
| 560 | dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeBuffer, content.Opaque) | 569 | dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeBuffer, content.Opaque) | ||
| 561 | r.setContentColor(&dr, content) | 570 | r.setContentColor(&dr, content) | ||
M internal/adapters/vulkan/shm.go +21 -9
| @@ -178,24 +178,36 @@ type shmSurface struct { | |||
| 178 | 178 | last uint64 // frame that drew it | |
| 179 | 179 | } | |
| 180 | 180 | ||
| 181 | - | // shmCopyFor returns the GPU buffer holding a surface's content st, copying | |
| 182 | - | // the client pixels (stride bytes per row from offset) into one when no | |
| 183 | - | // buffer holds them yet. A buffer holding an older content of the same | |
| 184 | - | // size gets only what changed since (damage, in buffer pixels; false: | |
| 185 | - | // everything). | |
| 186 | - | func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, stride int, damage func(since uint64) ([]ports.Rect, bool)) (*shmCopy, error) { | |
| 181 | + | // shmCached returns the GPU buffer already holding a surface's content st, | |
| 182 | + | // or nil. It needs no client pixels: a kept window whose pool is closed | |
| 183 | + | // still draws from it. | |
| 184 | + | func (r *Renderer) shmCached(key shmKey, st shmState) *shmCopy { | |
| 187 | 185 | s := r.shm[key] | |
| 188 | 186 | if s == nil { | |
| 189 | - | s = &shmSurface{} | |
| 190 | - | r.shm[key] = s | |
| 187 | + | return nil | |
| 191 | 188 | } | |
| 192 | 189 | s.last = r.frame | |
| 193 | 190 | for _, c := range s.bufs { | |
| 194 | 191 | if c != nil && c.valid && c.holds.w == st.w && c.holds.h == st.h && c.holds.seq == st.seq { | |
| 195 | 192 | c.gpu.last = r.frame | |
| 196 | - | return c, nil | |
| 193 | + | return c | |
| 197 | 194 | } | |
| 198 | 195 | } | |
| 196 | + | return nil | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | // shmCopyFor copies a surface's content st from the client pixels (stride | |
| 200 | + | // bytes per row from offset) into a GPU buffer; the caller found none | |
| 201 | + | // holding it (shmCached). A buffer holding an older content of the same | |
| 202 | + | // size gets only what changed since (damage, in buffer pixels; false: | |
| 203 | + | // everything). | |
| 204 | + | func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, stride int, damage func(since uint64) ([]ports.Rect, bool)) (*shmCopy, error) { | |
| 205 | + | s := r.shm[key] | |
| 206 | + | if s == nil { | |
| 207 | + | s = &shmSurface{} | |
| 208 | + | r.shm[key] = s | |
| 209 | + | } | |
| 210 | + | s.last = r.frame | |
| 199 | 211 | // The older buffer takes the new content. | |
| 200 | 212 | i := 0 | |
| 201 | 213 | if s.bufs[0] != nil && (s.bufs[1] == nil || s.bufs[1].gpu.last < s.bufs[0].gpu.last) { | |
| @@ -178,24 +178,36 @@ type shmSurface struct { | |||||
| 178 | last uint64 // frame that drew it | 178 | last uint64 // frame that drew it | ||
| 179 | } | 179 | } | ||
| 180 | 180 | ||||
| 181 | - | // shmCopyFor returns the GPU buffer holding a surface's content st, copying | 181 | + | // shmCached returns the GPU buffer already holding a surface's content st, |
| 182 | - | // the client pixels (stride bytes per row from offset) into one when no | 182 | + | // or nil. It needs no client pixels: a kept window whose pool is closed |
| 183 | - | // buffer holds them yet. A buffer holding an older content of the same | 183 | + | // still draws from it. |
| 184 | - | // size gets only what changed since (damage, in buffer pixels; false: | 184 | + | func (r *Renderer) shmCached(key shmKey, st shmState) *shmCopy { |
| 185 | - | // everything). | |||
| 186 | - | func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, stride int, damage func(since uint64) ([]ports.Rect, bool)) (*shmCopy, error) { | |||
| 187 | s := r.shm[key] | 185 | s := r.shm[key] | ||
| 188 | if s == nil { | 186 | if s == nil { | ||
| 189 | - | s = &shmSurface{} | 187 | + | return nil |
| 190 | - | r.shm[key] = s | |||
| 191 | } | 188 | } | ||
| 192 | s.last = r.frame | 189 | s.last = r.frame | ||
| 193 | for _, c := range s.bufs { | 190 | for _, c := range s.bufs { | ||
| 194 | if c != nil && c.valid && c.holds.w == st.w && c.holds.h == st.h && c.holds.seq == st.seq { | 191 | if c != nil && c.valid && c.holds.w == st.w && c.holds.h == st.h && c.holds.seq == st.seq { | ||
| 195 | c.gpu.last = r.frame | 192 | c.gpu.last = r.frame | ||
| 196 | - | return c, nil | 193 | + | return c |
| 197 | } | 194 | } | ||
| 198 | } | 195 | } | ||
| 196 | + | return nil | |||
| 197 | + | } | |||
| 198 | + | ||||
| 199 | + | // shmCopyFor copies a surface's content st from the client pixels (stride | |||
| 200 | + | // bytes per row from offset) into a GPU buffer; the caller found none | |||
| 201 | + | // holding it (shmCached). A buffer holding an older content of the same | |||
| 202 | + | // size gets only what changed since (damage, in buffer pixels; false: | |||
| 203 | + | // everything). | |||
| 204 | + | func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, stride int, damage func(since uint64) ([]ports.Rect, bool)) (*shmCopy, error) { | |||
| 205 | + | s := r.shm[key] | |||
| 206 | + | if s == nil { | |||
| 207 | + | s = &shmSurface{} | |||
| 208 | + | r.shm[key] = s | |||
| 209 | + | } | |||
| 210 | + | s.last = r.frame | |||
| 199 | // The older buffer takes the new content. | 211 | // The older buffer takes the new content. | ||
| 200 | i := 0 | 212 | i := 0 | ||
| 201 | if s.bufs[0] != nil && (s.bufs[1] == nil || s.bufs[1].gpu.last < s.bufs[0].gpu.last) { | 213 | if s.bufs[0] != nil && (s.bufs[1] == nil || s.bufs[1].gpu.last < s.bufs[0].gpu.last) { | ||