detached 4478718

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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) {
561561 }
562562 }
563563 }
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
543543 return
544544 }
545545 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- }
550546 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+ }
559568 }
560569 dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeBuffer, content.Opaque)
561570 r.setContentColor(&dr, content)
@@ -543,19 +543,28 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon
543 return543 return
544 }544 }
545 b := content.SHM545 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.DamageSince550+ 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- return554+ }
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 {
178178 last uint64 // frame that drew it
179179 }
180180
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 {
187185 s := r.shm[key]
188186 if s == nil {
189- s = &shmSurface{}
190- r.shm[key] = s
187+ return nil
191188 }
192189 s.last = r.frame
193190 for _, c := range s.bufs {
194191 if c != nil && c.valid && c.holds.w == st.w && c.holds.h == st.h && c.holds.seq == st.seq {
195192 c.gpu.last = r.frame
196- return c, nil
193+ return c
197194 }
198195 }
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
199211 // The older buffer takes the new content.
200212 i := 0
201213 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 it178 last uint64 // frame that drew it
179 }179 }
180 180
181-// shmCopyFor returns the GPU buffer holding a surface's content st, copying181+// 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 no182+// 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 same183+// 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.frame189 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.frame192 c.gpu.last = r.frame
196- return c, nil193+ 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 := 0212 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) {

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 25

Tags 10

↑↓ moveenter openesc close