detached 086975e

merge: perf-memory (shm read in place, GC tuning)

commit
086975eafe575e02d02db417da2e62e1b0ae0d5a
author
brice · · 2026-09-25 08:57 UTC
merge of
ffa8da1 + 99c725d · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
13 files · +369 -147
M cmd/nefertty/main.go +18 -1
@@ -23,7 +23,24 @@ import (
2323
2424 type usageError struct{ error }
2525
26-func main() { os.Exit(runCode()) }
26+func main() {
27+ tuneGC()
28+ os.Exit(runCode())
29+}
30+
31+// tuneGC keeps the heap close to what nefertty uses: a compositor runs for
32+// days and its per-frame garbage is small, so the default GOGC=100 (heap
33+// grows to twice the live data) mostly holds freed memory. A soft limit
34+// makes the collector work harder before the process gets large. GOGC and
35+// GOMEMLIMIT set in the environment win.
36+func tuneGC() {
37+ if os.Getenv("GOGC") == "" {
38+ debug.SetGCPercent(50)
39+ }
40+ if os.Getenv("GOMEMLIMIT") == "" {
41+ debug.SetMemoryLimit(256 << 20)
42+ }
43+}
2744 func runCode() int {
2845 err := run()
2946 if err == nil || errors.Is(err, flag.ErrHelp) || errors.Is(err, context.Canceled) {
@@ -23,7 +23,24 @@ import (
23 23
24 type usageError struct{ error }24 type usageError struct{ error }
25 25
26-func main() { os.Exit(runCode()) }26+func main() {
27+ tuneGC()
28+ os.Exit(runCode())
29+}
30+
31+// tuneGC keeps the heap close to what nefertty uses: a compositor runs for
32+// days and its per-frame garbage is small, so the default GOGC=100 (heap
33+// grows to twice the live data) mostly holds freed memory. A soft limit
34+// makes the collector work harder before the process gets large. GOGC and
35+// GOMEMLIMIT set in the environment win.
36+func tuneGC() {
37+ if os.Getenv("GOGC") == "" {
38+ debug.SetGCPercent(50)
39+ }
40+ if os.Getenv("GOMEMLIMIT") == "" {
41+ debug.SetMemoryLimit(256 << 20)
42+ }
43+}
27 func runCode() int {44 func runCode() int {
28 err := run()45 err := run()
29 if err == nil || errors.Is(err, flag.ErrHelp) || errors.Is(err, context.Canceled) {46 if err == nil || errors.Is(err, flag.ErrHelp) || errors.Is(err, context.Canceled) {
M internal/adapters/headless/output_test.go +4 -4
@@ -68,10 +68,10 @@ func TestRun(t *testing.T) {
6868 scenes := make(chan ports.Scene, 8)
6969 contents := make(chan ports.SurfaceContent, 8)
7070 r, f := recordingRenderer(t, nil)
71- contents <- ports.SurfaceContent{ID: 1, Pixels: []byte{1}}
71+ contents <- ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}}
7272 scenes <- ports.Scene{Seq: 1}
7373 scenes <- ports.Scene{Seq: 2}
74- contents <- ports.SurfaceContent{ID: 2, Pixels: []byte{2}}
74+ contents <- ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}}
7575 ctx, cancel := context.WithCancel(context.Background())
7676 done := make(chan error, 1)
7777 go func() {
@@ -149,7 +149,7 @@ func TestRunPassesLayerContent(t *testing.T) {
149149 contents := make(chan ports.SurfaceContent, 1)
150150 layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}}
151151 scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}}
152- contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 255, 255}}
152+ contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, SHM: &ports.SHMBuffer{Pool: 42, Stride: 4}}
153153 r, f := recordingRenderer(t, nil)
154154 ctx, cancel := context.WithCancel(context.Background())
155155 done := make(chan error, 1)
@@ -159,7 +159,7 @@ func TestRunPassesLayerContent(t *testing.T) {
159159 deadline := time.After(3 * time.Second)
160160 for {
161161 s, c := f.snapshot()
162- if len(c) > 0 && len(s[0].Layers) == 1 && c[0][42].Pixels != nil {
162+ if len(c) > 0 && len(s[0].Layers) == 1 && c[0][42].SHM != nil {
163163 break
164164 }
165165 select {
@@ -68,10 +68,10 @@ func TestRun(t *testing.T) {
68 scenes := make(chan ports.Scene, 8)68 scenes := make(chan ports.Scene, 8)
69 contents := make(chan ports.SurfaceContent, 8)69 contents := make(chan ports.SurfaceContent, 8)
70 r, f := recordingRenderer(t, nil)70 r, f := recordingRenderer(t, nil)
71- contents <- ports.SurfaceContent{ID: 1, Pixels: []byte{1}}71+ contents <- ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{Pool: 1}}
72 scenes <- ports.Scene{Seq: 1}72 scenes <- ports.Scene{Seq: 1}
73 scenes <- ports.Scene{Seq: 2}73 scenes <- ports.Scene{Seq: 2}
74- contents <- ports.SurfaceContent{ID: 2, Pixels: []byte{2}}74+ contents <- ports.SurfaceContent{ID: 2, SHM: &ports.SHMBuffer{Pool: 2}}
75 ctx, cancel := context.WithCancel(context.Background())75 ctx, cancel := context.WithCancel(context.Background())
76 done := make(chan error, 1)76 done := make(chan error, 1)
77 go func() {77 go func() {
@@ -149,7 +149,7 @@ func TestRunPassesLayerContent(t *testing.T) {
149 contents := make(chan ports.SurfaceContent, 1)149 contents := make(chan ports.SurfaceContent, 1)
150 layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}}150 layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}}
151 scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}}151 scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}}
152- contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 255, 255}}152+ contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, SHM: &ports.SHMBuffer{Pool: 42, Stride: 4}}
153 r, f := recordingRenderer(t, nil)153 r, f := recordingRenderer(t, nil)
154 ctx, cancel := context.WithCancel(context.Background())154 ctx, cancel := context.WithCancel(context.Background())
155 done := make(chan error, 1)155 done := make(chan error, 1)
@@ -159,7 +159,7 @@ func TestRunPassesLayerContent(t *testing.T) {
159 deadline := time.After(3 * time.Second)159 deadline := time.After(3 * time.Second)
160 for {160 for {
161 s, c := f.snapshot()161 s, c := f.snapshot()
162- if len(c) > 0 && len(s[0].Layers) == 1 && c[0][42].Pixels != nil {162+ if len(c) > 0 && len(s[0].Layers) == 1 && c[0][42].SHM != nil {
163 break163 break
164 }164 }
165 select {165 select {
M internal/adapters/vulkan/renderer.go +63 -40
@@ -41,6 +41,10 @@ type Renderer struct {
4141 // dmabuf is what clients may send; imports are their buffers by ID.
4242 dmabuf ports.DMABufSupport
4343 imports map[uint64]*imported
44+ // pools are client wl_shm pools mapped by pool ID.
45+ pools map[uint64]*mapping
46+ // retired are pool mappings replaced this frame, unmapped after staging.
47+ retired [][]byte
4448 frame uint64
4549 // fillRegions is reused by Render for the bands of solid fills.
4650 fillRegions []vk.BufferImageCopy
@@ -61,7 +65,7 @@ func New(width, height int) (r *Renderer, err error) {
6165 if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {
6266 return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)
6367 }
64- r = &Renderer{width: width, height: height, layout: vk.ImageLayoutUndefined, imports: map[uint64]*imported{}}
68+ r = &Renderer{width: width, height: height, layout: vk.ImageLayoutUndefined, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}}
6569 defer func() {
6670 if err != nil {
6771 r.Close()
@@ -346,6 +350,50 @@ func (r *Renderer) Clear(rgb [3]uint8) error {
346350 return r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil)
347351 }
348352
353+// upload is one piece of a frame: a solid fill, client pixels staged from
354+// a wl_shm pool, or a dmabuf blitted in place.
355+type upload struct {
356+ rect image.Rectangle
357+ offset int
358+ pixels []byte
359+ stride int
360+ // dst covers src; they differ in size when the buffer is not drawn at
361+ // the physical size (integer-scale clients, rounding).
362+ dst, src image.Rectangle
363+ color [3]uint8
364+ opaque bool
365+ // dma is a client GPU buffer, blitted instead of copied from staging.
366+ dma *imported
367+ // fill is a solid color: staging holds one strip of rows.
368+ fill bool
369+}
370+
371+// stage copies the visible pixels of a client buffer into dst, resampling
372+// when the buffer is not drawn at its size.
373+func (u *upload) stage(dst []byte) {
374+ for row := 0; row < u.rect.Dy(); row++ {
375+ line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]
376+ y := u.rect.Min.Y + row
377+ if u.dst.Size() == u.src.Size() {
378+ start := (u.src.Min.Y+y-u.dst.Min.Y)*u.stride + (u.src.Min.X+u.rect.Min.X-u.dst.Min.X)*4
379+ copy(line, u.pixels[start:start+len(line)])
380+ } else {
381+ // Nearest-neighbour resample, sampling pixel centres.
382+ sy := u.src.Min.Y + ((y-u.dst.Min.Y)*2+1)*u.src.Dy()/(u.dst.Dy()*2)
383+ src := u.pixels[sy*u.stride:]
384+ for i := 0; i < u.rect.Dx(); i++ {
385+ sx := u.src.Min.X + ((u.rect.Min.X+i-u.dst.Min.X)*2+1)*u.src.Dx()/(u.dst.Dx()*2)
386+ copy(line[i*4:i*4+4], src[sx*4:sx*4+4])
387+ }
388+ }
389+ if u.opaque {
390+ for i := 3; i < len(line); i += 4 {
391+ line[i] = 255
392+ }
393+ }
394+ }
395+}
396+
349397 func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
350398 // Scene rects are logical; everything below works in physical pixels.
351399 scale := s.Scale
@@ -354,21 +402,6 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
354402 }
355403 phys := func(v int) int { return int(math.Round(float64(v) * scale)) }
356404 physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) }
357- type upload struct {
358- rect image.Rectangle
359- offset int
360- pixels []byte
361- stride int
362- // dst covers src; they differ in size when the buffer is not drawn at
363- // the physical size (integer-scale clients, rounding).
364- dst, src image.Rectangle
365- color [3]uint8
366- opaque bool
367- // dma is a client GPU buffer, blitted instead of copied from staging.
368- dma *imported
369- // fill is a solid color: staging holds one strip of rows.
370- fill bool
371- }
372405 var uploads []upload
373406 used := 0
374407 bounds := image.Rect(0, 0, r.width, r.height)
@@ -397,7 +430,12 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
397430 uploads = append(uploads, upload{rect: rect, dst: dst, src: src, dma: im})
398431 return
399432 }
400- uploads = append(uploads, upload{rect: rect, offset: used, pixels: content.Pixels, stride: content.Stride, opaque: content.Opaque, dst: dst, src: src})
433+ b := content.SHM
434+ pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4)
435+ if err != nil {
436+ return
437+ }
438+ uploads = append(uploads, upload{rect: rect, offset: used, pixels: pixels[b.Offset:], stride: b.Stride, opaque: content.Opaque, dst: dst, src: src})
401439 used += rect.Dx() * rect.Dy() * 4
402440 }
403441 // drawSurface draws one surface buffer with its origin at (x, y)
@@ -407,10 +445,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
407445 if lw <= 0 || lh <= 0 {
408446 lw, lh = content.Width, content.Height
409447 }
410- if content.Pixels == nil && content.DMABuf == nil || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {
448+ if content.SHM == nil && content.DMABuf == nil || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {
411449 return
412450 }
413- if content.DMABuf == nil && (content.Stride < content.Width*4 || len(content.Pixels) < (content.Height-1)*content.Stride+content.Width*4) {
451+ if content.DMABuf == nil && (content.SHM.Stride < content.Width*4 || content.SHM.Offset < 0) {
414452 return
415453 }
416454 full := physRect(x, y, lw, lh)
@@ -516,27 +554,9 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
516554 continue
517555 }
518556 dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]
519- for row := 0; row < u.rect.Dy(); row++ {
520- line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]
521- y := u.rect.Min.Y + row
522- if u.dst.Size() == u.src.Size() {
523- start := (u.src.Min.Y+y-u.dst.Min.Y)*u.stride + (u.src.Min.X+u.rect.Min.X-u.dst.Min.X)*4
524- copy(line, u.pixels[start:start+len(line)])
525- } else {
526- // Nearest-neighbour resample, sampling pixel centres.
527- sy := u.src.Min.Y + ((y-u.dst.Min.Y)*2+1)*u.src.Dy()/(u.dst.Dy()*2)
528- src := u.pixels[sy*u.stride:]
529- for i := 0; i < u.rect.Dx(); i++ {
530- sx := u.src.Min.X + ((u.rect.Min.X+i-u.dst.Min.X)*2+1)*u.src.Dx()/(u.dst.Dx()*2)
531- copy(line[i*4:i*4+4], src[sx*4:sx*4+4])
532- }
533- }
534- if u.opaque {
535- for i := 3; i < len(line); i += 4 {
536- line[i] = 255
537- }
538- }
539- }
557+ // The client may shrink its pool under the mapping: its window
558+ // then shows garbage for this frame, never a crash.
559+ copyGuarded(func() { u.stage(dst) })
540560 }
541561 // B8G8R8A8 pixels are copied unchanged; blending belongs to the compositing pipeline.
542562 rgb := parseColor(s.Background)
@@ -663,6 +683,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
663683 return err
664684 }
665685 r.dropUnused()
686+ r.dropPools()
666687 return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
667688 }
668689
@@ -705,6 +726,8 @@ func (r *Renderer) Close() {
705726 r.release(im)
706727 delete(r.imports, id)
707728 }
729+ r.frame += importTTL + 1
730+ r.dropPools()
708731 if r.fence != 0 {
709732 d.DestroyFence(r.device, r.fence, nil)
710733 r.fence = 0
@@ -41,6 +41,10 @@ type Renderer struct {
41 // dmabuf is what clients may send; imports are their buffers by ID.41 // dmabuf is what clients may send; imports are their buffers by ID.
42 dmabuf ports.DMABufSupport42 dmabuf ports.DMABufSupport
43 imports map[uint64]*imported43 imports map[uint64]*imported
44+ // pools are client wl_shm pools mapped by pool ID.
45+ pools map[uint64]*mapping
46+ // retired are pool mappings replaced this frame, unmapped after staging.
47+ retired [][]byte
44 frame uint6448 frame uint64
45 // fillRegions is reused by Render for the bands of solid fills.49 // fillRegions is reused by Render for the bands of solid fills.
46 fillRegions []vk.BufferImageCopy50 fillRegions []vk.BufferImageCopy
@@ -61,7 +65,7 @@ func New(width, height int) (r *Renderer, err error) {
61 if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {65 if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {
62 return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)66 return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)
63 }67 }
64- r = &Renderer{width: width, height: height, layout: vk.ImageLayoutUndefined, imports: map[uint64]*imported{}}68+ r = &Renderer{width: width, height: height, layout: vk.ImageLayoutUndefined, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}}
65 defer func() {69 defer func() {
66 if err != nil {70 if err != nil {
67 r.Close()71 r.Close()
@@ -346,6 +350,50 @@ func (r *Renderer) Clear(rgb [3]uint8) error {
346 return r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil)350 return r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil)
347 }351 }
348 352
353+// upload is one piece of a frame: a solid fill, client pixels staged from
354+// a wl_shm pool, or a dmabuf blitted in place.
355+type upload struct {
356+ rect image.Rectangle
357+ offset int
358+ pixels []byte
359+ stride int
360+ // dst covers src; they differ in size when the buffer is not drawn at
361+ // the physical size (integer-scale clients, rounding).
362+ dst, src image.Rectangle
363+ color [3]uint8
364+ opaque bool
365+ // dma is a client GPU buffer, blitted instead of copied from staging.
366+ dma *imported
367+ // fill is a solid color: staging holds one strip of rows.
368+ fill bool
369+}
370+
371+// stage copies the visible pixels of a client buffer into dst, resampling
372+// when the buffer is not drawn at its size.
373+func (u *upload) stage(dst []byte) {
374+ for row := 0; row < u.rect.Dy(); row++ {
375+ line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]
376+ y := u.rect.Min.Y + row
377+ if u.dst.Size() == u.src.Size() {
378+ start := (u.src.Min.Y+y-u.dst.Min.Y)*u.stride + (u.src.Min.X+u.rect.Min.X-u.dst.Min.X)*4
379+ copy(line, u.pixels[start:start+len(line)])
380+ } else {
381+ // Nearest-neighbour resample, sampling pixel centres.
382+ sy := u.src.Min.Y + ((y-u.dst.Min.Y)*2+1)*u.src.Dy()/(u.dst.Dy()*2)
383+ src := u.pixels[sy*u.stride:]
384+ for i := 0; i < u.rect.Dx(); i++ {
385+ sx := u.src.Min.X + ((u.rect.Min.X+i-u.dst.Min.X)*2+1)*u.src.Dx()/(u.dst.Dx()*2)
386+ copy(line[i*4:i*4+4], src[sx*4:sx*4+4])
387+ }
388+ }
389+ if u.opaque {
390+ for i := 3; i < len(line); i += 4 {
391+ line[i] = 255
392+ }
393+ }
394+ }
395+}
396+
349 func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {397 func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
350 // Scene rects are logical; everything below works in physical pixels.398 // Scene rects are logical; everything below works in physical pixels.
351 scale := s.Scale399 scale := s.Scale
@@ -354,21 +402,6 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
354 }402 }
355 phys := func(v int) int { return int(math.Round(float64(v) * scale)) }403 phys := func(v int) int { return int(math.Round(float64(v) * scale)) }
356 physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) }404 physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) }
357- type upload struct {
358- rect image.Rectangle
359- offset int
360- pixels []byte
361- stride int
362- // dst covers src; they differ in size when the buffer is not drawn at
363- // the physical size (integer-scale clients, rounding).
364- dst, src image.Rectangle
365- color [3]uint8
366- opaque bool
367- // dma is a client GPU buffer, blitted instead of copied from staging.
368- dma *imported
369- // fill is a solid color: staging holds one strip of rows.
370- fill bool
371- }
372 var uploads []upload405 var uploads []upload
373 used := 0406 used := 0
374 bounds := image.Rect(0, 0, r.width, r.height)407 bounds := image.Rect(0, 0, r.width, r.height)
@@ -397,7 +430,12 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
397 uploads = append(uploads, upload{rect: rect, dst: dst, src: src, dma: im})430 uploads = append(uploads, upload{rect: rect, dst: dst, src: src, dma: im})
398 return431 return
399 }432 }
400- uploads = append(uploads, upload{rect: rect, offset: used, pixels: content.Pixels, stride: content.Stride, opaque: content.Opaque, dst: dst, src: src})433+ b := content.SHM
434+ pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4)
435+ if err != nil {
436+ return
437+ }
438+ uploads = append(uploads, upload{rect: rect, offset: used, pixels: pixels[b.Offset:], stride: b.Stride, opaque: content.Opaque, dst: dst, src: src})
401 used += rect.Dx() * rect.Dy() * 4439 used += rect.Dx() * rect.Dy() * 4
402 }440 }
403 // drawSurface draws one surface buffer with its origin at (x, y)441 // drawSurface draws one surface buffer with its origin at (x, y)
@@ -407,10 +445,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
407 if lw <= 0 || lh <= 0 {445 if lw <= 0 || lh <= 0 {
408 lw, lh = content.Width, content.Height446 lw, lh = content.Width, content.Height
409 }447 }
410- if content.Pixels == nil && content.DMABuf == nil || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {448+ if content.SHM == nil && content.DMABuf == nil || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {
411 return449 return
412 }450 }
413- if content.DMABuf == nil && (content.Stride < content.Width*4 || len(content.Pixels) < (content.Height-1)*content.Stride+content.Width*4) {451+ if content.DMABuf == nil && (content.SHM.Stride < content.Width*4 || content.SHM.Offset < 0) {
414 return452 return
415 }453 }
416 full := physRect(x, y, lw, lh)454 full := physRect(x, y, lw, lh)
@@ -516,27 +554,9 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
516 continue554 continue
517 }555 }
518 dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]556 dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]
519- for row := 0; row < u.rect.Dy(); row++ {557+ // The client may shrink its pool under the mapping: its window
520- line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]558+ // then shows garbage for this frame, never a crash.
521- y := u.rect.Min.Y + row559+ copyGuarded(func() { u.stage(dst) })
522- if u.dst.Size() == u.src.Size() {
523- start := (u.src.Min.Y+y-u.dst.Min.Y)*u.stride + (u.src.Min.X+u.rect.Min.X-u.dst.Min.X)*4
524- copy(line, u.pixels[start:start+len(line)])
525- } else {
526- // Nearest-neighbour resample, sampling pixel centres.
527- sy := u.src.Min.Y + ((y-u.dst.Min.Y)*2+1)*u.src.Dy()/(u.dst.Dy()*2)
528- src := u.pixels[sy*u.stride:]
529- for i := 0; i < u.rect.Dx(); i++ {
530- sx := u.src.Min.X + ((u.rect.Min.X+i-u.dst.Min.X)*2+1)*u.src.Dx()/(u.dst.Dx()*2)
531- copy(line[i*4:i*4+4], src[sx*4:sx*4+4])
532- }
533- }
534- if u.opaque {
535- for i := 3; i < len(line); i += 4 {
536- line[i] = 255
537- }
538- }
539- }
540 }560 }
541 // B8G8R8A8 pixels are copied unchanged; blending belongs to the compositing pipeline.561 // B8G8R8A8 pixels are copied unchanged; blending belongs to the compositing pipeline.
542 rgb := parseColor(s.Background)562 rgb := parseColor(s.Background)
@@ -663,6 +683,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
663 return err683 return err
664 }684 }
665 r.dropUnused()685 r.dropUnused()
686+ r.dropPools()
666 return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))687 return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
667 }688 }
668 689
@@ -705,6 +726,8 @@ func (r *Renderer) Close() {
705 r.release(im)726 r.release(im)
706 delete(r.imports, id)727 delete(r.imports, id)
707 }728 }
729+ r.frame += importTTL + 1
730+ r.dropPools()
708 if r.fence != 0 {731 if r.fence != 0 {
709 d.DestroyFence(r.device, r.fence, nil)732 d.DestroyFence(r.device, r.fence, nil)
710 r.fence = 0733 r.fence = 0
M internal/adapters/vulkan/renderer_test.go +46 -15
@@ -1,10 +1,13 @@
11 package vulkan
22
33 import (
4- "github.com/bnema/nefertty/internal/ports"
54 "image"
65 "image/color"
6+ "os"
77 "testing"
8+
9+ "github.com/bnema/nefertty/internal/ports"
10+ "golang.org/x/sys/unix"
811 )
912
1013 func TestRendererClear(t *testing.T) {
@@ -99,7 +102,7 @@ func TestRendererContents(t *testing.T) {
99102 t.Errorf("At(%d,%d)=%v want %v", x, y, got, want)
100103 }
101104 }
102- render(&ports.SurfaceContent{Width: 4, Height: 2, Stride: 16, Pixels: pixels})
105+ render(shmContent(t, 4, 2, 16, pixels))
103106 check(8, 8, color.RGBA{255, 0, 0, 255})
104107 check(11, 9, color.RGBA{255, 0, 0, 255})
105108 check(12, 8, bg)
@@ -107,7 +110,7 @@ func TestRendererContents(t *testing.T) {
107110 scene.Windows[0].Rect = ports.Rect{X: -2, Y: 0, W: 10, H: 10}
108111 padded := make([]byte, 4*20)
109112 copy(padded[8:12], []byte{7, 11, 23, 255})
110- render(&ports.SurfaceContent{Width: 4, Height: 4, Stride: 20, Pixels: padded})
113+ render(shmContent(t, 4, 4, 20, padded))
111114 check(0, 0, color.RGBA{23, 11, 7, 255})
112115 render(nil)
113116 c := windowColor(1)
@@ -133,10 +136,13 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) {
133136 }
134137 scene.Border = ports.Border{Width: 4, Active: "#ffffff"}
135138 scene.Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}, Focused: true}}
136- contents := map[ports.WindowID]ports.SurfaceContent{1: {Width: 16, Height: 16, Stride: 64, Opaque: true, Pixels: make([]byte, 16*16*4)}}
137- for i := 0; i < len(contents[1].Pixels); i += 4 {
138- copy(contents[1].Pixels[i:i+4], []byte{3, 5, 7, 0})
139+ px := make([]byte, 16*16*4)
140+ for i := 0; i < len(px); i += 4 {
141+ copy(px[i:i+4], []byte{3, 5, 7, 0})
139142 }
143+ opaque := shmContent(t, 16, 16, 64, px)
144+ opaque.Opaque = true
145+ contents := map[ports.WindowID]ports.SurfaceContent{1: *opaque}
140146 if err := r.Render(scene, contents); err != nil {
141147 t.Fatal(err)
142148 }
@@ -160,7 +166,9 @@ func TestRendererLayers(t *testing.T) {
160166 for i := 0; i < len(red); i += 4 {
161167 copy(red[i:i+4], []byte{0, 0, 255, 0})
162168 }
163- contents := map[ports.WindowID]ports.SurfaceContent{2: {ID: 2, Width: 64, Height: 8, Stride: 64 * 4, Opaque: true, Pixels: red}}
169+ layer := shmContent(t, 64, 8, 64*4, red)
170+ layer.ID, layer.Opaque = 2, true
171+ contents := map[ports.WindowID]ports.SurfaceContent{2: *layer}
164172 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, Layers: []ports.SceneLayer{{ID: 2, Layer: ports.LayerTop, Rect: ports.Rect{W: 64, H: 8}}}}
165173 wc := windowColor(1)
166174 window := color.RGBA{wc[0], wc[1], wc[2], 255}
@@ -207,7 +215,9 @@ func TestRendererScale(t *testing.T) {
207215 // Scale 2: a 10x10 logical window at (4,4) covers physical (8,8)-(28,28);
208216 // its 20x20 buffer (fractional client) is copied 1:1.
209217 s := ports.Scene{Scale: 2, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 10, H: 10}, Borderless: true}}}
210- content := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 20, Height: 20, LogicalW: 10, LogicalH: 10, Stride: 80, Pixels: solid(20, 20, red)}}
218+ scaled := shmContent(t, 20, 20, 80, solid(20, 20, red))
219+ scaled.ID, scaled.LogicalW, scaled.LogicalH = 1, 10, 10
220+ content := map[ports.WindowID]ports.SurfaceContent{1: *scaled}
211221 if err := r.Render(s, content); err != nil {
212222 t.Fatal(err)
213223 }
@@ -239,7 +249,9 @@ func TestRendererScale(t *testing.T) {
239249 t.Errorf("1.5 outside = %v", got)
240250 }
241251 // A buffer 1px wider than its slot is clipped, not resampled.
242- content[1] = ports.SurfaceContent{ID: 1, Width: 16, Height: 15, LogicalW: 10, LogicalH: 10, Stride: 64, Pixels: solid(16, 15, red)}
252+ odd := shmContent(t, 16, 15, 64, solid(16, 15, red))
253+ odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10
254+ content[1] = *odd
243255 if err := r.Render(s, content); err != nil {
244256 t.Fatal(err)
245257 }
@@ -249,14 +261,33 @@ func TestRendererScale(t *testing.T) {
249261 }
250262
251263 // solidContent is a w×h B8G8R8A8 buffer of one color.
252-func solidContent(w, h int, c color.RGBA) ports.SurfaceContent {
264+func solidContent(t *testing.T, w, h int, c color.RGBA) ports.SurfaceContent {
253265 px := make([]byte, w*h*4)
254266 for i := 0; i < len(px); i += 4 {
255267 px[i], px[i+1], px[i+2], px[i+3] = c.B, c.G, c.R, 255
256268 }
257- return ports.SurfaceContent{Width: w, Height: h, Stride: w * 4, Pixels: px}
269+ return *shmContent(t, w, h, w*4, px)
270+}
271+
272+// shmContent puts pixels in a memfd pool, like a wl_shm client.
273+func shmContent(t *testing.T, w, h, stride int, pixels []byte) *ports.SurfaceContent {
274+ t.Helper()
275+ fd, err := unix.MemfdCreate("shm-test", unix.MFD_CLOEXEC)
276+ if err != nil {
277+ t.Fatal(err)
278+ }
279+ f := os.NewFile(uintptr(fd), "shm-test")
280+ t.Cleanup(func() { f.Close() })
281+ if _, err := f.Write(pixels); err != nil {
282+ t.Fatal(err)
283+ }
284+ shmPools++
285+ return &ports.SurfaceContent{Width: w, Height: h, SHM: &ports.SHMBuffer{Pool: shmPools, File: f, Stride: stride}}
258286 }
259287
288+// shmPools numbers test pools: a renderer maps each pool ID once.
289+var shmPools uint64
290+
260291 // The window geometry lands on the window rect and the client shadow
261292 // around it is clipped; subsurfaces draw from the root origin, above or
262293 // below it.
@@ -270,13 +301,13 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
270301 bg := color.RGBA{16, 32, 48, 255}
271302 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 10}, Borderless: true}}}
272303 // A 30×20 surface whose window is the 20×10 at (5, 5): a 5px shadow.
273- root := solidContent(30, 20, red)
304+ root := solidContent(t, 30, 20, red)
274305 root.Geometry = ports.Rect{X: 5, Y: 5, W: 20, H: 10}
275306 // Green covers the window's top-left 4×4 corner; blue sits below the
276307 // root, hidden by it.
277308 root.Children = []ports.Subsurface{
278- {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(30, 20, blue)},
279- {X: 5, Y: 5, SurfaceContent: solidContent(4, 4, green)},
309+ {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(t, 30, 20, blue)},
310+ {X: 5, Y: 5, SurfaceContent: solidContent(t, 4, 4, green)},
280311 }
281312 if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
282313 t.Fatal(err)
@@ -295,7 +326,7 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
295326 }
296327 }
297328 // A root with no buffer of its own still shows its children.
298- only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(20, 10, blue)}}}
329+ only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(t, 20, 10, blue)}}}
299330 if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {
300331 t.Fatal(err)
301332 }
@@ -1,10 +1,13 @@
1 package vulkan1 package vulkan
2 2
3 import (3 import (
4- "github.com/bnema/nefertty/internal/ports"
5 "image"4 "image"
6 "image/color"5 "image/color"
6+ "os"
7 "testing"7 "testing"
8+
9+ "github.com/bnema/nefertty/internal/ports"
10+ "golang.org/x/sys/unix"
8 )11 )
9 12
10 func TestRendererClear(t *testing.T) {13 func TestRendererClear(t *testing.T) {
@@ -99,7 +102,7 @@ func TestRendererContents(t *testing.T) {
99 t.Errorf("At(%d,%d)=%v want %v", x, y, got, want)102 t.Errorf("At(%d,%d)=%v want %v", x, y, got, want)
100 }103 }
101 }104 }
102- render(&ports.SurfaceContent{Width: 4, Height: 2, Stride: 16, Pixels: pixels})105+ render(shmContent(t, 4, 2, 16, pixels))
103 check(8, 8, color.RGBA{255, 0, 0, 255})106 check(8, 8, color.RGBA{255, 0, 0, 255})
104 check(11, 9, color.RGBA{255, 0, 0, 255})107 check(11, 9, color.RGBA{255, 0, 0, 255})
105 check(12, 8, bg)108 check(12, 8, bg)
@@ -107,7 +110,7 @@ func TestRendererContents(t *testing.T) {
107 scene.Windows[0].Rect = ports.Rect{X: -2, Y: 0, W: 10, H: 10}110 scene.Windows[0].Rect = ports.Rect{X: -2, Y: 0, W: 10, H: 10}
108 padded := make([]byte, 4*20)111 padded := make([]byte, 4*20)
109 copy(padded[8:12], []byte{7, 11, 23, 255})112 copy(padded[8:12], []byte{7, 11, 23, 255})
110- render(&ports.SurfaceContent{Width: 4, Height: 4, Stride: 20, Pixels: padded})113+ render(shmContent(t, 4, 4, 20, padded))
111 check(0, 0, color.RGBA{23, 11, 7, 255})114 check(0, 0, color.RGBA{23, 11, 7, 255})
112 render(nil)115 render(nil)
113 c := windowColor(1)116 c := windowColor(1)
@@ -133,10 +136,13 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) {
133 }136 }
134 scene.Border = ports.Border{Width: 4, Active: "#ffffff"}137 scene.Border = ports.Border{Width: 4, Active: "#ffffff"}
135 scene.Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}, Focused: true}}138 scene.Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}, Focused: true}}
136- contents := map[ports.WindowID]ports.SurfaceContent{1: {Width: 16, Height: 16, Stride: 64, Opaque: true, Pixels: make([]byte, 16*16*4)}}139+ px := make([]byte, 16*16*4)
137- for i := 0; i < len(contents[1].Pixels); i += 4 {140+ for i := 0; i < len(px); i += 4 {
138- copy(contents[1].Pixels[i:i+4], []byte{3, 5, 7, 0})141+ copy(px[i:i+4], []byte{3, 5, 7, 0})
139 }142 }
143+ opaque := shmContent(t, 16, 16, 64, px)
144+ opaque.Opaque = true
145+ contents := map[ports.WindowID]ports.SurfaceContent{1: *opaque}
140 if err := r.Render(scene, contents); err != nil {146 if err := r.Render(scene, contents); err != nil {
141 t.Fatal(err)147 t.Fatal(err)
142 }148 }
@@ -160,7 +166,9 @@ func TestRendererLayers(t *testing.T) {
160 for i := 0; i < len(red); i += 4 {166 for i := 0; i < len(red); i += 4 {
161 copy(red[i:i+4], []byte{0, 0, 255, 0})167 copy(red[i:i+4], []byte{0, 0, 255, 0})
162 }168 }
163- contents := map[ports.WindowID]ports.SurfaceContent{2: {ID: 2, Width: 64, Height: 8, Stride: 64 * 4, Opaque: true, Pixels: red}}169+ layer := shmContent(t, 64, 8, 64*4, red)
170+ layer.ID, layer.Opaque = 2, true
171+ contents := map[ports.WindowID]ports.SurfaceContent{2: *layer}
164 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, Layers: []ports.SceneLayer{{ID: 2, Layer: ports.LayerTop, Rect: ports.Rect{W: 64, H: 8}}}}172 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, Layers: []ports.SceneLayer{{ID: 2, Layer: ports.LayerTop, Rect: ports.Rect{W: 64, H: 8}}}}
165 wc := windowColor(1)173 wc := windowColor(1)
166 window := color.RGBA{wc[0], wc[1], wc[2], 255}174 window := color.RGBA{wc[0], wc[1], wc[2], 255}
@@ -207,7 +215,9 @@ func TestRendererScale(t *testing.T) {
207 // Scale 2: a 10x10 logical window at (4,4) covers physical (8,8)-(28,28);215 // Scale 2: a 10x10 logical window at (4,4) covers physical (8,8)-(28,28);
208 // its 20x20 buffer (fractional client) is copied 1:1.216 // its 20x20 buffer (fractional client) is copied 1:1.
209 s := ports.Scene{Scale: 2, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 10, H: 10}, Borderless: true}}}217 s := ports.Scene{Scale: 2, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 10, H: 10}, Borderless: true}}}
210- content := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 20, Height: 20, LogicalW: 10, LogicalH: 10, Stride: 80, Pixels: solid(20, 20, red)}}218+ scaled := shmContent(t, 20, 20, 80, solid(20, 20, red))
219+ scaled.ID, scaled.LogicalW, scaled.LogicalH = 1, 10, 10
220+ content := map[ports.WindowID]ports.SurfaceContent{1: *scaled}
211 if err := r.Render(s, content); err != nil {221 if err := r.Render(s, content); err != nil {
212 t.Fatal(err)222 t.Fatal(err)
213 }223 }
@@ -239,7 +249,9 @@ func TestRendererScale(t *testing.T) {
239 t.Errorf("1.5 outside = %v", got)249 t.Errorf("1.5 outside = %v", got)
240 }250 }
241 // A buffer 1px wider than its slot is clipped, not resampled.251 // A buffer 1px wider than its slot is clipped, not resampled.
242- content[1] = ports.SurfaceContent{ID: 1, Width: 16, Height: 15, LogicalW: 10, LogicalH: 10, Stride: 64, Pixels: solid(16, 15, red)}252+ odd := shmContent(t, 16, 15, 64, solid(16, 15, red))
253+ odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10
254+ content[1] = *odd
243 if err := r.Render(s, content); err != nil {255 if err := r.Render(s, content); err != nil {
244 t.Fatal(err)256 t.Fatal(err)
245 }257 }
@@ -249,14 +261,33 @@ func TestRendererScale(t *testing.T) {
249 }261 }
250 262
251 // solidContent is a w×h B8G8R8A8 buffer of one color.263 // solidContent is a w×h B8G8R8A8 buffer of one color.
252-func solidContent(w, h int, c color.RGBA) ports.SurfaceContent {264+func solidContent(t *testing.T, w, h int, c color.RGBA) ports.SurfaceContent {
253 px := make([]byte, w*h*4)265 px := make([]byte, w*h*4)
254 for i := 0; i < len(px); i += 4 {266 for i := 0; i < len(px); i += 4 {
255 px[i], px[i+1], px[i+2], px[i+3] = c.B, c.G, c.R, 255267 px[i], px[i+1], px[i+2], px[i+3] = c.B, c.G, c.R, 255
256 }268 }
257- return ports.SurfaceContent{Width: w, Height: h, Stride: w * 4, Pixels: px}269+ return *shmContent(t, w, h, w*4, px)
270+}
271+
272+// shmContent puts pixels in a memfd pool, like a wl_shm client.
273+func shmContent(t *testing.T, w, h, stride int, pixels []byte) *ports.SurfaceContent {
274+ t.Helper()
275+ fd, err := unix.MemfdCreate("shm-test", unix.MFD_CLOEXEC)
276+ if err != nil {
277+ t.Fatal(err)
278+ }
279+ f := os.NewFile(uintptr(fd), "shm-test")
280+ t.Cleanup(func() { f.Close() })
281+ if _, err := f.Write(pixels); err != nil {
282+ t.Fatal(err)
283+ }
284+ shmPools++
285+ return &ports.SurfaceContent{Width: w, Height: h, SHM: &ports.SHMBuffer{Pool: shmPools, File: f, Stride: stride}}
258 }286 }
259 287
288+// shmPools numbers test pools: a renderer maps each pool ID once.
289+var shmPools uint64
290+
260 // The window geometry lands on the window rect and the client shadow291 // The window geometry lands on the window rect and the client shadow
261 // around it is clipped; subsurfaces draw from the root origin, above or292 // around it is clipped; subsurfaces draw from the root origin, above or
262 // below it.293 // below it.
@@ -270,13 +301,13 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
270 bg := color.RGBA{16, 32, 48, 255}301 bg := color.RGBA{16, 32, 48, 255}
271 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 10}, Borderless: true}}}302 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 10}, Borderless: true}}}
272 // A 30×20 surface whose window is the 20×10 at (5, 5): a 5px shadow.303 // A 30×20 surface whose window is the 20×10 at (5, 5): a 5px shadow.
273- root := solidContent(30, 20, red)304+ root := solidContent(t, 30, 20, red)
274 root.Geometry = ports.Rect{X: 5, Y: 5, W: 20, H: 10}305 root.Geometry = ports.Rect{X: 5, Y: 5, W: 20, H: 10}
275 // Green covers the window's top-left 4×4 corner; blue sits below the306 // Green covers the window's top-left 4×4 corner; blue sits below the
276 // root, hidden by it.307 // root, hidden by it.
277 root.Children = []ports.Subsurface{308 root.Children = []ports.Subsurface{
278- {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(30, 20, blue)},309+ {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(t, 30, 20, blue)},
279- {X: 5, Y: 5, SurfaceContent: solidContent(4, 4, green)},310+ {X: 5, Y: 5, SurfaceContent: solidContent(t, 4, 4, green)},
280 }311 }
281 if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {312 if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
282 t.Fatal(err)313 t.Fatal(err)
@@ -295,7 +326,7 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
295 }326 }
296 }327 }
297 // A root with no buffer of its own still shows its children.328 // A root with no buffer of its own still shows its children.
298- only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(20, 10, blue)}}}329+ only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(t, 20, 10, blue)}}}
299 if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {330 if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {
300 t.Fatal(err)331 t.Fatal(err)
301 }332 }
A internal/adapters/vulkan/shm.go +82 -0
@@ -0,0 +1,82 @@
1+package vulkan
2+
3+import (
4+ "fmt"
5+ "runtime/debug"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+ "golang.org/x/sys/unix"
9+)
10+
11+// wl_shm buffers are read in place: each client pool is mapped once, read
12+// only, and the visible pixels are copied from it into staging when a frame
13+// is drawn. Mappings follow the dmabuf import rules: kept while drawn,
14+// dropped after importTTL frames without use.
15+
16+// mapping is a client pool mapped read-only.
17+type mapping struct {
18+ data []byte
19+ last uint64
20+}
21+
22+// shmPixels returns the pool bytes of a buffer, mapping the pool once and
23+// again when it grew past the mapping.
24+func (r *Renderer) shmPixels(b *ports.SHMBuffer, end int) ([]byte, error) {
25+ if m := r.pools[b.Pool]; m != nil {
26+ if end <= len(m.data) {
27+ m.last = r.frame
28+ return m.data, nil
29+ }
30+ // Uploads collected earlier this frame may still slice the old
31+ // mapping: it is unmapped once staging is done.
32+ r.retired = append(r.retired, m.data)
33+ delete(r.pools, b.Pool)
34+ }
35+ fd, err := dupFile(b.File)
36+ if err != nil {
37+ return nil, err
38+ }
39+ defer unix.Close(fd)
40+ var st unix.Stat_t
41+ if err := unix.Fstat(fd, &st); err != nil {
42+ return nil, err
43+ }
44+ if int64(end) > st.Size {
45+ return nil, fmt.Errorf("shm pool of %d bytes, buffer ends at %d", st.Size, end)
46+ }
47+ data, err := unix.Mmap(fd, 0, int(st.Size), unix.PROT_READ, unix.MAP_SHARED)
48+ if err != nil {
49+ return nil, err
50+ }
51+ r.pools[b.Pool] = &mapping{data: data, last: r.frame}
52+ return data, nil
53+}
54+
55+// dropPools unmaps replaced mappings and pools not drawn for importTTL
56+// frames.
57+func (r *Renderer) dropPools() {
58+ for _, data := range r.retired {
59+ _ = unix.Munmap(data)
60+ }
61+ r.retired = r.retired[:0]
62+ for id, m := range r.pools {
63+ if r.frame-m.last > importTTL {
64+ _ = unix.Munmap(m.data)
65+ delete(r.pools, id)
66+ }
67+ }
68+}
69+
70+// copyGuarded runs copy with page faults turned into a recoverable panic:
71+// a client may shrink its pool file under the mapping (SIGBUS). It reports
72+// false when the client broke its buffer.
73+func copyGuarded(copy func()) (ok bool) {
74+ defer debug.SetPanicOnFault(debug.SetPanicOnFault(true))
75+ defer func() {
76+ if recover() != nil {
77+ ok = false
78+ }
79+ }()
80+ copy()
81+ return true
82+}
@@ -0,0 +1,82 @@
1+package vulkan
2+
3+import (
4+ "fmt"
5+ "runtime/debug"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+ "golang.org/x/sys/unix"
9+)
10+
11+// wl_shm buffers are read in place: each client pool is mapped once, read
12+// only, and the visible pixels are copied from it into staging when a frame
13+// is drawn. Mappings follow the dmabuf import rules: kept while drawn,
14+// dropped after importTTL frames without use.
15+
16+// mapping is a client pool mapped read-only.
17+type mapping struct {
18+ data []byte
19+ last uint64
20+}
21+
22+// shmPixels returns the pool bytes of a buffer, mapping the pool once and
23+// again when it grew past the mapping.
24+func (r *Renderer) shmPixels(b *ports.SHMBuffer, end int) ([]byte, error) {
25+ if m := r.pools[b.Pool]; m != nil {
26+ if end <= len(m.data) {
27+ m.last = r.frame
28+ return m.data, nil
29+ }
30+ // Uploads collected earlier this frame may still slice the old
31+ // mapping: it is unmapped once staging is done.
32+ r.retired = append(r.retired, m.data)
33+ delete(r.pools, b.Pool)
34+ }
35+ fd, err := dupFile(b.File)
36+ if err != nil {
37+ return nil, err
38+ }
39+ defer unix.Close(fd)
40+ var st unix.Stat_t
41+ if err := unix.Fstat(fd, &st); err != nil {
42+ return nil, err
43+ }
44+ if int64(end) > st.Size {
45+ return nil, fmt.Errorf("shm pool of %d bytes, buffer ends at %d", st.Size, end)
46+ }
47+ data, err := unix.Mmap(fd, 0, int(st.Size), unix.PROT_READ, unix.MAP_SHARED)
48+ if err != nil {
49+ return nil, err
50+ }
51+ r.pools[b.Pool] = &mapping{data: data, last: r.frame}
52+ return data, nil
53+}
54+
55+// dropPools unmaps replaced mappings and pools not drawn for importTTL
56+// frames.
57+func (r *Renderer) dropPools() {
58+ for _, data := range r.retired {
59+ _ = unix.Munmap(data)
60+ }
61+ r.retired = r.retired[:0]
62+ for id, m := range r.pools {
63+ if r.frame-m.last > importTTL {
64+ _ = unix.Munmap(m.data)
65+ delete(r.pools, id)
66+ }
67+ }
68+}
69+
70+// copyGuarded runs copy with page faults turned into a recoverable panic:
71+// a client may shrink its pool file under the mapping (SIGBUS). It reports
72+// false when the client broke its buffer.
73+func copyGuarded(copy func()) (ok bool) {
74+ defer debug.SetPanicOnFault(debug.SetPanicOnFault(true))
75+ defer func() {
76+ if recover() != nil {
77+ ok = false
78+ }
79+ }()
80+ copy()
81+ return true
82+}
M internal/adapters/wayland/dmabuf_test.go +1 -1
@@ -163,7 +163,7 @@ func TestWestonDMABuf(t *testing.T) {
163163 for {
164164 select {
165165 case c := <-contents:
166- if c.ID != w.ID || c.DMABuf == nil || c.Pixels != nil || len(c.DMABuf.Planes) == 0 || c.DMABuf.Planes[0].File == nil {
166+ if c.ID != w.ID || c.DMABuf == nil || c.SHM != nil || len(c.DMABuf.Planes) == 0 || c.DMABuf.Planes[0].File == nil {
167167 t.Fatalf("content %+v", c)
168168 }
169169 return
@@ -163,7 +163,7 @@ func TestWestonDMABuf(t *testing.T) {
163 for {163 for {
164 select {164 select {
165 case c := <-contents:165 case c := <-contents:
166- if c.ID != w.ID || c.DMABuf == nil || c.Pixels != nil || len(c.DMABuf.Planes) == 0 || c.DMABuf.Planes[0].File == nil {166+ if c.ID != w.ID || c.DMABuf == nil || c.SHM != nil || len(c.DMABuf.Planes) == 0 || c.DMABuf.Planes[0].File == nil {
167 t.Fatalf("content %+v", c)167 t.Fatalf("content %+v", c)
168 }168 }
169 return169 return
M internal/adapters/wayland/globals.go +29 -44
@@ -4,8 +4,7 @@ import (
44 "github.com/bnema/nefertty/internal/ports"
55 "github.com/bnema/purego-libwayland/protocol/wayland"
66 "github.com/bnema/purego-libwayland/server"
7- "golang.org/x/sys/unix"
8- "runtime/debug"
7+ "os"
98 )
109
1110 func registerGlobals(d *server.Display, o Options, s *Server) error {
@@ -206,49 +205,46 @@ func (region) Subtract(*wayland.Region, int32, int32, int32, int32) {}
206205 type shm struct{ server *Server }
207206
208207 func (h shm) CreatePool(r *wayland.Shm, id uint32, fd int, size int32) {
209- if size <= 0 {
210- unix.Close(fd)
208+ file := os.NewFile(uintptr(fd), "wl_shm_pool")
209+ if size <= 0 || !fileHolds(file, int64(size)) {
210+ file.Close()
211211 r.PostError(uint32(wayland.ShmErrorInvalidFd), "invalid pool size")
212212 return
213213 }
214- data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_SHARED)
215- if err != nil {
216- unix.Close(fd)
217- r.PostError(uint32(wayland.ShmErrorInvalidFd), "cannot map pool")
218- return
219- }
220- state := &pool{fd: fd, size: size, data: data, shm: r, server: h.server}
214+ h.server.nextPool++
215+ state := &pool{id: h.server.nextPool, file: file, size: size, shm: r, server: h.server}
221216 p, err := wayland.NewShmPool(r.Client(), 1, id, state)
222217 if err != nil {
223- state.close()
218+ file.Close()
224219 return
225220 }
226221 p.OnDestroy = func() {
227222 state.destroyed = true
228223 if state.refs == 0 {
229- state.close()
224+ state.file.Close()
230225 }
231226 }
232227 }
233228 func (shm) Release(*wayland.Shm) {}
234229
230+// fileHolds reports whether the file has at least size bytes.
231+func fileHolds(f *os.File, size int64) bool {
232+ st, err := f.Stat()
233+ return err == nil && st.Size() >= size
234+}
235+
236+// pool is a wl_shm pool. nefertty never maps it: renderers map the file
237+// and read the pixels when they draw (ports.SHMBuffer).
235238 type pool struct {
236- fd int
239+ id uint64
240+ file *os.File
237241 size int32
238- data []byte
239242 shm *wayland.Shm
240243 server *Server
241244 refs int
242245 destroyed bool
243246 }
244247
245-func (p *pool) close() {
246- if p.data != nil {
247- _ = unix.Munmap(p.data)
248- p.data = nil
249- }
250- _ = unix.Close(p.fd)
251-}
252248 func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, offset, width, height, stride int32, format uint32) {
253249 if format != uint32(wayland.ShmFormatArgb8888) && format != uint32(wayland.ShmFormatXrgb8888) {
254250 p.shm.PostError(uint32(wayland.ShmErrorInvalidFormat), "unsupported format")
@@ -269,7 +265,7 @@ func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, offset, width, height
269265 delete(p.server.buffers, b.Resource)
270266 p.refs--
271267 if p.destroyed && p.refs == 0 {
272- p.close()
268+ p.file.Close()
273269 }
274270 }
275271 }
@@ -279,13 +275,11 @@ func (p *pool) Resize(r *wayland.ShmPool, size int32) {
279275 p.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "pool must grow")
280276 return
281277 }
282- data, err := unix.Mmap(p.fd, 0, int(size), unix.PROT_READ, unix.MAP_SHARED)
283- if err != nil {
278+ if !fileHolds(p.file, int64(size)) {
284279 p.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "cannot resize pool")
285280 return
286281 }
287- _ = unix.Munmap(p.data)
288- p.data, p.size = data, size
282+ p.size = size
289283 }
290284
291285 type buffer struct {
@@ -315,23 +309,14 @@ func (s *Server) addBuffer(r *wayland.Buffer, b *dmabufBuffer) {
315309 }
316310 }
317311
318-// A malicious client may truncate its fd after mmap; SetPanicOnFault converts
319-// the resulting SIGBUS on this goroutine to a recoverable panic.
320-func (b *buffer) content(id ports.WindowID) (content ports.SurfaceContent, ok bool) {
321- defer debug.SetPanicOnFault(debug.SetPanicOnFault(true))
322- defer func() {
323- if recover() != nil {
324- content = ports.SurfaceContent{}
325- ok = false
326- }
327- }()
328- size := (b.height-1)*b.stride + b.width*4
329- pixels := make([]byte, size)
330- for y := 0; y < b.height; y++ {
331- start := b.offset + y*b.stride
332- copy(pixels[y*b.stride:y*b.stride+b.width*4], b.pool.data[start:start+b.width*4])
333- }
334- return ports.SurfaceContent{ID: id, Width: b.width, Height: b.height, Stride: b.stride, Opaque: b.format == uint32(wayland.ShmFormatXrgb8888), Pixels: pixels}, true
312+// content points the renderer at the pool; a file shrunk below the buffer
313+// is refused.
314+func (b *buffer) content(id ports.WindowID) (ports.SurfaceContent, bool) {
315+ if !fileHolds(b.pool.file, int64(b.offset+(b.height-1)*b.stride+b.width*4)) {
316+ return ports.SurfaceContent{}, false
317+ }
318+ shm := &ports.SHMBuffer{Pool: b.pool.id, File: b.pool.file, Offset: b.offset, Stride: b.stride}
319+ return ports.SurfaceContent{ID: id, Width: b.width, Height: b.height, Opaque: b.format == uint32(wayland.ShmFormatXrgb8888), SHM: shm}, true
335320 }
336321
337322 type seat struct{ server *Server }
@@ -4,8 +4,7 @@ import (
4 "github.com/bnema/nefertty/internal/ports"4 "github.com/bnema/nefertty/internal/ports"
5 "github.com/bnema/purego-libwayland/protocol/wayland"5 "github.com/bnema/purego-libwayland/protocol/wayland"
6 "github.com/bnema/purego-libwayland/server"6 "github.com/bnema/purego-libwayland/server"
7- "golang.org/x/sys/unix"7+ "os"
8- "runtime/debug"
9 )8 )
10 9
11 func registerGlobals(d *server.Display, o Options, s *Server) error {10 func registerGlobals(d *server.Display, o Options, s *Server) error {
@@ -206,49 +205,46 @@ func (region) Subtract(*wayland.Region, int32, int32, int32, int32) {}
206 type shm struct{ server *Server }205 type shm struct{ server *Server }
207 206
208 func (h shm) CreatePool(r *wayland.Shm, id uint32, fd int, size int32) {207 func (h shm) CreatePool(r *wayland.Shm, id uint32, fd int, size int32) {
209- if size <= 0 {208+ file := os.NewFile(uintptr(fd), "wl_shm_pool")
210- unix.Close(fd)209+ if size <= 0 || !fileHolds(file, int64(size)) {
210+ file.Close()
211 r.PostError(uint32(wayland.ShmErrorInvalidFd), "invalid pool size")211 r.PostError(uint32(wayland.ShmErrorInvalidFd), "invalid pool size")
212 return212 return
213 }213 }
214- data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_SHARED)214+ h.server.nextPool++
215- if err != nil {215+ state := &pool{id: h.server.nextPool, file: file, size: size, shm: r, server: h.server}
216- unix.Close(fd)
217- r.PostError(uint32(wayland.ShmErrorInvalidFd), "cannot map pool")
218- return
219- }
220- state := &pool{fd: fd, size: size, data: data, shm: r, server: h.server}
221 p, err := wayland.NewShmPool(r.Client(), 1, id, state)216 p, err := wayland.NewShmPool(r.Client(), 1, id, state)
222 if err != nil {217 if err != nil {
223- state.close()218+ file.Close()
224 return219 return
225 }220 }
226 p.OnDestroy = func() {221 p.OnDestroy = func() {
227 state.destroyed = true222 state.destroyed = true
228 if state.refs == 0 {223 if state.refs == 0 {
229- state.close()224+ state.file.Close()
230 }225 }
231 }226 }
232 }227 }
233 func (shm) Release(*wayland.Shm) {}228 func (shm) Release(*wayland.Shm) {}
234 229
230+// fileHolds reports whether the file has at least size bytes.
231+func fileHolds(f *os.File, size int64) bool {
232+ st, err := f.Stat()
233+ return err == nil && st.Size() >= size
234+}
235+
236+// pool is a wl_shm pool. nefertty never maps it: renderers map the file
237+// and read the pixels when they draw (ports.SHMBuffer).
235 type pool struct {238 type pool struct {
236- fd int239+ id uint64
240+ file *os.File
237 size int32241 size int32
238- data []byte
239 shm *wayland.Shm242 shm *wayland.Shm
240 server *Server243 server *Server
241 refs int244 refs int
242 destroyed bool245 destroyed bool
243 }246 }
244 247
245-func (p *pool) close() {
246- if p.data != nil {
247- _ = unix.Munmap(p.data)
248- p.data = nil
249- }
250- _ = unix.Close(p.fd)
251-}
252 func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, offset, width, height, stride int32, format uint32) {248 func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, offset, width, height, stride int32, format uint32) {
253 if format != uint32(wayland.ShmFormatArgb8888) && format != uint32(wayland.ShmFormatXrgb8888) {249 if format != uint32(wayland.ShmFormatArgb8888) && format != uint32(wayland.ShmFormatXrgb8888) {
254 p.shm.PostError(uint32(wayland.ShmErrorInvalidFormat), "unsupported format")250 p.shm.PostError(uint32(wayland.ShmErrorInvalidFormat), "unsupported format")
@@ -269,7 +265,7 @@ func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, offset, width, height
269 delete(p.server.buffers, b.Resource)265 delete(p.server.buffers, b.Resource)
270 p.refs--266 p.refs--
271 if p.destroyed && p.refs == 0 {267 if p.destroyed && p.refs == 0 {
272- p.close()268+ p.file.Close()
273 }269 }
274 }270 }
275 }271 }
@@ -279,13 +275,11 @@ func (p *pool) Resize(r *wayland.ShmPool, size int32) {
279 p.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "pool must grow")275 p.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "pool must grow")
280 return276 return
281 }277 }
282- data, err := unix.Mmap(p.fd, 0, int(size), unix.PROT_READ, unix.MAP_SHARED)278+ if !fileHolds(p.file, int64(size)) {
283- if err != nil {
284 p.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "cannot resize pool")279 p.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "cannot resize pool")
285 return280 return
286 }281 }
287- _ = unix.Munmap(p.data)282+ p.size = size
288- p.data, p.size = data, size
289 }283 }
290 284
291 type buffer struct {285 type buffer struct {
@@ -315,23 +309,14 @@ func (s *Server) addBuffer(r *wayland.Buffer, b *dmabufBuffer) {
315 }309 }
316 }310 }
317 311
318-// A malicious client may truncate its fd after mmap; SetPanicOnFault converts312+// content points the renderer at the pool; a file shrunk below the buffer
319-// the resulting SIGBUS on this goroutine to a recoverable panic.313+// is refused.
320-func (b *buffer) content(id ports.WindowID) (content ports.SurfaceContent, ok bool) {314+func (b *buffer) content(id ports.WindowID) (ports.SurfaceContent, bool) {
321- defer debug.SetPanicOnFault(debug.SetPanicOnFault(true))315+ if !fileHolds(b.pool.file, int64(b.offset+(b.height-1)*b.stride+b.width*4)) {
322- defer func() {316+ return ports.SurfaceContent{}, false
323- if recover() != nil {317+ }
324- content = ports.SurfaceContent{}318+ shm := &ports.SHMBuffer{Pool: b.pool.id, File: b.pool.file, Offset: b.offset, Stride: b.stride}
325- ok = false319+ return ports.SurfaceContent{ID: id, Width: b.width, Height: b.height, Opaque: b.format == uint32(wayland.ShmFormatXrgb8888), SHM: shm}, true
326- }
327- }()
328- size := (b.height-1)*b.stride + b.width*4
329- pixels := make([]byte, size)
330- for y := 0; y < b.height; y++ {
331- start := b.offset + y*b.stride
332- copy(pixels[y*b.stride:y*b.stride+b.width*4], b.pool.data[start:start+b.width*4])
333- }
334- return ports.SurfaceContent{ID: id, Width: b.width, Height: b.height, Stride: b.stride, Opaque: b.format == uint32(wayland.ShmFormatXrgb8888), Pixels: pixels}, true
335 }320 }
336 321
337 type seat struct{ server *Server }322 type seat struct{ server *Server }
M internal/adapters/wayland/protocol_test.go +88 -11
@@ -4,6 +4,7 @@ import (
44 "bytes"
55 "errors"
66 "github.com/bnema/nefertty/internal/ports"
7+ "os"
78 "path/filepath"
89 "testing"
910 "time"
@@ -327,19 +328,95 @@ func TestSHMUnpaddedLastRow(t *testing.T) {
327328 }
328329 }
329330
330-func TestBufferContentUnpaddedLastRow(t *testing.T) {
331- p := &pool{data: make([]byte, 28)}
332- b := &buffer{pool: p, offset: 4, width: 2, height: 2, stride: 16}
333- copy(p.data[20:], []byte{1, 2, 3, 4, 5, 6, 7, 8})
334- content, ok := b.content(1)
335- if !ok || len(content.Pixels) != 24 {
336- t.Fatalf("content ok=%v size=%d", ok, len(content.Pixels))
337- }
338- for i, v := range []byte{1, 2, 3, 4, 5, 6, 7, 8} {
339- if content.Pixels[16+i] != v {
340- t.Fatalf("last row byte %d = %d, want %d", i, content.Pixels[16+i], v)
331+// A buffer whose pool file was shrunk below it has no content.
332+func TestBufferContentTruncatedPool(t *testing.T) {
333+ fd, err := unix.MemfdCreate("truncated-pool", unix.MFD_CLOEXEC)
334+ if err != nil {
335+ t.Fatal(err)
336+ }
337+ f := os.NewFile(uintptr(fd), "truncated-pool")
338+ defer f.Close()
339+ if err := f.Truncate(28); err != nil {
340+ t.Fatal(err)
341+ }
342+ b := &buffer{pool: &pool{id: 3, file: f, size: 28}, offset: 4, width: 2, height: 2, stride: 16}
343+ c, ok := b.content(1)
344+ if !ok || c.SHM == nil || c.SHM.Pool != 3 || c.SHM.Offset != 4 || c.SHM.Stride != 16 {
345+ t.Fatalf("content ok=%v %+v", ok, c.SHM)
346+ }
347+ if err := f.Truncate(27); err != nil {
348+ t.Fatal(err)
349+ }
350+ if _, ok := b.content(1); ok {
351+ t.Fatal("content from a truncated pool")
352+ }
353+}
354+
355+type releaseProxy struct {
356+ wlturbo.BaseProxy
357+ released chan uint32
358+}
359+
360+func (p *releaseProxy) Dispatch(e *wlturbo.Event) {
361+ if e.Opcode == uint16(wayland.BufferEventRelease) {
362+ p.released <- p.ID()
363+ }
364+}
365+
366+// wl_shm buffers are read in place: released when replaced or when their
367+// surface is destroyed, never on commit.
368+func TestSHMBufferReleasedOnReplaceAndDestroy(t *testing.T) {
369+ s, _, _, dir := lifecycleServer(t)
370+ c := protocolClient(t, s, dir)
371+ comp := bindProtocol(t, c, "wl_compositor")
372+ shm := bindProtocol(t, c, "wl_shm")
373+ fd, err := unix.MemfdCreate("release", unix.MFD_CLOEXEC)
374+ if err != nil {
375+ t.Fatal(err)
376+ }
377+ defer unix.Close(fd)
378+ if err := unix.Ftruncate(fd, 64); err != nil {
379+ t.Fatal(err)
380+ }
381+ pool := c.AllocateID()
382+ registerProtocol(t, c, shm)
383+ registerProtocol(t, c, pool)
384+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(64)); err != nil {
385+ t.Fatal(err)
386+ }
387+ released := make(chan uint32, 4)
388+ bufs := make([]uint32, 2)
389+ for i := range bufs {
390+ bufs[i] = c.AllocateID()
391+ p := &releaseProxy{released: released}
392+ p.SetID(bufs[i])
393+ c.Context().Register(p)
394+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, bufs[i], int32(i*32), int32(2), int32(2), int32(8), uint32(wayland.ShmFormatXrgb8888))
395+ }
396+ surf := c.AllocateID()
397+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
398+ registerProtocol(t, c, surf)
399+ expect := func(want []uint32) {
400+ t.Helper()
401+ if err := c.Roundtrip(); err != nil {
402+ t.Fatal(err)
403+ }
404+ var got []uint32
405+ for len(released) > 0 {
406+ got = append(got, <-released)
407+ }
408+ if len(got) != len(want) || (len(want) == 1 && got[0] != want[0]) {
409+ t.Fatalf("released %v, want %v", got, want)
341410 }
342411 }
412+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0))
413+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
414+ expect(nil)
415+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0))
416+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
417+ expect([]uint32{bufs[0]})
418+ requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy)
419+ expect([]uint32{bufs[1]})
343420 }
344421
345422 type pointerEvents struct {
@@ -4,6 +4,7 @@ import (
4 "bytes"4 "bytes"
5 "errors"5 "errors"
6 "github.com/bnema/nefertty/internal/ports"6 "github.com/bnema/nefertty/internal/ports"
7+ "os"
7 "path/filepath"8 "path/filepath"
8 "testing"9 "testing"
9 "time"10 "time"
@@ -327,19 +328,95 @@ func TestSHMUnpaddedLastRow(t *testing.T) {
327 }328 }
328 }329 }
329 330
330-func TestBufferContentUnpaddedLastRow(t *testing.T) {331+// A buffer whose pool file was shrunk below it has no content.
331- p := &pool{data: make([]byte, 28)}332+func TestBufferContentTruncatedPool(t *testing.T) {
332- b := &buffer{pool: p, offset: 4, width: 2, height: 2, stride: 16}333+ fd, err := unix.MemfdCreate("truncated-pool", unix.MFD_CLOEXEC)
333- copy(p.data[20:], []byte{1, 2, 3, 4, 5, 6, 7, 8})334+ if err != nil {
334- content, ok := b.content(1)335+ t.Fatal(err)
335- if !ok || len(content.Pixels) != 24 {336+ }
336- t.Fatalf("content ok=%v size=%d", ok, len(content.Pixels))337+ f := os.NewFile(uintptr(fd), "truncated-pool")
337- }338+ defer f.Close()
338- for i, v := range []byte{1, 2, 3, 4, 5, 6, 7, 8} {339+ if err := f.Truncate(28); err != nil {
339- if content.Pixels[16+i] != v {340+ t.Fatal(err)
340- t.Fatalf("last row byte %d = %d, want %d", i, content.Pixels[16+i], v)341+ }
342+ b := &buffer{pool: &pool{id: 3, file: f, size: 28}, offset: 4, width: 2, height: 2, stride: 16}
343+ c, ok := b.content(1)
344+ if !ok || c.SHM == nil || c.SHM.Pool != 3 || c.SHM.Offset != 4 || c.SHM.Stride != 16 {
345+ t.Fatalf("content ok=%v %+v", ok, c.SHM)
346+ }
347+ if err := f.Truncate(27); err != nil {
348+ t.Fatal(err)
349+ }
350+ if _, ok := b.content(1); ok {
351+ t.Fatal("content from a truncated pool")
352+ }
353+}
354+
355+type releaseProxy struct {
356+ wlturbo.BaseProxy
357+ released chan uint32
358+}
359+
360+func (p *releaseProxy) Dispatch(e *wlturbo.Event) {
361+ if e.Opcode == uint16(wayland.BufferEventRelease) {
362+ p.released <- p.ID()
363+ }
364+}
365+
366+// wl_shm buffers are read in place: released when replaced or when their
367+// surface is destroyed, never on commit.
368+func TestSHMBufferReleasedOnReplaceAndDestroy(t *testing.T) {
369+ s, _, _, dir := lifecycleServer(t)
370+ c := protocolClient(t, s, dir)
371+ comp := bindProtocol(t, c, "wl_compositor")
372+ shm := bindProtocol(t, c, "wl_shm")
373+ fd, err := unix.MemfdCreate("release", unix.MFD_CLOEXEC)
374+ if err != nil {
375+ t.Fatal(err)
376+ }
377+ defer unix.Close(fd)
378+ if err := unix.Ftruncate(fd, 64); err != nil {
379+ t.Fatal(err)
380+ }
381+ pool := c.AllocateID()
382+ registerProtocol(t, c, shm)
383+ registerProtocol(t, c, pool)
384+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(64)); err != nil {
385+ t.Fatal(err)
386+ }
387+ released := make(chan uint32, 4)
388+ bufs := make([]uint32, 2)
389+ for i := range bufs {
390+ bufs[i] = c.AllocateID()
391+ p := &releaseProxy{released: released}
392+ p.SetID(bufs[i])
393+ c.Context().Register(p)
394+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, bufs[i], int32(i*32), int32(2), int32(2), int32(8), uint32(wayland.ShmFormatXrgb8888))
395+ }
396+ surf := c.AllocateID()
397+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
398+ registerProtocol(t, c, surf)
399+ expect := func(want []uint32) {
400+ t.Helper()
401+ if err := c.Roundtrip(); err != nil {
402+ t.Fatal(err)
403+ }
404+ var got []uint32
405+ for len(released) > 0 {
406+ got = append(got, <-released)
407+ }
408+ if len(got) != len(want) || (len(want) == 1 && got[0] != want[0]) {
409+ t.Fatalf("released %v, want %v", got, want)
341 }410 }
342 }411 }
412+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0))
413+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
414+ expect(nil)
415+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0))
416+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
417+ expect([]uint32{bufs[0]})
418+ requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy)
419+ expect([]uint32{bufs[1]})
343 }420 }
344 421
345 type pointerEvents struct {422 type pointerEvents struct {
M internal/adapters/wayland/scale_test.go +1 -1
@@ -157,7 +157,7 @@ func TestFractionalScaleAndViewport(t *testing.T) {
157157 for {
158158 select {
159159 case got := <-contents:
160- if got.Pixels == nil {
160+ if got.SHM == nil {
161161 continue
162162 }
163163 if got.Width != 30 || got.Height != 15 || got.LogicalW != 20 || got.LogicalH != 10 {
@@ -157,7 +157,7 @@ func TestFractionalScaleAndViewport(t *testing.T) {
157 for {157 for {
158 select {158 select {
159 case got := <-contents:159 case got := <-contents:
160- if got.Pixels == nil {160+ if got.SHM == nil {
161 continue161 continue
162 }162 }
163 if got.Width != 30 || got.Height != 15 || got.LogicalW != 20 || got.LogicalH != 10 {163 if got.Width != 30 || got.Height != 15 || got.LogicalW != 20 || got.LogicalH != 10 {
M internal/adapters/wayland/server.go +1 -0
@@ -52,6 +52,7 @@ type Server struct {
5252 windows map[ports.WindowID]*window
5353 layers map[ports.WindowID]*layerSurface
5454 nextWindow ports.WindowID
55+ nextPool uint64
5556 focused ports.WindowID
5657 pointerFocus ports.WindowID
5758 pointerX, pointerY float64
@@ -52,6 +52,7 @@ type Server struct {
52 windows map[ports.WindowID]*window52 windows map[ports.WindowID]*window
53 layers map[ports.WindowID]*layerSurface53 layers map[ports.WindowID]*layerSurface
54 nextWindow ports.WindowID54 nextWindow ports.WindowID
55+ nextPool uint64
55 focused ports.WindowID56 focused ports.WindowID
56 pointerFocus ports.WindowID57 pointerFocus ports.WindowID
57 pointerX, pointerY float6458 pointerX, pointerY float64
M internal/adapters/wayland/server_test.go +8 -9
@@ -7,6 +7,7 @@ import (
77 "os"
88 "os/exec"
99 "path/filepath"
10+ "slices"
1011 "strings"
1112 "sync"
1213 "testing"
@@ -398,17 +399,15 @@ func TestWestonContents(t *testing.T) {
398399 for count < 2 {
399400 select {
400401 case c := <-contents:
401- if c.ID != w.ID || c.Width <= 0 || c.Height <= 0 || c.Stride < c.Width*4 || len(c.Pixels) != c.Stride*c.Height {
402- t.Fatalf("invalid content: id=%d dimensions=%dx%d stride=%d length=%d", c.ID, c.Width, c.Height, c.Stride, len(c.Pixels))
402+ if c.ID != w.ID || c.Width <= 0 || c.Height <= 0 || c.SHM == nil || c.SHM.Stride < c.Width*4 || c.SHM.File == nil {
403+ t.Fatalf("invalid content: %+v", c)
403404 }
404- nonzero := false
405- for _, b := range c.Pixels {
406- if b != 0 {
407- nonzero = true
408- break
409- }
405+ // The pool holds the client's pixels: read them like a renderer.
406+ px := make([]byte, c.SHM.Stride)
407+ if _, err := c.SHM.File.ReadAt(px, int64(c.SHM.Offset+c.Height/2*c.SHM.Stride)); err != nil {
408+ t.Fatal(err)
410409 }
411- if !nonzero {
410+ if !slices.ContainsFunc(px, func(b byte) bool { return b != 0 }) {
412411 t.Fatal("empty pixels")
413412 }
414413 if count == 0 {
@@ -7,6 +7,7 @@ import (
7 "os"7 "os"
8 "os/exec"8 "os/exec"
9 "path/filepath"9 "path/filepath"
10+ "slices"
10 "strings"11 "strings"
11 "sync"12 "sync"
12 "testing"13 "testing"
@@ -398,17 +399,15 @@ func TestWestonContents(t *testing.T) {
398 for count < 2 {399 for count < 2 {
399 select {400 select {
400 case c := <-contents:401 case c := <-contents:
401- if c.ID != w.ID || c.Width <= 0 || c.Height <= 0 || c.Stride < c.Width*4 || len(c.Pixels) != c.Stride*c.Height {402+ if c.ID != w.ID || c.Width <= 0 || c.Height <= 0 || c.SHM == nil || c.SHM.Stride < c.Width*4 || c.SHM.File == nil {
402- t.Fatalf("invalid content: id=%d dimensions=%dx%d stride=%d length=%d", c.ID, c.Width, c.Height, c.Stride, len(c.Pixels))403+ t.Fatalf("invalid content: %+v", c)
403 }404 }
404- nonzero := false405+ // The pool holds the client's pixels: read them like a renderer.
405- for _, b := range c.Pixels {406+ px := make([]byte, c.SHM.Stride)
406- if b != 0 {407+ if _, err := c.SHM.File.ReadAt(px, int64(c.SHM.Offset+c.Height/2*c.SHM.Stride)); err != nil {
407- nonzero = true408+ t.Fatal(err)
408- break
409- }
410 }409 }
411- if !nonzero {410+ if !slices.ContainsFunc(px, func(b byte) bool { return b != 0 }) {
412 t.Fatal("empty pixels")411 t.Fatal("empty pixels")
413 }412 }
414 if count == 0 {413 if count == 0 {
M internal/adapters/wayland/surface.go +7 -11
@@ -31,7 +31,6 @@ type surface struct {
3131 callbacks []*wayland.Callback
3232 role func(bool)
3333 destroyed bool
34- released bool
3534 // on is the output the surface entered; scale is the last scale sent.
3635 on *output
3736 scale float64
@@ -142,6 +141,10 @@ func (s *surface) Destroy(*wayland.Surface) {
142141 cb.Destroy()
143142 }
144143 s.callbacks = nil
144+ // The client may reuse the buffer on another surface.
145+ if s.current != nil && s.current.Resource.Alive() {
146+ s.current.SendRelease()
147+ }
145148 s.current, s.pending = nil, nil
146149 if s.role != nil {
147150 s.role(false)
@@ -175,12 +178,11 @@ func (s *surface) Commit(*wayland.Surface) {
175178 }
176179 if s.attached {
177180 if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {
178- if !s.released && s.current.Resource.Alive() {
181+ if s.current.Resource.Alive() {
179182 s.current.SendRelease()
180183 }
181184 }
182185 s.current = s.pending
183- s.released = false
184186 s.pending = nil
185187 s.attached = false
186188 }
@@ -201,14 +203,8 @@ func (s *surface) Commit(*wayland.Surface) {
201203 } else if shm, ok := state.(*buffer); ok {
202204 shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")
203205 }
204- // wl_shm pixels were copied: the client may reuse the buffer.
205- // A dmabuf is read in place until the next buffer replaces it.
206- if _, gpu := state.(*dmabufBuffer); !gpu {
207- if s.current.Resource.Alive() {
208- s.current.SendRelease()
209- }
210- s.released = true
211- }
206+ // Buffers are read in place: released when the next one
207+ // replaces them.
212208 }
213209 }
214210 if s.current == nil {
@@ -31,7 +31,6 @@ type surface struct {
31 callbacks []*wayland.Callback31 callbacks []*wayland.Callback
32 role func(bool)32 role func(bool)
33 destroyed bool33 destroyed bool
34- released bool
35 // on is the output the surface entered; scale is the last scale sent.34 // on is the output the surface entered; scale is the last scale sent.
36 on *output35 on *output
37 scale float6436 scale float64
@@ -142,6 +141,10 @@ func (s *surface) Destroy(*wayland.Surface) {
142 cb.Destroy()141 cb.Destroy()
143 }142 }
144 s.callbacks = nil143 s.callbacks = nil
144+ // The client may reuse the buffer on another surface.
145+ if s.current != nil && s.current.Resource.Alive() {
146+ s.current.SendRelease()
147+ }
145 s.current, s.pending = nil, nil148 s.current, s.pending = nil, nil
146 if s.role != nil {149 if s.role != nil {
147 s.role(false)150 s.role(false)
@@ -175,12 +178,11 @@ func (s *surface) Commit(*wayland.Surface) {
175 }178 }
176 if s.attached {179 if s.attached {
177 if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {180 if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {
178- if !s.released && s.current.Resource.Alive() {181+ if s.current.Resource.Alive() {
179 s.current.SendRelease()182 s.current.SendRelease()
180 }183 }
181 }184 }
182 s.current = s.pending185 s.current = s.pending
183- s.released = false
184 s.pending = nil186 s.pending = nil
185 s.attached = false187 s.attached = false
186 }188 }
@@ -201,14 +203,8 @@ func (s *surface) Commit(*wayland.Surface) {
201 } else if shm, ok := state.(*buffer); ok {203 } else if shm, ok := state.(*buffer); ok {
202 shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")204 shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")
203 }205 }
204- // wl_shm pixels were copied: the client may reuse the buffer.206+ // Buffers are read in place: released when the next one
205- // A dmabuf is read in place until the next buffer replaces it.207+ // replaces them.
206- if _, gpu := state.(*dmabufBuffer); !gpu {
207- if s.current.Resource.Alive() {
208- s.current.SendRelease()
209- }
210- s.released = true
211- }
212 }208 }
213 }209 }
214 if s.current == nil {210 if s.current == nil {
M internal/ports/messages.go +21 -10
@@ -341,20 +341,18 @@ type SceneLayer struct {
341341 }
342342
343343 // SurfaceContent carries wayland → output the latest committed content of a
344-// window: either Pixels, a private copy in B8G8R8A8 (wl_shm
345-// argb8888/xrgb8888 little-endian) with Stride bytes per row, or DMABuf, a
346-// GPU buffer the renderer reads in place. Receivers never modify either.
347-// Empty means the window has no content. LogicalW and LogicalH are the
348-// surface size in logical pixels (buffer scale and viewport applied).
349-// Subsurfaces come in Children; Geometry is the part of the surface that
350-// is the window (xdg window geometry), the rest being client shadows.
344+// window: SHM, a client shared-memory buffer, or DMABuf, a GPU buffer.
345+// Renderers read both in place and never modify them. Empty means the window
346+// has no content. LogicalW and LogicalH are the surface size in logical
347+// pixels (buffer scale and viewport applied). Subsurfaces come in Children;
348+// Geometry is the part of the surface that is the window (xdg window
349+// geometry), the rest being client shadows.
351350 type SurfaceContent struct {
352351 ID WindowID
353352 Width, Height int
354353 LogicalW, LogicalH int
355- Stride int
356354 Opaque bool // x formats: ignore the alpha byte
357- Pixels []byte
355+ SHM *SHMBuffer
358356 DMABuf *DMABuf
359357 // Children are the subsurfaces, bottom to top, flattened.
360358 Children []Subsurface
@@ -373,7 +371,20 @@ type Subsurface struct {
373371
374372 // Empty reports whether the content has nothing to draw.
375373 func (c SurfaceContent) Empty() bool {
376- return c.Pixels == nil && c.DMABuf == nil && len(c.Children) == 0
374+ return c.SHM == nil && c.DMABuf == nil && len(c.Children) == 0
375+}
376+
377+// SHMBuffer is a client wl_shm buffer: B8G8R8A8 pixels (argb8888/xrgb8888
378+// little-endian) at Offset in the pool File, Stride bytes per row. The
379+// client keeps writing the file, so renderers copy from it when they draw.
380+// Pool is unique per wl_shm pool for the session: renderers map a pool
381+// once. File stays open while the client's buffer exists and follows the
382+// DMABuf rules: duplicate it under File.SyscallConn before keeping it. A
383+// client may shrink the file at any time, so readers must survive faults.
384+type SHMBuffer struct {
385+ Pool uint64
386+ File *os.File
387+ Offset, Stride int
377388 }
378389
379390 // DMABuf is a client GPU buffer (linux-dmabuf). Its files stay open while
@@ -341,20 +341,18 @@ type SceneLayer struct {
341 }341 }
342 342
343 // SurfaceContent carries wayland → output the latest committed content of a343 // SurfaceContent carries wayland → output the latest committed content of a
344-// window: either Pixels, a private copy in B8G8R8A8 (wl_shm344+// window: SHM, a client shared-memory buffer, or DMABuf, a GPU buffer.
345-// argb8888/xrgb8888 little-endian) with Stride bytes per row, or DMABuf, a345+// Renderers read both in place and never modify them. Empty means the window
346-// GPU buffer the renderer reads in place. Receivers never modify either.346+// has no content. LogicalW and LogicalH are the surface size in logical
347-// Empty means the window has no content. LogicalW and LogicalH are the347+// pixels (buffer scale and viewport applied). Subsurfaces come in Children;
348-// surface size in logical pixels (buffer scale and viewport applied).348+// Geometry is the part of the surface that is the window (xdg window
349-// Subsurfaces come in Children; Geometry is the part of the surface that349+// geometry), the rest being client shadows.
350-// is the window (xdg window geometry), the rest being client shadows.
351 type SurfaceContent struct {350 type SurfaceContent struct {
352 ID WindowID351 ID WindowID
353 Width, Height int352 Width, Height int
354 LogicalW, LogicalH int353 LogicalW, LogicalH int
355- Stride int
356 Opaque bool // x formats: ignore the alpha byte354 Opaque bool // x formats: ignore the alpha byte
357- Pixels []byte355+ SHM *SHMBuffer
358 DMABuf *DMABuf356 DMABuf *DMABuf
359 // Children are the subsurfaces, bottom to top, flattened.357 // Children are the subsurfaces, bottom to top, flattened.
360 Children []Subsurface358 Children []Subsurface
@@ -373,7 +371,20 @@ type Subsurface struct {
373 371
374 // Empty reports whether the content has nothing to draw.372 // Empty reports whether the content has nothing to draw.
375 func (c SurfaceContent) Empty() bool {373 func (c SurfaceContent) Empty() bool {
376- return c.Pixels == nil && c.DMABuf == nil && len(c.Children) == 0374+ return c.SHM == nil && c.DMABuf == nil && len(c.Children) == 0
375+}
376+
377+// SHMBuffer is a client wl_shm buffer: B8G8R8A8 pixels (argb8888/xrgb8888
378+// little-endian) at Offset in the pool File, Stride bytes per row. The
379+// client keeps writing the file, so renderers copy from it when they draw.
380+// Pool is unique per wl_shm pool for the session: renderers map a pool
381+// once. File stays open while the client's buffer exists and follows the
382+// DMABuf rules: duplicate it under File.SyscallConn before keeping it. A
383+// client may shrink the file at any time, so readers must survive faults.
384+type SHMBuffer struct {
385+ Pool uint64
386+ File *os.File
387+ Offset, Stride int
377 }388 }
378 389
379 // DMABuf is a client GPU buffer (linux-dmabuf). Its files stay open while390 // DMABuf is a client GPU buffer (linux-dmabuf). Its files stay open while

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