fix(syncobj): reject points without an attached buffer, wait on acquire fences for subsurfaces and the overlay, and stop polling dropped waits
- commit
- a30b85b24cceadd1638560e254850c8a46b543f4
- author
- brice · · 2026-09-26 12:31 UTC
- parent
- 1d6f996
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +240 -32
M internal/adapters/drm/output.go +2 -0
| @@ -838,6 +838,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 838 | 838 | if done != nil { | |
| 839 | 839 | done.Close() | |
| 840 | 840 | } | |
| 841 | + | ov.close() | |
| 841 | 842 | ov = overlayWin{} | |
| 842 | 843 | if done, rerr = r.Render(scene, surfaces); rerr != nil { | |
| 843 | 844 | return fmt.Errorf("render frame: %w", rerr) | |
| @@ -850,6 +851,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 850 | 851 | if done != nil { | |
| 851 | 852 | done.Close() | |
| 852 | 853 | } | |
| 854 | + | ov.close() | |
| 853 | 855 | if err != nil && o.overlayConflict(err, ov.buf) { | |
| 854 | 856 | err = errOverlayDropped | |
| 855 | 857 | } | |
| @@ -838,6 +838,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 838 | if done != nil { | 838 | if done != nil { | ||
| 839 | done.Close() | 839 | done.Close() | ||
| 840 | } | 840 | } | ||
| 841 | + | ov.close() | |||
| 841 | ov = overlayWin{} | 842 | ov = overlayWin{} | ||
| 842 | if done, rerr = r.Render(scene, surfaces); rerr != nil { | 843 | if done, rerr = r.Render(scene, surfaces); rerr != nil { | ||
| 843 | return fmt.Errorf("render frame: %w", rerr) | 844 | return fmt.Errorf("render frame: %w", rerr) | ||
| @@ -850,6 +851,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 850 | if done != nil { | 851 | if done != nil { | ||
| 851 | done.Close() | 852 | done.Close() | ||
| 852 | } | 853 | } | ||
| 854 | + | ov.close() | |||
| 853 | if err != nil && o.overlayConflict(err, ov.buf) { | 855 | if err != nil && o.overlayConflict(err, ov.buf) { | ||
| 854 | err = errOverlayDropped | 856 | err = errOverlayDropped | ||
| 855 | } | 857 | } | ||
M internal/adapters/drm/overlay.go +17 -1
| @@ -2,6 +2,7 @@ package drm | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | + | "os" | |
| 5 | 6 | "time" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/nefertty/internal/ports" | |
| @@ -49,6 +50,9 @@ type overlayWin struct { | |||
| 49 | 50 | buf uint64 | |
| 50 | 51 | rect ports.Rect // CRTC rect | |
| 51 | 52 | w, h int // buffer size | |
| 53 | + | // acquire is the client's explicit-sync fence, a duplicate owned by | |
| 54 | + | // the frame loop (closed after the commit); nil without one. | |
| 55 | + | acquire *os.File | |
| 52 | 56 | } | |
| 53 | 57 | ||
| 54 | 58 | // overlayCandidate finds the one window the overlay can show: an opaque | |
| @@ -121,7 +125,8 @@ func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.S | |||
| 121 | 125 | scale = 1 | |
| 122 | 126 | } | |
| 123 | 127 | ov = overlayWin{id: w.ID, fb: fb, buf: c.DMABuf.ID, w: c.Width, h: c.Height, | |
| 124 | - | rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height}} | |
| 128 | + | rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height}, | |
| 129 | + | acquire: dupFence(c.Acquire)} | |
| 125 | 130 | } | |
| 126 | 131 | } | |
| 127 | 132 | } | |
| @@ -164,6 +169,17 @@ func (o *Output) overlayProps(req *atomicReq, ov overlayWin) { | |||
| 164 | 169 | req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y))) | |
| 165 | 170 | req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W)) | |
| 166 | 171 | req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H)) | |
| 172 | + | if ov.acquire != nil { | |
| 173 | + | // KMS waits for the client's GPU work before scanning it out. | |
| 174 | + | req.set(p.id, p.prop("IN_FENCE_FD"), uint64(ov.acquire.Fd())) | |
| 175 | + | } | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | // close releases the frame's duplicate of the acquire fence. | |
| 179 | + | func (ov overlayWin) close() { | |
| 180 | + | if ov.acquire != nil { | |
| 181 | + | ov.acquire.Close() | |
| 182 | + | } | |
| 167 | 183 | } | |
| 168 | 184 | ||
| 169 | 185 | // testOverlay asks KMS whether it takes the frame with ov on the overlay, | |
| @@ -2,6 +2,7 @@ package drm | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "errors" | 4 | "errors" | ||
| 5 | + | "os" | |||
| 5 | "time" | 6 | "time" | ||
| 6 | 7 | ||||
| 7 | "github.com/bnema/nefertty/internal/ports" | 8 | "github.com/bnema/nefertty/internal/ports" | ||
| @@ -49,6 +50,9 @@ type overlayWin struct { | |||||
| 49 | buf uint64 | 50 | buf uint64 | ||
| 50 | rect ports.Rect // CRTC rect | 51 | rect ports.Rect // CRTC rect | ||
| 51 | w, h int // buffer size | 52 | w, h int // buffer size | ||
| 53 | + | // acquire is the client's explicit-sync fence, a duplicate owned by | |||
| 54 | + | // the frame loop (closed after the commit); nil without one. | |||
| 55 | + | acquire *os.File | |||
| 52 | } | 56 | } | ||
| 53 | 57 | ||||
| 54 | // overlayCandidate finds the one window the overlay can show: an opaque | 58 | // overlayCandidate finds the one window the overlay can show: an opaque | ||
| @@ -121,7 +125,8 @@ func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.S | |||||
| 121 | scale = 1 | 125 | scale = 1 | ||
| 122 | } | 126 | } | ||
| 123 | ov = overlayWin{id: w.ID, fb: fb, buf: c.DMABuf.ID, w: c.Width, h: c.Height, | 127 | ov = overlayWin{id: w.ID, fb: fb, buf: c.DMABuf.ID, w: c.Width, h: c.Height, | ||
| 124 | - | rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height}} | 128 | + | rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height}, |
| 129 | + | acquire: dupFence(c.Acquire)} | |||
| 125 | } | 130 | } | ||
| 126 | } | 131 | } | ||
| 127 | } | 132 | } | ||
| @@ -164,6 +169,17 @@ func (o *Output) overlayProps(req *atomicReq, ov overlayWin) { | |||||
| 164 | req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y))) | 169 | req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y))) | ||
| 165 | req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W)) | 170 | req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W)) | ||
| 166 | req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H)) | 171 | req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H)) | ||
| 172 | + | if ov.acquire != nil { | |||
| 173 | + | // KMS waits for the client's GPU work before scanning it out. | |||
| 174 | + | req.set(p.id, p.prop("IN_FENCE_FD"), uint64(ov.acquire.Fd())) | |||
| 175 | + | } | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | // close releases the frame's duplicate of the acquire fence. | |||
| 179 | + | func (ov overlayWin) close() { | |||
| 180 | + | if ov.acquire != nil { | |||
| 181 | + | ov.acquire.Close() | |||
| 182 | + | } | |||
| 167 | } | 183 | } | ||
| 168 | 184 | ||||
| 169 | // testOverlay asks KMS whether it takes the frame with ov on the overlay, | 185 | // testOverlay asks KMS whether it takes the frame with ov on the overlay, | ||
M internal/adapters/drm/overlay_test.go +35 -0
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package drm | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "os" | |
| 4 | 5 | "testing" | |
| 5 | 6 | "time" | |
| 6 | 7 | ||
| @@ -145,3 +146,37 @@ func TestOverlayCursorConflictDropsOverlay(t *testing.T) { | |||
| 145 | 146 | t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason) | |
| 146 | 147 | } | |
| 147 | 148 | } | |
| 149 | + | ||
| 150 | + | // An explicit-sync window on the overlay: the plane carries a duplicate of | |
| 151 | + | // the client's acquire fence, closed once the frame is committed. | |
| 152 | + | func TestOverlayCarriesAcquireFence(t *testing.T) { | |
| 153 | + | o, k, commits := overlayOutput(t) | |
| 154 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |
| 155 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 156 | + | s, c := overlayScene() | |
| 157 | + | r, w, err := os.Pipe() | |
| 158 | + | if err != nil { | |
| 159 | + | t.Fatal(err) | |
| 160 | + | } | |
| 161 | + | defer r.Close() | |
| 162 | + | defer w.Close() | |
| 163 | + | win := c[2] | |
| 164 | + | win.Acquire = r | |
| 165 | + | c[2] = win | |
| 166 | + | ov, _ := o.overlayFrame(s, c) | |
| 167 | + | if ov.acquire == nil || ov.acquire.Fd() == r.Fd() { | |
| 168 | + | t.Fatal("overlay has no duplicate of the acquire fence") | |
| 169 | + | } | |
| 170 | + | fd := ov.acquire.Fd() | |
| 171 | + | if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil { | |
| 172 | + | t.Fatal(err) | |
| 173 | + | } | |
| 174 | + | ov.close() | |
| 175 | + | last := (*commits)[len(*commits)-1] | |
| 176 | + | if v, ok := last.req.value(tOverlay, pFence); !ok || v != uint64(fd) { | |
| 177 | + | t.Fatalf("overlay IN_FENCE_FD %d %v, want %d", v, ok, fd) | |
| 178 | + | } | |
| 179 | + | if _, err := ov.acquire.Stat(); err == nil { | |
| 180 | + | t.Fatal("duplicate left open") | |
| 181 | + | } | |
| 182 | + | } | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package drm | 1 | package drm | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "os" | |||
| 4 | "testing" | 5 | "testing" | ||
| 5 | "time" | 6 | "time" | ||
| 6 | 7 | ||||
| @@ -145,3 +146,37 @@ func TestOverlayCursorConflictDropsOverlay(t *testing.T) { | |||||
| 145 | t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason) | 146 | t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason) | ||
| 146 | } | 147 | } | ||
| 147 | } | 148 | } | ||
| 149 | + | ||||
| 150 | + | // An explicit-sync window on the overlay: the plane carries a duplicate of | |||
| 151 | + | // the client's acquire fence, closed once the frame is committed. | |||
| 152 | + | func TestOverlayCarriesAcquireFence(t *testing.T) { | |||
| 153 | + | o, k, commits := overlayOutput(t) | |||
| 154 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |||
| 155 | + | o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 156 | + | s, c := overlayScene() | |||
| 157 | + | r, w, err := os.Pipe() | |||
| 158 | + | if err != nil { | |||
| 159 | + | t.Fatal(err) | |||
| 160 | + | } | |||
| 161 | + | defer r.Close() | |||
| 162 | + | defer w.Close() | |||
| 163 | + | win := c[2] | |||
| 164 | + | win.Acquire = r | |||
| 165 | + | c[2] = win | |||
| 166 | + | ov, _ := o.overlayFrame(s, c) | |||
| 167 | + | if ov.acquire == nil || ov.acquire.Fd() == r.Fd() { | |||
| 168 | + | t.Fatal("overlay has no duplicate of the acquire fence") | |||
| 169 | + | } | |||
| 170 | + | fd := ov.acquire.Fd() | |||
| 171 | + | if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil { | |||
| 172 | + | t.Fatal(err) | |||
| 173 | + | } | |||
| 174 | + | ov.close() | |||
| 175 | + | last := (*commits)[len(*commits)-1] | |||
| 176 | + | if v, ok := last.req.value(tOverlay, pFence); !ok || v != uint64(fd) { | |||
| 177 | + | t.Fatalf("overlay IN_FENCE_FD %d %v, want %d", v, ok, fd) | |||
| 178 | + | } | |||
| 179 | + | if _, err := ov.acquire.Stat(); err == nil { | |||
| 180 | + | t.Fatal("duplicate left open") | |||
| 181 | + | } | |||
| 182 | + | } | |||
M internal/adapters/vulkan/dmabuf_test.go +29 -0
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package vulkan | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "image" | |
| 4 | 5 | "image/color" | |
| 5 | 6 | "os" | |
| 6 | 7 | "slices" | |
| @@ -106,3 +107,31 @@ func TestRendererImportsDMABuf(t *testing.T) { | |||
| 106 | 107 | t.Fatalf("import kept: %d", len(r.imports)) | |
| 107 | 108 | } | |
| 108 | 109 | } | |
| 110 | + | ||
| 111 | + | // A subsurface dmabuf is read after its own acquire fence, not the root's. | |
| 112 | + | func TestRendererSubsurfaceAcquire(t *testing.T) { | |
| 113 | + | r, err := New(80, 32) | |
| 114 | + | if err != nil { | |
| 115 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 116 | + | } | |
| 117 | + | defer r.Close() | |
| 118 | + | linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0} | |
| 119 | + | if !slices.Contains(r.DMABuf().Formats, linear) { | |
| 120 | + | t.Skipf("linear ARGB8888 not importable: %+v", r.DMABuf()) | |
| 121 | + | } | |
| 122 | + | f := udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{0, 0, 255, 255} }) | |
| 123 | + | fence, w, _ := os.Pipe() | |
| 124 | + | defer fence.Close() | |
| 125 | + | defer w.Close() | |
| 126 | + | child := ports.SurfaceContent{ID: 1, Width: 64, Height: 16, Acquire: fence, | |
| 127 | + | DMABuf: &ports.DMABuf{ID: 8, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}} | |
| 128 | + | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}} | |
| 129 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, Children: []ports.Subsurface{{SurfaceContent: child}}}} | |
| 130 | + | ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 80, 32))) | |
| 131 | + | for _, d := range ds { | |
| 132 | + | if d.im != nil && d.acquire == fence { | |
| 133 | + | return | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | t.Fatal("subsurface draw has no acquire fence") | |
| 137 | + | } | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package vulkan | 1 | package vulkan | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "image" | |||
| 4 | "image/color" | 5 | "image/color" | ||
| 5 | "os" | 6 | "os" | ||
| 6 | "slices" | 7 | "slices" | ||
| @@ -106,3 +107,31 @@ func TestRendererImportsDMABuf(t *testing.T) { | |||||
| 106 | t.Fatalf("import kept: %d", len(r.imports)) | 107 | t.Fatalf("import kept: %d", len(r.imports)) | ||
| 107 | } | 108 | } | ||
| 108 | } | 109 | } | ||
| 110 | + | ||||
| 111 | + | // A subsurface dmabuf is read after its own acquire fence, not the root's. | |||
| 112 | + | func TestRendererSubsurfaceAcquire(t *testing.T) { | |||
| 113 | + | r, err := New(80, 32) | |||
| 114 | + | if err != nil { | |||
| 115 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 116 | + | } | |||
| 117 | + | defer r.Close() | |||
| 118 | + | linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0} | |||
| 119 | + | if !slices.Contains(r.DMABuf().Formats, linear) { | |||
| 120 | + | t.Skipf("linear ARGB8888 not importable: %+v", r.DMABuf()) | |||
| 121 | + | } | |||
| 122 | + | f := udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{0, 0, 255, 255} }) | |||
| 123 | + | fence, w, _ := os.Pipe() | |||
| 124 | + | defer fence.Close() | |||
| 125 | + | defer w.Close() | |||
| 126 | + | child := ports.SurfaceContent{ID: 1, Width: 64, Height: 16, Acquire: fence, | |||
| 127 | + | DMABuf: &ports.DMABuf{ID: 8, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}} | |||
| 128 | + | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}} | |||
| 129 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, Children: []ports.Subsurface{{SurfaceContent: child}}}} | |||
| 130 | + | ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 80, 32))) | |||
| 131 | + | for _, d := range ds { | |||
| 132 | + | if d.im != nil && d.acquire == fence { | |||
| 133 | + | return | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | t.Fatal("subsurface draw has no acquire fence") | |||
| 137 | + | } | |||
M internal/adapters/vulkan/renderer.go +1 -3
| @@ -333,9 +333,7 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||
| 333 | 333 | } | |
| 334 | 334 | dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque) | |
| 335 | 335 | dr.set, dr.im = im.set, im | |
| 336 | - | if key.index == 0 { | |
| 337 | - | dr.acquire = content.Acquire | |
| 338 | - | } | |
| 336 | + | dr.acquire = content.Acquire | |
| 339 | 337 | draws = append(draws, dr) | |
| 340 | 338 | return | |
| 341 | 339 | } | |
| @@ -333,9 +333,7 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||||
| 333 | } | 333 | } | ||
| 334 | dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque) | 334 | dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque) | ||
| 335 | dr.set, dr.im = im.set, im | 335 | dr.set, dr.im = im.set, im | ||
| 336 | - | if key.index == 0 { | 336 | + | dr.acquire = content.Acquire |
| 337 | - | dr.acquire = content.Acquire | |||
| 338 | - | } | |||
| 339 | draws = append(draws, dr) | 337 | draws = append(draws, dr) | ||
| 340 | return | 338 | return | ||
| 341 | } | 339 | } | ||
M internal/adapters/wayland/inhibit.go +2 -2
| @@ -18,8 +18,8 @@ import ( | |||
| 18 | 18 | type inhibitor struct { | |
| 19 | 19 | surf *surface | |
| 20 | 20 | shortcut *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1 // nil: idle | |
| 21 | - | window ports.WindowID // reported to core (0: not yet) | |
| 22 | - | active bool // shortcuts: last state sent | |
| 21 | + | window ports.WindowID // reported to core (0: not yet) | |
| 22 | + | active bool // shortcuts: last state sent | |
| 23 | 23 | } | |
| 24 | 24 | ||
| 25 | 25 | func registerInhibit(d *server.Display, s *Server) error { | |
| @@ -18,8 +18,8 @@ import ( | |||||
| 18 | type inhibitor struct { | 18 | type inhibitor struct { | ||
| 19 | surf *surface | 19 | surf *surface | ||
| 20 | shortcut *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1 // nil: idle | 20 | shortcut *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1 // nil: idle | ||
| 21 | - | window ports.WindowID // reported to core (0: not yet) | 21 | + | window ports.WindowID // reported to core (0: not yet) |
| 22 | - | active bool // shortcuts: last state sent | 22 | + | active bool // shortcuts: last state sent |
| 23 | } | 23 | } | ||
| 24 | 24 | ||||
| 25 | func registerInhibit(d *server.Display, s *Server) error { | 25 | func registerInhibit(d *server.Display, s *Server) error { | ||
M internal/adapters/wayland/presentation.go +5 -5
| @@ -22,11 +22,11 @@ const presentationVersion = 2 | |||
| 22 | 22 | // feedbackWait is a committed feedback: presented once a flip on the | |
| 23 | 23 | // surface's output shows content seq of window win. | |
| 24 | 24 | type feedbackWait struct { | |
| 25 | - | fb *presentationtime.WpPresentationFeedback | |
| 26 | - | surf *surface | |
| 27 | - | win ports.WindowID | |
| 28 | - | seq uint64 | |
| 29 | - | at time.Time | |
| 25 | + | fb *presentationtime.WpPresentationFeedback | |
| 26 | + | surf *surface | |
| 27 | + | win ports.WindowID | |
| 28 | + | seq uint64 | |
| 29 | + | at time.Time | |
| 30 | 30 | // replaced is the window content Seq of the surface's next commit (0: | |
| 31 | 31 | // none yet): a flip showing it or later shows the replacement. | |
| 32 | 32 | replaced uint64 | |
| @@ -22,11 +22,11 @@ const presentationVersion = 2 | |||||
| 22 | // feedbackWait is a committed feedback: presented once a flip on the | 22 | // feedbackWait is a committed feedback: presented once a flip on the | ||
| 23 | // surface's output shows content seq of window win. | 23 | // surface's output shows content seq of window win. | ||
| 24 | type feedbackWait struct { | 24 | type feedbackWait struct { | ||
| 25 | - | fb *presentationtime.WpPresentationFeedback | 25 | + | fb *presentationtime.WpPresentationFeedback |
| 26 | - | surf *surface | 26 | + | surf *surface |
| 27 | - | win ports.WindowID | 27 | + | win ports.WindowID |
| 28 | - | seq uint64 | 28 | + | seq uint64 |
| 29 | - | at time.Time | 29 | + | at time.Time |
| 30 | // replaced is the window content Seq of the surface's next commit (0: | 30 | // replaced is the window content Seq of the surface's next commit (0: | ||
| 31 | // none yet): a flip showing it or later shows the replacement. | 31 | // none yet): a flip showing it or later shows the replacement. | ||
| 32 | replaced uint64 | 32 | replaced uint64 | ||
M internal/adapters/wayland/syncobj.go +41 -19
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "errors" | |
| 6 | 6 | "os" | |
| 7 | 7 | "sync" | |
| 8 | + | "sync/atomic" | |
| 8 | 9 | ||
| 9 | 10 | "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj" | |
| 10 | 11 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| @@ -147,11 +148,12 @@ func (s *surface) checkSyncCommit() bool { | |||
| 147 | 148 | return true | |
| 148 | 149 | } | |
| 149 | 150 | a, r := st.acquire, st.release | |
| 150 | - | hasBuffer := s.attached && s.pending != nil || !s.attached && s.current != nil | |
| 151 | + | // Points belong only to a commit that attaches a non-null buffer. | |
| 152 | + | hasBuffer := s.attached && s.pending != nil | |
| 151 | 153 | res := st.resource | |
| 152 | 154 | switch { | |
| 153 | 155 | case !a.set() && !r.set(): | |
| 154 | - | if hasBuffer && s.attached { | |
| 156 | + | if hasBuffer { | |
| 155 | 157 | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "buffer without acquire point") | |
| 156 | 158 | return false | |
| 157 | 159 | } | |
| @@ -194,13 +196,14 @@ type syncWaiter struct { | |||
| 194 | 196 | wake func() | |
| 195 | 197 | pipeR *os.File | |
| 196 | 198 | pipeW *os.File | |
| 197 | - | add chan *syncWait | |
| 199 | + | added []*syncWait // new waits for run (under mu) | |
| 198 | 200 | } | |
| 199 | 201 | ||
| 200 | 202 | // syncWait is one acquire point a queued commit waits for. | |
| 201 | 203 | type syncWait struct { | |
| 202 | - | efd int | |
| 203 | - | done bool // the eventfd fired (pacer, under Do) | |
| 204 | + | efd int | |
| 205 | + | done bool // the eventfd fired or the wait was cancelled (pacer, under Do) | |
| 206 | + | cancelled atomic.Bool // run stops polling it and closes efd | |
| 204 | 207 | } | |
| 205 | 208 | ||
| 206 | 209 | func newSyncWaiter(wake func()) (*syncWaiter, error) { | |
| @@ -208,7 +211,7 @@ func newSyncWaiter(wake func()) (*syncWaiter, error) { | |||
| 208 | 211 | if err != nil { | |
| 209 | 212 | return nil, err | |
| 210 | 213 | } | |
| 211 | - | return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, add: make(chan *syncWait, 64)}, nil | |
| 214 | + | return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w}, nil | |
| 212 | 215 | } | |
| 213 | 216 | ||
| 214 | 217 | // watch registers an eventfd for p; the wait is ready once it fires. | |
| @@ -222,7 +225,9 @@ func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | |||
| 222 | 225 | return nil, err | |
| 223 | 226 | } | |
| 224 | 227 | w := &syncWait{efd: efd} | |
| 225 | - | sw.add <- w | |
| 228 | + | sw.mu.Lock() | |
| 229 | + | sw.added = append(sw.added, w) | |
| 230 | + | sw.mu.Unlock() | |
| 226 | 231 | _, _ = sw.pipeW.Write([]byte{0}) | |
| 227 | 232 | return w, nil | |
| 228 | 233 | } | |
| @@ -240,10 +245,12 @@ func (sw *syncWaiter) fired(w *syncWait) bool { | |||
| 240 | 245 | ||
| 241 | 246 | // cancel stops watching w (the commit was dropped). | |
| 242 | 247 | func (sw *syncWaiter) cancel(w *syncWait) { | |
| 248 | + | w.cancelled.Store(true) | |
| 243 | 249 | sw.mu.Lock() | |
| 244 | 250 | delete(sw.ready, w) | |
| 245 | 251 | sw.mu.Unlock() | |
| 246 | 252 | w.done = true | |
| 253 | + | _, _ = sw.pipeW.Write([]byte{0}) // run closes its eventfd | |
| 247 | 254 | } | |
| 248 | 255 | ||
| 249 | 256 | // run polls every watched eventfd; it owns and closes them. | |
| @@ -251,11 +258,24 @@ func (sw *syncWaiter) run(ctx context.Context) { | |||
| 251 | 258 | defer sw.pipeR.Close() | |
| 252 | 259 | var waits []*syncWait | |
| 253 | 260 | defer func() { | |
| 261 | + | sw.mu.Lock() | |
| 262 | + | waits = append(waits, sw.added...) | |
| 263 | + | sw.added = nil | |
| 264 | + | sw.mu.Unlock() | |
| 254 | 265 | for _, w := range waits { | |
| 255 | 266 | unix.Close(w.efd) | |
| 256 | 267 | } | |
| 257 | 268 | }() | |
| 258 | 269 | for ctx.Err() == nil { | |
| 270 | + | live := waits[:0] | |
| 271 | + | for _, w := range waits { | |
| 272 | + | if w.cancelled.Load() { | |
| 273 | + | unix.Close(w.efd) | |
| 274 | + | continue | |
| 275 | + | } | |
| 276 | + | live = append(live, w) | |
| 277 | + | } | |
| 278 | + | waits = live | |
| 259 | 279 | fds := []unix.PollFd{{Fd: int32(sw.pipeR.Fd()), Events: unix.POLLIN}} | |
| 260 | 280 | for _, w := range waits { | |
| 261 | 281 | fds = append(fds, unix.PollFd{Fd: int32(w.efd), Events: unix.POLLIN}) | |
| @@ -285,15 +305,10 @@ func (sw *syncWaiter) run(ctx context.Context) { | |||
| 285 | 305 | if fds[0].Revents != 0 { | |
| 286 | 306 | var b [64]byte | |
| 287 | 307 | _, _ = sw.pipeR.Read(b[:]) | |
| 288 | - | drain: | |
| 289 | - | for { | |
| 290 | - | select { | |
| 291 | - | case w := <-sw.add: | |
| 292 | - | waits = append(waits, w) | |
| 293 | - | default: | |
| 294 | - | break drain | |
| 295 | - | } | |
| 296 | - | } | |
| 308 | + | sw.mu.Lock() | |
| 309 | + | waits = append(waits, sw.added...) | |
| 310 | + | sw.added = nil | |
| 311 | + | sw.mu.Unlock() | |
| 297 | 312 | } | |
| 298 | 313 | if fired { | |
| 299 | 314 | sw.wake() | |
| @@ -362,18 +377,25 @@ func (s *surface) syncReady(cs *commitSync) bool { | |||
| 362 | 377 | return cs == nil || cs.wait == nil || s.server.syncWait.fired(cs.wait) | |
| 363 | 378 | } | |
| 364 | 379 | ||
| 365 | - | // dropSync forgets a commit that will never apply: its points are | |
| 366 | - | // signalled so the client can reuse the buffer. | |
| 380 | + | // dropSync forgets a commit that will never apply. Its release point is | |
| 381 | + | // signalled only once the acquire point has a fence: signalling earlier | |
| 382 | + | // could move a shared timeline past a point the client has not submitted. | |
| 367 | 383 | func (s *surface) dropSync(cs *commitSync) { | |
| 368 | 384 | if cs == nil { | |
| 369 | 385 | return | |
| 370 | 386 | } | |
| 387 | + | ready := cs.wait != nil && s.server.syncWait.fired(cs.wait) | |
| 371 | 388 | if cs.wait != nil { | |
| 372 | 389 | s.server.syncWait.cancel(cs.wait) | |
| 373 | 390 | } | |
| 374 | 391 | cs.acquire.tl.uses-- | |
| 375 | 392 | s.server.dropTimeline(cs.acquire.tl) | |
| 376 | - | s.server.releaseSync(syncHold{release: cs.release}) | |
| 393 | + | if ready { | |
| 394 | + | s.server.releaseSync(syncHold{release: cs.release}) | |
| 395 | + | return | |
| 396 | + | } | |
| 397 | + | cs.release.tl.uses-- | |
| 398 | + | s.server.dropTimeline(cs.release.tl) | |
| 377 | 399 | } | |
| 378 | 400 | ||
| 379 | 401 | // applySync makes a commit's points the current buffer's: its acquire | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "os" | 6 | "os" | ||
| 7 | "sync" | 7 | "sync" | ||
| 8 | + | "sync/atomic" | |||
| 8 | 9 | ||||
| 9 | "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj" | 10 | "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj" | ||
| 10 | "github.com/bnema/purego-libwayland/protocol/wayland" | 11 | "github.com/bnema/purego-libwayland/protocol/wayland" | ||
| @@ -147,11 +148,12 @@ func (s *surface) checkSyncCommit() bool { | |||||
| 147 | return true | 148 | return true | ||
| 148 | } | 149 | } | ||
| 149 | a, r := st.acquire, st.release | 150 | a, r := st.acquire, st.release | ||
| 150 | - | hasBuffer := s.attached && s.pending != nil || !s.attached && s.current != nil | 151 | + | // Points belong only to a commit that attaches a non-null buffer. |
| 152 | + | hasBuffer := s.attached && s.pending != nil | |||
| 151 | res := st.resource | 153 | res := st.resource | ||
| 152 | switch { | 154 | switch { | ||
| 153 | case !a.set() && !r.set(): | 155 | case !a.set() && !r.set(): | ||
| 154 | - | if hasBuffer && s.attached { | 156 | + | if hasBuffer { |
| 155 | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "buffer without acquire point") | 157 | res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "buffer without acquire point") | ||
| 156 | return false | 158 | return false | ||
| 157 | } | 159 | } | ||
| @@ -194,13 +196,14 @@ type syncWaiter struct { | |||||
| 194 | wake func() | 196 | wake func() | ||
| 195 | pipeR *os.File | 197 | pipeR *os.File | ||
| 196 | pipeW *os.File | 198 | pipeW *os.File | ||
| 197 | - | add chan *syncWait | 199 | + | added []*syncWait // new waits for run (under mu) |
| 198 | } | 200 | } | ||
| 199 | 201 | ||||
| 200 | // syncWait is one acquire point a queued commit waits for. | 202 | // syncWait is one acquire point a queued commit waits for. | ||
| 201 | type syncWait struct { | 203 | type syncWait struct { | ||
| 202 | - | efd int | 204 | + | efd int |
| 203 | - | done bool // the eventfd fired (pacer, under Do) | 205 | + | done bool // the eventfd fired or the wait was cancelled (pacer, under Do) |
| 206 | + | cancelled atomic.Bool // run stops polling it and closes efd | |||
| 204 | } | 207 | } | ||
| 205 | 208 | ||||
| 206 | func newSyncWaiter(wake func()) (*syncWaiter, error) { | 209 | func newSyncWaiter(wake func()) (*syncWaiter, error) { | ||
| @@ -208,7 +211,7 @@ func newSyncWaiter(wake func()) (*syncWaiter, error) { | |||||
| 208 | if err != nil { | 211 | if err != nil { | ||
| 209 | return nil, err | 212 | return nil, err | ||
| 210 | } | 213 | } | ||
| 211 | - | return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, add: make(chan *syncWait, 64)}, nil | 214 | + | return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w}, nil |
| 212 | } | 215 | } | ||
| 213 | 216 | ||||
| 214 | // watch registers an eventfd for p; the wait is ready once it fires. | 217 | // watch registers an eventfd for p; the wait is ready once it fires. | ||
| @@ -222,7 +225,9 @@ func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | |||||
| 222 | return nil, err | 225 | return nil, err | ||
| 223 | } | 226 | } | ||
| 224 | w := &syncWait{efd: efd} | 227 | w := &syncWait{efd: efd} | ||
| 225 | - | sw.add <- w | 228 | + | sw.mu.Lock() |
| 229 | + | sw.added = append(sw.added, w) | |||
| 230 | + | sw.mu.Unlock() | |||
| 226 | _, _ = sw.pipeW.Write([]byte{0}) | 231 | _, _ = sw.pipeW.Write([]byte{0}) | ||
| 227 | return w, nil | 232 | return w, nil | ||
| 228 | } | 233 | } | ||
| @@ -240,10 +245,12 @@ func (sw *syncWaiter) fired(w *syncWait) bool { | |||||
| 240 | 245 | ||||
| 241 | // cancel stops watching w (the commit was dropped). | 246 | // cancel stops watching w (the commit was dropped). | ||
| 242 | func (sw *syncWaiter) cancel(w *syncWait) { | 247 | func (sw *syncWaiter) cancel(w *syncWait) { | ||
| 248 | + | w.cancelled.Store(true) | |||
| 243 | sw.mu.Lock() | 249 | sw.mu.Lock() | ||
| 244 | delete(sw.ready, w) | 250 | delete(sw.ready, w) | ||
| 245 | sw.mu.Unlock() | 251 | sw.mu.Unlock() | ||
| 246 | w.done = true | 252 | w.done = true | ||
| 253 | + | _, _ = sw.pipeW.Write([]byte{0}) // run closes its eventfd | |||
| 247 | } | 254 | } | ||
| 248 | 255 | ||||
| 249 | // run polls every watched eventfd; it owns and closes them. | 256 | // run polls every watched eventfd; it owns and closes them. | ||
| @@ -251,11 +258,24 @@ func (sw *syncWaiter) run(ctx context.Context) { | |||||
| 251 | defer sw.pipeR.Close() | 258 | defer sw.pipeR.Close() | ||
| 252 | var waits []*syncWait | 259 | var waits []*syncWait | ||
| 253 | defer func() { | 260 | defer func() { | ||
| 261 | + | sw.mu.Lock() | |||
| 262 | + | waits = append(waits, sw.added...) | |||
| 263 | + | sw.added = nil | |||
| 264 | + | sw.mu.Unlock() | |||
| 254 | for _, w := range waits { | 265 | for _, w := range waits { | ||
| 255 | unix.Close(w.efd) | 266 | unix.Close(w.efd) | ||
| 256 | } | 267 | } | ||
| 257 | }() | 268 | }() | ||
| 258 | for ctx.Err() == nil { | 269 | for ctx.Err() == nil { | ||
| 270 | + | live := waits[:0] | |||
| 271 | + | for _, w := range waits { | |||
| 272 | + | if w.cancelled.Load() { | |||
| 273 | + | unix.Close(w.efd) | |||
| 274 | + | continue | |||
| 275 | + | } | |||
| 276 | + | live = append(live, w) | |||
| 277 | + | } | |||
| 278 | + | waits = live | |||
| 259 | fds := []unix.PollFd{{Fd: int32(sw.pipeR.Fd()), Events: unix.POLLIN}} | 279 | fds := []unix.PollFd{{Fd: int32(sw.pipeR.Fd()), Events: unix.POLLIN}} | ||
| 260 | for _, w := range waits { | 280 | for _, w := range waits { | ||
| 261 | fds = append(fds, unix.PollFd{Fd: int32(w.efd), Events: unix.POLLIN}) | 281 | fds = append(fds, unix.PollFd{Fd: int32(w.efd), Events: unix.POLLIN}) | ||
| @@ -285,15 +305,10 @@ func (sw *syncWaiter) run(ctx context.Context) { | |||||
| 285 | if fds[0].Revents != 0 { | 305 | if fds[0].Revents != 0 { | ||
| 286 | var b [64]byte | 306 | var b [64]byte | ||
| 287 | _, _ = sw.pipeR.Read(b[:]) | 307 | _, _ = sw.pipeR.Read(b[:]) | ||
| 288 | - | drain: | 308 | + | sw.mu.Lock() |
| 289 | - | for { | 309 | + | waits = append(waits, sw.added...) |
| 290 | - | select { | 310 | + | sw.added = nil |
| 291 | - | case w := <-sw.add: | 311 | + | sw.mu.Unlock() |
| 292 | - | waits = append(waits, w) | |||
| 293 | - | default: | |||
| 294 | - | break drain | |||
| 295 | - | } | |||
| 296 | - | } | |||
| 297 | } | 312 | } | ||
| 298 | if fired { | 313 | if fired { | ||
| 299 | sw.wake() | 314 | sw.wake() | ||
| @@ -362,18 +377,25 @@ func (s *surface) syncReady(cs *commitSync) bool { | |||||
| 362 | return cs == nil || cs.wait == nil || s.server.syncWait.fired(cs.wait) | 377 | return cs == nil || cs.wait == nil || s.server.syncWait.fired(cs.wait) | ||
| 363 | } | 378 | } | ||
| 364 | 379 | ||||
| 365 | - | // dropSync forgets a commit that will never apply: its points are | 380 | + | // dropSync forgets a commit that will never apply. Its release point is |
| 366 | - | // signalled so the client can reuse the buffer. | 381 | + | // signalled only once the acquire point has a fence: signalling earlier |
| 382 | + | // could move a shared timeline past a point the client has not submitted. | |||
| 367 | func (s *surface) dropSync(cs *commitSync) { | 383 | func (s *surface) dropSync(cs *commitSync) { | ||
| 368 | if cs == nil { | 384 | if cs == nil { | ||
| 369 | return | 385 | return | ||
| 370 | } | 386 | } | ||
| 387 | + | ready := cs.wait != nil && s.server.syncWait.fired(cs.wait) | |||
| 371 | if cs.wait != nil { | 388 | if cs.wait != nil { | ||
| 372 | s.server.syncWait.cancel(cs.wait) | 389 | s.server.syncWait.cancel(cs.wait) | ||
| 373 | } | 390 | } | ||
| 374 | cs.acquire.tl.uses-- | 391 | cs.acquire.tl.uses-- | ||
| 375 | s.server.dropTimeline(cs.acquire.tl) | 392 | s.server.dropTimeline(cs.acquire.tl) | ||
| 376 | - | s.server.releaseSync(syncHold{release: cs.release}) | 393 | + | if ready { |
| 394 | + | s.server.releaseSync(syncHold{release: cs.release}) | |||
| 395 | + | return | |||
| 396 | + | } | |||
| 397 | + | cs.release.tl.uses-- | |||
| 398 | + | s.server.dropTimeline(cs.release.tl) | |||
| 377 | } | 399 | } | ||
| 378 | 400 | ||||
| 379 | // applySync makes a commit's points the current buffer's: its acquire | 401 | // applySync makes a commit's points the current buffer's: its acquire | ||
M internal/adapters/wayland/syncobj_test.go +108 -2
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "os" | |
| 6 | 6 | "sync" | |
| 7 | + | "strings" | |
| 7 | 8 | "testing" | |
| 8 | 9 | "time" | |
| 9 | 10 | "unsafe" | |
| @@ -185,11 +186,15 @@ func (p *syncReleaseProxy) Dispatch(e *wlturbo.Event) { | |||
| 185 | 186 | } | |
| 186 | 187 | } | |
| 187 | 188 | ||
| 189 | + | func (h *syncHarness) setPoints(acquire, release uint64) { | |
| 190 | + | requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire)) | |
| 191 | + | requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release)) | |
| 192 | + | } | |
| 193 | + | ||
| 188 | 194 | func (h *syncHarness) commit(buf uint32, acquire, release uint64) { | |
| 189 | 195 | t, c := h.t, h.c | |
| 190 | 196 | requestProtocol(t, c, h.surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 191 | - | requestProtocol(t, c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire)) | |
| 192 | - | requestProtocol(t, c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release)) | |
| 197 | + | h.setPoints(acquire, release) | |
| 193 | 198 | requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit) | |
| 194 | 199 | if err := c.Roundtrip(); err != nil { | |
| 195 | 200 | t.Fatal(err) | |
| @@ -315,6 +320,34 @@ func TestSyncobjProtocolErrors(t *testing.T) { | |||
| 315 | 320 | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |
| 316 | 321 | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |
| 317 | 322 | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)}, | |
| 323 | + | {"no acquire point", func(h *syncHarness) { | |
| 324 | + | b := h.dmabuf() | |
| 325 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |
| 326 | + | requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5)) | |
| 327 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |
| 328 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)}, | |
| 329 | + | {"points without attach", func(h *syncHarness) { | |
| 330 | + | b := h.dmabuf() | |
| 331 | + | h.commit(b, 1, 2) | |
| 332 | + | h.fire(1) | |
| 333 | + | h.setPoints(3, 4) | |
| 334 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |
| 335 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)}, | |
| 336 | + | {"points with null attach", func(h *syncHarness) { | |
| 337 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0)) | |
| 338 | + | h.setPoints(1, 2) | |
| 339 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |
| 340 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)}, | |
| 341 | + | {"wl_shm buffer", func(h *syncHarness) { | |
| 342 | + | b := shmBuffer(t, h.c) | |
| 343 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |
| 344 | + | h.setPoints(1, 2) | |
| 345 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |
| 346 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer)}, | |
| 347 | + | {"surface destroyed", func(h *syncHarness) { | |
| 348 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy) | |
| 349 | + | h.setPoints(1, 2) | |
| 350 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface)}, | |
| 318 | 351 | {"surface exists", func(h *syncHarness) { | |
| 319 | 352 | mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1") | |
| 320 | 353 | h.mgr = mgr | |
| @@ -353,3 +386,76 @@ func TestSyncobjInvalidTimeline(t *testing.T) { | |||
| 353 | 386 | } | |
| 354 | 387 | expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline)) | |
| 355 | 388 | } | |
| 389 | + | ||
| 390 | + | // A commit dropped before its acquire point has a fence signals nothing | |
| 391 | + | // (the client has not submitted that point) and sends no wl_buffer.release; | |
| 392 | + | // the waiter closes its eventfd and no acquire file is left open. | |
| 393 | + | func TestSyncobjDroppedCommit(t *testing.T) { | |
| 394 | + | h := newSyncHarness(t) | |
| 395 | + | h.releases = map[uint32]*syncReleaseProxy{} | |
| 396 | + | a := h.dmabuf() | |
| 397 | + | before := openFDs(t) | |
| 398 | + | h.commit(a, 1, 2) | |
| 399 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy) | |
| 400 | + | if err := h.c.Roundtrip(); err != nil { | |
| 401 | + | t.Fatal(err) | |
| 402 | + | } | |
| 403 | + | // The point never gets a fence: the waiter must still stop polling it. | |
| 404 | + | time.Sleep(250 * time.Millisecond) | |
| 405 | + | if err := h.c.Roundtrip(); err != nil { | |
| 406 | + | t.Fatal(err) | |
| 407 | + | } | |
| 408 | + | if got := h.signalled(); len(got) != 0 { | |
| 409 | + | t.Fatalf("signalled %v for a commit whose acquire point never arrived", got) | |
| 410 | + | } | |
| 411 | + | select { | |
| 412 | + | case <-h.releases[a].released: | |
| 413 | + | t.Fatal("wl_buffer.release sent under explicit sync") | |
| 414 | + | default: | |
| 415 | + | } | |
| 416 | + | // The harness keeps its own duplicate of the eventfd. | |
| 417 | + | if after := openFDs(t); after > before+1 { | |
| 418 | + | t.Fatalf("eventfds and sync files %d -> %d", before, after) | |
| 419 | + | } | |
| 420 | + | } | |
| 421 | + | ||
| 422 | + | // A commit dropped after its acquire point fired signals its release point. | |
| 423 | + | func TestSyncobjDroppedReadyCommitReleases(t *testing.T) { | |
| 424 | + | h := newSyncHarness(t) | |
| 425 | + | h.releases = map[uint32]*syncReleaseProxy{} | |
| 426 | + | a := h.dmabuf() | |
| 427 | + | h.commit(a, 1, 2) | |
| 428 | + | h.fire(1) | |
| 429 | + | if _, ok := h.content(2 * time.Second); !ok { | |
| 430 | + | t.Fatal("content") | |
| 431 | + | } | |
| 432 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy) | |
| 433 | + | deadline := time.Now().Add(2 * time.Second) | |
| 434 | + | for len(h.signalled()) == 0 && time.Now().Before(deadline) { | |
| 435 | + | if err := h.c.Roundtrip(); err != nil { | |
| 436 | + | t.Fatal(err) | |
| 437 | + | } | |
| 438 | + | time.Sleep(2 * time.Millisecond) | |
| 439 | + | } | |
| 440 | + | if got := h.signalled(); len(got) != 1 || got[0] != 2 { | |
| 441 | + | t.Fatalf("signalled %v", got) | |
| 442 | + | } | |
| 443 | + | } | |
| 444 | + | ||
| 445 | + | // openFDs counts the eventfds and sync files (pipes stand in for them) | |
| 446 | + | // the process holds. | |
| 447 | + | func openFDs(t *testing.T) int { | |
| 448 | + | t.Helper() | |
| 449 | + | ents, err := os.ReadDir("/proc/self/fd") | |
| 450 | + | if err != nil { | |
| 451 | + | t.Fatal(err) | |
| 452 | + | } | |
| 453 | + | n := 0 | |
| 454 | + | for _, e := range ents { | |
| 455 | + | l, _ := os.Readlink("/proc/self/fd/" + e.Name()) | |
| 456 | + | if strings.Contains(l, "eventfd") || strings.HasPrefix(l, "pipe:") { | |
| 457 | + | n++ | |
| 458 | + | } | |
| 459 | + | } | |
| 460 | + | return n | |
| 461 | + | } | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "os" | 5 | "os" | ||
| 6 | "sync" | 6 | "sync" | ||
| 7 | + | "strings" | |||
| 7 | "testing" | 8 | "testing" | ||
| 8 | "time" | 9 | "time" | ||
| 9 | "unsafe" | 10 | "unsafe" | ||
| @@ -185,11 +186,15 @@ func (p *syncReleaseProxy) Dispatch(e *wlturbo.Event) { | |||||
| 185 | } | 186 | } | ||
| 186 | } | 187 | } | ||
| 187 | 188 | ||||
| 189 | + | func (h *syncHarness) setPoints(acquire, release uint64) { | |||
| 190 | + | requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire)) | |||
| 191 | + | requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release)) | |||
| 192 | + | } | |||
| 193 | + | ||||
| 188 | func (h *syncHarness) commit(buf uint32, acquire, release uint64) { | 194 | func (h *syncHarness) commit(buf uint32, acquire, release uint64) { | ||
| 189 | t, c := h.t, h.c | 195 | t, c := h.t, h.c | ||
| 190 | requestProtocol(t, c, h.surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | 196 | requestProtocol(t, c, h.surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | ||
| 191 | - | requestProtocol(t, c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire)) | 197 | + | h.setPoints(acquire, release) |
| 192 | - | requestProtocol(t, c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release)) | |||
| 193 | requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit) | 198 | requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit) | ||
| 194 | if err := c.Roundtrip(); err != nil { | 199 | if err := c.Roundtrip(); err != nil { | ||
| 195 | t.Fatal(err) | 200 | t.Fatal(err) | ||
| @@ -315,6 +320,34 @@ func TestSyncobjProtocolErrors(t *testing.T) { | |||||
| 315 | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | 320 | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | ||
| 316 | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | 321 | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | ||
| 317 | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)}, | 322 | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)}, | ||
| 323 | + | {"no acquire point", func(h *syncHarness) { | |||
| 324 | + | b := h.dmabuf() | |||
| 325 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |||
| 326 | + | requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5)) | |||
| 327 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 328 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)}, | |||
| 329 | + | {"points without attach", func(h *syncHarness) { | |||
| 330 | + | b := h.dmabuf() | |||
| 331 | + | h.commit(b, 1, 2) | |||
| 332 | + | h.fire(1) | |||
| 333 | + | h.setPoints(3, 4) | |||
| 334 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 335 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)}, | |||
| 336 | + | {"points with null attach", func(h *syncHarness) { | |||
| 337 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0)) | |||
| 338 | + | h.setPoints(1, 2) | |||
| 339 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 340 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)}, | |||
| 341 | + | {"wl_shm buffer", func(h *syncHarness) { | |||
| 342 | + | b := shmBuffer(t, h.c) | |||
| 343 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0)) | |||
| 344 | + | h.setPoints(1, 2) | |||
| 345 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit) | |||
| 346 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer)}, | |||
| 347 | + | {"surface destroyed", func(h *syncHarness) { | |||
| 348 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy) | |||
| 349 | + | h.setPoints(1, 2) | |||
| 350 | + | }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface)}, | |||
| 318 | {"surface exists", func(h *syncHarness) { | 351 | {"surface exists", func(h *syncHarness) { | ||
| 319 | mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1") | 352 | mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1") | ||
| 320 | h.mgr = mgr | 353 | h.mgr = mgr | ||
| @@ -353,3 +386,76 @@ func TestSyncobjInvalidTimeline(t *testing.T) { | |||||
| 353 | } | 386 | } | ||
| 354 | expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline)) | 387 | expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline)) | ||
| 355 | } | 388 | } | ||
| 389 | + | ||||
| 390 | + | // A commit dropped before its acquire point has a fence signals nothing | |||
| 391 | + | // (the client has not submitted that point) and sends no wl_buffer.release; | |||
| 392 | + | // the waiter closes its eventfd and no acquire file is left open. | |||
| 393 | + | func TestSyncobjDroppedCommit(t *testing.T) { | |||
| 394 | + | h := newSyncHarness(t) | |||
| 395 | + | h.releases = map[uint32]*syncReleaseProxy{} | |||
| 396 | + | a := h.dmabuf() | |||
| 397 | + | before := openFDs(t) | |||
| 398 | + | h.commit(a, 1, 2) | |||
| 399 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy) | |||
| 400 | + | if err := h.c.Roundtrip(); err != nil { | |||
| 401 | + | t.Fatal(err) | |||
| 402 | + | } | |||
| 403 | + | // The point never gets a fence: the waiter must still stop polling it. | |||
| 404 | + | time.Sleep(250 * time.Millisecond) | |||
| 405 | + | if err := h.c.Roundtrip(); err != nil { | |||
| 406 | + | t.Fatal(err) | |||
| 407 | + | } | |||
| 408 | + | if got := h.signalled(); len(got) != 0 { | |||
| 409 | + | t.Fatalf("signalled %v for a commit whose acquire point never arrived", got) | |||
| 410 | + | } | |||
| 411 | + | select { | |||
| 412 | + | case <-h.releases[a].released: | |||
| 413 | + | t.Fatal("wl_buffer.release sent under explicit sync") | |||
| 414 | + | default: | |||
| 415 | + | } | |||
| 416 | + | // The harness keeps its own duplicate of the eventfd. | |||
| 417 | + | if after := openFDs(t); after > before+1 { | |||
| 418 | + | t.Fatalf("eventfds and sync files %d -> %d", before, after) | |||
| 419 | + | } | |||
| 420 | + | } | |||
| 421 | + | ||||
| 422 | + | // A commit dropped after its acquire point fired signals its release point. | |||
| 423 | + | func TestSyncobjDroppedReadyCommitReleases(t *testing.T) { | |||
| 424 | + | h := newSyncHarness(t) | |||
| 425 | + | h.releases = map[uint32]*syncReleaseProxy{} | |||
| 426 | + | a := h.dmabuf() | |||
| 427 | + | h.commit(a, 1, 2) | |||
| 428 | + | h.fire(1) | |||
| 429 | + | if _, ok := h.content(2 * time.Second); !ok { | |||
| 430 | + | t.Fatal("content") | |||
| 431 | + | } | |||
| 432 | + | requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy) | |||
| 433 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 434 | + | for len(h.signalled()) == 0 && time.Now().Before(deadline) { | |||
| 435 | + | if err := h.c.Roundtrip(); err != nil { | |||
| 436 | + | t.Fatal(err) | |||
| 437 | + | } | |||
| 438 | + | time.Sleep(2 * time.Millisecond) | |||
| 439 | + | } | |||
| 440 | + | if got := h.signalled(); len(got) != 1 || got[0] != 2 { | |||
| 441 | + | t.Fatalf("signalled %v", got) | |||
| 442 | + | } | |||
| 443 | + | } | |||
| 444 | + | ||||
| 445 | + | // openFDs counts the eventfds and sync files (pipes stand in for them) | |||
| 446 | + | // the process holds. | |||
| 447 | + | func openFDs(t *testing.T) int { | |||
| 448 | + | t.Helper() | |||
| 449 | + | ents, err := os.ReadDir("/proc/self/fd") | |||
| 450 | + | if err != nil { | |||
| 451 | + | t.Fatal(err) | |||
| 452 | + | } | |||
| 453 | + | n := 0 | |||
| 454 | + | for _, e := range ents { | |||
| 455 | + | l, _ := os.Readlink("/proc/self/fd/" + e.Name()) | |||
| 456 | + | if strings.Contains(l, "eventfd") || strings.HasPrefix(l, "pipe:") { | |||
| 457 | + | n++ | |||
| 458 | + | } | |||
| 459 | + | } | |||
| 460 | + | return n | |||
| 461 | + | } | |||