detached e904307

Merge branch 'nc/surfaces': layer, toplevel and lock surfaces with explicit-sync presentation

commit
e904307a8278b31a212c44e0e9e762084afef66d
author
brice · · 2026-10-01 17:51 UTC
merge of
9ff7f73 + 7e0d12d · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
18 files · +3527 -6
M .mockery.yml +12 -0
@@ -13,3 +13,15 @@ packages:
1313 filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go'
1414 interfaces:
1515 Handler:
16+ configs:
17+ - dir: mocks
18+ pkgname: neferclientmocks
19+ filename: handler_mock.go
20+ - dir: .
21+ pkgname: neferclient
22+ filename: handler_mock_test.go
23+ surfaceRequests:
24+ config:
25+ dir: .
26+ pkgname: neferclient
27+ filename: surface_mock_test.go
@@ -13,3 +13,15 @@ packages:
13 filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go'13 filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go'
14 interfaces:14 interfaces:
15 Handler:15 Handler:
16+ configs:
17+ - dir: mocks
18+ pkgname: neferclientmocks
19+ filename: handler_mock.go
20+ - dir: .
21+ pkgname: neferclient
22+ filename: handler_mock_test.go
23+ surfaceRequests:
24+ config:
25+ dir: .
26+ pkgname: neferclient
27+ filename: surface_mock_test.go
M alloc_test.go +88 -3
@@ -1,9 +1,11 @@
11 package neferclient_test
22
33 import (
4+ "encoding/binary"
45 "runtime"
56 "testing"
67
8+ "github.com/bnema/go-wayland-bindings/client/wayland"
79 "github.com/stretchr/testify/require"
810 "golang.org/x/sys/unix"
911
@@ -14,11 +16,12 @@ import (
1416 // only counts, so the test sees the cost of the library alone.
1517 type countingHandler struct {
1618 neferclient.NopHandler
17- fdReady, errs int
19+ fdReady, errs, frames int
1820 }
1921
20-func (h *countingHandler) FDReady(uint64) { h.fdReady++ }
21-func (h *countingHandler) Error(error) { h.errs++ }
22+func (h *countingHandler) FDReady(uint64) { h.fdReady++ }
23+func (h *countingHandler) Error(error) { h.errs++ }
24+func (h *countingHandler) Frame(neferclient.SurfaceID) { h.frames++ }
2225
2326 // TestAllocDispatch measures the whole steady-state pipeline over a real
2427 // socketpair: wire frames in, reader decode, queue, Dispatch (output state
@@ -65,3 +68,85 @@ func TestAllocDispatch(t *testing.T) {
6568 require.Positive(t, h.fdReady)
6669 require.Zero(t, h.errs)
6770 }
71+
72+// TestAllocPresent measures the steady-state present loop over a real
73+// socketpair: Present (all requests, including the frame callback), the peer
74+// answering the callback, reader decode, queue and Dispatch applying Frame.
75+// The peer parses and discards everything else.
76+func TestAllocPresent(t *testing.T) {
77+ skipUnderRace(t)
78+ globals := []wireGlobal{{1, wayland.CompositorInterface, 6}, {2, "zwp_linux_dmabuf_v1", 4},
79+ {3, "wp_linux_drm_syncobj_manager_v1", 1}, {4, "wp_viewporter", 1}, {5, "xdg_wm_base", 6}}
80+ c, srv := connectWire(t, globals, nil)
81+ s, err := c.NewToplevel("alloc", 64, 48)
82+ require.NoError(t, err)
83+ s.MarkPresentable(64, 48)
84+
85+ surfaceID := s.SurfaceObjectID()
86+ srv.mu.Lock()
87+ srv.hook = func(obj uint32, op uint16, body []byte) {
88+ if obj == surfaceID && op == 3 && len(body) == 4 { // wl_surface.frame(new_id)
89+ srv.write(frame(binary.LittleEndian.Uint32(body), 0, appendU32(nil, 1)))
90+ srv.write(frame(1, 1, body)) // wl_display.delete_id: the id is recycled
91+ }
92+ }
93+ srv.mu.Unlock()
94+
95+ memfd, err := unix.MemfdCreate("alloc", unix.MFD_CLOEXEC)
96+ require.NoError(t, err)
97+ defer unix.Close(memfd)
98+ require.NoError(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 0x34325258,
99+ Planes: [4]neferclient.Plane{{FD: memfd, Stride: 256}}, PlaneCount: 1}))
100+ require.NoError(t, s.ImportTimeline(1, memfd))
101+ require.NoError(t, s.ImportTimeline(2, memfd))
102+
103+ var point uint64
104+ damage := []neferclient.Rect{{X: 0, Y: 0, Width: 16, Height: 16}}
105+ present := func() {
106+ point++
107+ if err := s.Present(&neferclient.Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2,
108+ AcquirePoint: point, ReleasePoint: point, Damage: damage, Opaque: true}); err != nil {
109+ t.Fatal(err)
110+ }
111+ }
112+
113+ // The request path: every request Present sends, over the real socket,
114+ // without the frame callback.
115+ s.SkipFrameRequests()
116+ requests := func() {
117+ s.ResetFrame()
118+ present()
119+ }
120+ for range 4 { // warm-up beyond AllocsPerRun's own
121+ requests()
122+ }
123+ allocs := testing.AllocsPerRun(100, requests)
124+ t.Logf("allocs per Present (requests only): %v", allocs)
125+ require.Zero(t, allocs)
126+
127+ // The complete cycle adds the frame callback. wlturbo registers each
128+ // server-created wl_callback with two sync.Map stores (Context.proxies and
129+ // Display.objects) and a sync.Map store always allocates a node, so two
130+ // allocations per frame are inherent to the transport (wlturbo is out of
131+ // scope for this change). They are pinned so a regression above that shows.
132+ s.UseWireRequests()
133+ h := &countingHandler{}
134+ cycle := func() {
135+ present()
136+ for c.QueueLen() < 1 {
137+ runtime.Gosched()
138+ }
139+ if err := c.Dispatch(h); err != nil {
140+ t.Fatal(err)
141+ }
142+ }
143+ s.ResetFrame()
144+ for range 4 {
145+ cycle()
146+ }
147+ full := testing.AllocsPerRun(100, cycle)
148+ t.Logf("allocs per Present+frame round trip: %v (wlturbo callback registration)", full)
149+ require.LessOrEqual(t, full, 2.0)
150+ require.Positive(t, h.frames)
151+ require.Zero(t, h.errs)
152+}
@@ -1,9 +1,11 @@
1 package neferclient_test1 package neferclient_test
2 2
3 import (3 import (
4+ "encoding/binary"
4 "runtime"5 "runtime"
5 "testing"6 "testing"
6 7
8+ "github.com/bnema/go-wayland-bindings/client/wayland"
7 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
8 "golang.org/x/sys/unix"10 "golang.org/x/sys/unix"
9 11
@@ -14,11 +16,12 @@ import (
14 // only counts, so the test sees the cost of the library alone.16 // only counts, so the test sees the cost of the library alone.
15 type countingHandler struct {17 type countingHandler struct {
16 neferclient.NopHandler18 neferclient.NopHandler
17- fdReady, errs int19+ fdReady, errs, frames int
18 }20 }
19 21
20-func (h *countingHandler) FDReady(uint64) { h.fdReady++ }22+func (h *countingHandler) FDReady(uint64) { h.fdReady++ }
21-func (h *countingHandler) Error(error) { h.errs++ }23+func (h *countingHandler) Error(error) { h.errs++ }
24+func (h *countingHandler) Frame(neferclient.SurfaceID) { h.frames++ }
22 25
23 // TestAllocDispatch measures the whole steady-state pipeline over a real26 // TestAllocDispatch measures the whole steady-state pipeline over a real
24 // socketpair: wire frames in, reader decode, queue, Dispatch (output state27 // socketpair: wire frames in, reader decode, queue, Dispatch (output state
@@ -65,3 +68,85 @@ func TestAllocDispatch(t *testing.T) {
65 require.Positive(t, h.fdReady)68 require.Positive(t, h.fdReady)
66 require.Zero(t, h.errs)69 require.Zero(t, h.errs)
67 }70 }
71+
72+// TestAllocPresent measures the steady-state present loop over a real
73+// socketpair: Present (all requests, including the frame callback), the peer
74+// answering the callback, reader decode, queue and Dispatch applying Frame.
75+// The peer parses and discards everything else.
76+func TestAllocPresent(t *testing.T) {
77+ skipUnderRace(t)
78+ globals := []wireGlobal{{1, wayland.CompositorInterface, 6}, {2, "zwp_linux_dmabuf_v1", 4},
79+ {3, "wp_linux_drm_syncobj_manager_v1", 1}, {4, "wp_viewporter", 1}, {5, "xdg_wm_base", 6}}
80+ c, srv := connectWire(t, globals, nil)
81+ s, err := c.NewToplevel("alloc", 64, 48)
82+ require.NoError(t, err)
83+ s.MarkPresentable(64, 48)
84+
85+ surfaceID := s.SurfaceObjectID()
86+ srv.mu.Lock()
87+ srv.hook = func(obj uint32, op uint16, body []byte) {
88+ if obj == surfaceID && op == 3 && len(body) == 4 { // wl_surface.frame(new_id)
89+ srv.write(frame(binary.LittleEndian.Uint32(body), 0, appendU32(nil, 1)))
90+ srv.write(frame(1, 1, body)) // wl_display.delete_id: the id is recycled
91+ }
92+ }
93+ srv.mu.Unlock()
94+
95+ memfd, err := unix.MemfdCreate("alloc", unix.MFD_CLOEXEC)
96+ require.NoError(t, err)
97+ defer unix.Close(memfd)
98+ require.NoError(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 0x34325258,
99+ Planes: [4]neferclient.Plane{{FD: memfd, Stride: 256}}, PlaneCount: 1}))
100+ require.NoError(t, s.ImportTimeline(1, memfd))
101+ require.NoError(t, s.ImportTimeline(2, memfd))
102+
103+ var point uint64
104+ damage := []neferclient.Rect{{X: 0, Y: 0, Width: 16, Height: 16}}
105+ present := func() {
106+ point++
107+ if err := s.Present(&neferclient.Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2,
108+ AcquirePoint: point, ReleasePoint: point, Damage: damage, Opaque: true}); err != nil {
109+ t.Fatal(err)
110+ }
111+ }
112+
113+ // The request path: every request Present sends, over the real socket,
114+ // without the frame callback.
115+ s.SkipFrameRequests()
116+ requests := func() {
117+ s.ResetFrame()
118+ present()
119+ }
120+ for range 4 { // warm-up beyond AllocsPerRun's own
121+ requests()
122+ }
123+ allocs := testing.AllocsPerRun(100, requests)
124+ t.Logf("allocs per Present (requests only): %v", allocs)
125+ require.Zero(t, allocs)
126+
127+ // The complete cycle adds the frame callback. wlturbo registers each
128+ // server-created wl_callback with two sync.Map stores (Context.proxies and
129+ // Display.objects) and a sync.Map store always allocates a node, so two
130+ // allocations per frame are inherent to the transport (wlturbo is out of
131+ // scope for this change). They are pinned so a regression above that shows.
132+ s.UseWireRequests()
133+ h := &countingHandler{}
134+ cycle := func() {
135+ present()
136+ for c.QueueLen() < 1 {
137+ runtime.Gosched()
138+ }
139+ if err := c.Dispatch(h); err != nil {
140+ t.Fatal(err)
141+ }
142+ }
143+ s.ResetFrame()
144+ for range 4 {
145+ cycle()
146+ }
147+ full := testing.AllocsPerRun(100, cycle)
148+ t.Logf("allocs per Present+frame round trip: %v (wlturbo callback registration)", full)
149+ require.LessOrEqual(t, full, 2.0)
150+ require.Positive(t, h.frames)
151+ require.Zero(t, h.errs)
152+}
M conn.go +12 -0
@@ -76,6 +76,13 @@ type Conn struct {
7676 outputs []*outputEntry // sorted by Global
7777 view []Output // reused by Outputs
7878
79+ g globals // lazily bound protocol globals, owner only
80+ surfaces map[SurfaceID]*Surface
81+ nextSurf SurfaceID
82+ nextLock uint32
83+ curLock *Lock // the live session lock, if any
84+ tableBuf []byte
85+
7986 ready bool // Connect finished; announcements go to the Handler
8087 dispatching bool
8188 closed bool
@@ -137,6 +144,8 @@ func connectConn(ctx context.Context, sock net.Conn) (res *Conn, err error) {
137144 efd: -1,
138145 epfd: -1,
139146 watched: make(map[int32]uint64),
147+
148+ surfaces: make(map[SurfaceID]*Surface),
140149 }
141150 watch := context.AfterFunc(ctx, func() { _ = display.Close() })
142151 defer func() {
@@ -385,6 +394,8 @@ func (c *Conn) apply(ev *event, h Handler) {
385394 h.Error(err)
386395 }
387396 }
397+ default:
398+ c.applySurface(ev, h)
388399 }
389400 }
390401
@@ -579,5 +590,6 @@ func (c *Conn) Close() error {
579590 if c.efd >= 0 {
580591 _ = unix.Close(c.efd)
581592 }
593+ c.q.closeFDs() // both producers are gone: close what nobody will drain
582594 return err
583595 }
@@ -76,6 +76,13 @@ type Conn struct {
76 outputs []*outputEntry // sorted by Global76 outputs []*outputEntry // sorted by Global
77 view []Output // reused by Outputs77 view []Output // reused by Outputs
78 78
79+ g globals // lazily bound protocol globals, owner only
80+ surfaces map[SurfaceID]*Surface
81+ nextSurf SurfaceID
82+ nextLock uint32
83+ curLock *Lock // the live session lock, if any
84+ tableBuf []byte
85+
79 ready bool // Connect finished; announcements go to the Handler86 ready bool // Connect finished; announcements go to the Handler
80 dispatching bool87 dispatching bool
81 closed bool88 closed bool
@@ -137,6 +144,8 @@ func connectConn(ctx context.Context, sock net.Conn) (res *Conn, err error) {
137 efd: -1,144 efd: -1,
138 epfd: -1,145 epfd: -1,
139 watched: make(map[int32]uint64),146 watched: make(map[int32]uint64),
147+
148+ surfaces: make(map[SurfaceID]*Surface),
140 }149 }
141 watch := context.AfterFunc(ctx, func() { _ = display.Close() })150 watch := context.AfterFunc(ctx, func() { _ = display.Close() })
142 defer func() {151 defer func() {
@@ -385,6 +394,8 @@ func (c *Conn) apply(ev *event, h Handler) {
385 h.Error(err)394 h.Error(err)
386 }395 }
387 }396 }
397+ default:
398+ c.applySurface(ev, h)
388 }399 }
389 }400 }
390 401
@@ -579,5 +590,6 @@ func (c *Conn) Close() error {
579 if c.efd >= 0 {590 if c.efd >= 0 {
580 _ = unix.Close(c.efd)591 _ = unix.Close(c.efd)
581 }592 }
593+ c.q.closeFDs() // both producers are gone: close what nobody will drain
582 return err594 return err
583 }595 }
M export_test.go +24 -0
@@ -15,3 +15,27 @@ func ConnectConn(ctx context.Context, sock net.Conn) (*Conn, error) { return con
1515 func SocketPath(name string) (string, error) { return socketPath(name) }
1616
1717 func (c *Conn) QueueLen() int { return c.q.length() }
18+
19+// MarkPresentable stands in for the configure and feedback rounds, which the
20+// allocation test does not drive over the wire.
21+func (s *Surface) MarkPresentable(w, h int32) {
22+ s.width, s.height = w, h
23+ s.configured, s.frameReady, s.fbDone = true, true, true
24+}
25+
26+func (s *Surface) SurfaceObjectID() uint32 { return s.surf.ID() }
27+
28+// SkipFrameRequests makes Present omit wl_surface.frame.
29+func (s *Surface) SkipFrameRequests() { s.req = wireRequests{s: s, skipFrame: true} }
30+
31+// ResetFrame re-arms Present when frame requests are skipped.
32+func (s *Surface) ResetFrame() { s.frameReady = true }
33+
34+// UseWireRequests restores the full request path.
35+func (s *Surface) UseWireRequests() { s.req = wireRequests{s: s} }
36+
37+// MarkPresented stands in for a committed Present.
38+func (s *Surface) MarkPresented() { s.presented = true }
39+
40+// DropFrameCallback marks the pending frame callback as answered.
41+func (s *Surface) PendingFrame() bool { return !s.frameReady }
@@ -15,3 +15,27 @@ func ConnectConn(ctx context.Context, sock net.Conn) (*Conn, error) { return con
15 func SocketPath(name string) (string, error) { return socketPath(name) }15 func SocketPath(name string) (string, error) { return socketPath(name) }
16 16
17 func (c *Conn) QueueLen() int { return c.q.length() }17 func (c *Conn) QueueLen() int { return c.q.length() }
18+
19+// MarkPresentable stands in for the configure and feedback rounds, which the
20+// allocation test does not drive over the wire.
21+func (s *Surface) MarkPresentable(w, h int32) {
22+ s.width, s.height = w, h
23+ s.configured, s.frameReady, s.fbDone = true, true, true
24+}
25+
26+func (s *Surface) SurfaceObjectID() uint32 { return s.surf.ID() }
27+
28+// SkipFrameRequests makes Present omit wl_surface.frame.
29+func (s *Surface) SkipFrameRequests() { s.req = wireRequests{s: s, skipFrame: true} }
30+
31+// ResetFrame re-arms Present when frame requests are skipped.
32+func (s *Surface) ResetFrame() { s.frameReady = true }
33+
34+// UseWireRequests restores the full request path.
35+func (s *Surface) UseWireRequests() { s.req = wireRequests{s: s} }
36+
37+// MarkPresented stands in for a committed Present.
38+func (s *Surface) MarkPresented() { s.presented = true }
39+
40+// DropFrameCallback marks the pending frame callback as answered.
41+func (s *Surface) PendingFrame() bool { return !s.frameReady }
A globals.go +132 -0
@@ -0,0 +1,132 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/go-wayland-bindings/client/extsessionlock"
8+ "github.com/bnema/go-wayland-bindings/client/fractionalscale"
9+ "github.com/bnema/go-wayland-bindings/client/linuxdmabuf"
10+ "github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj"
11+ "github.com/bnema/go-wayland-bindings/client/viewporter"
12+ "github.com/bnema/go-wayland-bindings/client/wayland"
13+ "github.com/bnema/go-wayland-bindings/client/wlrlayershell"
14+ "github.com/bnema/go-wayland-bindings/client/xdgshell"
15+ "github.com/bnema/wlturbo/wl"
16+)
17+
18+// CapabilityError reports a missing or too old required protocol global.
19+type CapabilityError struct {
20+ Name string
21+ Cause error
22+}
23+
24+func (e *CapabilityError) Error() string {
25+ return fmt.Sprintf("neferclient: capability %s: %v", e.Name, e.Cause)
26+}
27+func (e *CapabilityError) Unwrap() error { return e.Cause }
28+
29+// globals holds the protocol globals bound on first use. Owner goroutine only.
30+type globals struct {
31+ compositor *wayland.Compositor
32+ dmabuf *linuxdmabuf.LinuxDmabuf
33+ syncobj *linuxdrmsyncobj.WpLinuxDrmSyncobjManager
34+ xdg *xdgshell.XdgWmBase
35+ layer *wlrlayershell.LayerShell
36+ lockMgr *extsessionlock.ExtSessionLockManager
37+ viewporter *viewporter.WpViewporter
38+ fscale *fractionalscale.WpFractionalScaleManager
39+
40+ layerVersion uint32
41+ triedViewport bool
42+ triedFScale bool
43+}
44+
45+// require binds iface at min(announced, supported) and fails with a
46+// *CapabilityError when it is absent or older than minimum.
47+func (c *Conn) require[P wl.Proxy](iface string, supported, minimum uint32, newProxy func(*wl.Context) P) (P, uint32, error) {
48+ p, v, err := c.Bind(iface, supported, newProxy)
49+ if err != nil {
50+ var zero P
51+ return zero, 0, &CapabilityError{Name: iface, Cause: err}
52+ }
53+ if v < minimum {
54+ var zero P
55+ return zero, 0, &CapabilityError{Name: iface, Cause: fmt.Errorf("requires version %d, negotiated %d", minimum, v)}
56+ }
57+ return p, v, nil
58+}
59+
60+// ensureCore binds wl_compositor, linux-dmabuf (v4) and drm-syncobj, and the
61+// optional viewporter and fractional-scale managers.
62+func (c *Conn) ensureCore() error {
63+ g := &c.g
64+ var err error
65+ if g.compositor == nil {
66+ if g.compositor, _, err = c.require(wayland.CompositorInterface, 6, 4, wayland.NewCompositor); err != nil {
67+ return err
68+ }
69+ }
70+ if g.dmabuf == nil {
71+ if g.dmabuf, _, err = c.require(linuxdmabuf.LinuxDmabufInterface, 4, 4, linuxdmabuf.NewLinuxDmabuf); err != nil {
72+ return err
73+ }
74+ }
75+ if g.syncobj == nil {
76+ if g.syncobj, _, err = c.require(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, 1, linuxdrmsyncobj.NewWpLinuxDrmSyncobjManager); err != nil {
77+ return err
78+ }
79+ }
80+ if !g.triedViewport {
81+ g.triedViewport = true
82+ v, _, e := c.Bind(viewporter.WpViewporterInterface, 1, viewporter.NewWpViewporter)
83+ if e != nil && !errors.Is(e, ErrGlobalNotFound) {
84+ return e
85+ }
86+ g.viewporter = v
87+ }
88+ if !g.triedFScale {
89+ g.triedFScale = true
90+ v, _, e := c.Bind(fractionalscale.WpFractionalScaleManagerInterface, 1, fractionalscale.NewWpFractionalScaleManager)
91+ if e != nil && !errors.Is(e, ErrGlobalNotFound) {
92+ return e
93+ }
94+ g.fscale = v
95+ }
96+ return nil
97+}
98+
99+func (c *Conn) ensureXdg() error {
100+ if c.g.xdg != nil {
101+ return nil
102+ }
103+ q := c.q
104+ x, _, err := c.require(xdgshell.XdgWmBaseInterface, 6, 1, func(ctx *wl.Context) *xdgshell.XdgWmBase {
105+ x := xdgshell.NewXdgWmBase(ctx)
106+ x.OnPing(func(serial uint32) {
107+ e := event{kind: evPing, serial: serial}
108+ q.post(&e)
109+ })
110+ return x
111+ })
112+ c.g.xdg = x
113+ return err
114+}
115+
116+func (c *Conn) ensureLayerShell() error {
117+ if c.g.layer != nil {
118+ return nil
119+ }
120+ l, v, err := c.require(wlrlayershell.LayerShellInterface, 4, 1, wlrlayershell.NewLayerShell)
121+ c.g.layer, c.g.layerVersion = l, v
122+ return err
123+}
124+
125+func (c *Conn) ensureLockManager() error {
126+ if c.g.lockMgr != nil {
127+ return nil
128+ }
129+ m, _, err := c.require(extsessionlock.ExtSessionLockManagerInterface, 1, 1, extsessionlock.NewExtSessionLockManager)
130+ c.g.lockMgr = m
131+ return err
132+}
@@ -0,0 +1,132 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/go-wayland-bindings/client/extsessionlock"
8+ "github.com/bnema/go-wayland-bindings/client/fractionalscale"
9+ "github.com/bnema/go-wayland-bindings/client/linuxdmabuf"
10+ "github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj"
11+ "github.com/bnema/go-wayland-bindings/client/viewporter"
12+ "github.com/bnema/go-wayland-bindings/client/wayland"
13+ "github.com/bnema/go-wayland-bindings/client/wlrlayershell"
14+ "github.com/bnema/go-wayland-bindings/client/xdgshell"
15+ "github.com/bnema/wlturbo/wl"
16+)
17+
18+// CapabilityError reports a missing or too old required protocol global.
19+type CapabilityError struct {
20+ Name string
21+ Cause error
22+}
23+
24+func (e *CapabilityError) Error() string {
25+ return fmt.Sprintf("neferclient: capability %s: %v", e.Name, e.Cause)
26+}
27+func (e *CapabilityError) Unwrap() error { return e.Cause }
28+
29+// globals holds the protocol globals bound on first use. Owner goroutine only.
30+type globals struct {
31+ compositor *wayland.Compositor
32+ dmabuf *linuxdmabuf.LinuxDmabuf
33+ syncobj *linuxdrmsyncobj.WpLinuxDrmSyncobjManager
34+ xdg *xdgshell.XdgWmBase
35+ layer *wlrlayershell.LayerShell
36+ lockMgr *extsessionlock.ExtSessionLockManager
37+ viewporter *viewporter.WpViewporter
38+ fscale *fractionalscale.WpFractionalScaleManager
39+
40+ layerVersion uint32
41+ triedViewport bool
42+ triedFScale bool
43+}
44+
45+// require binds iface at min(announced, supported) and fails with a
46+// *CapabilityError when it is absent or older than minimum.
47+func (c *Conn) require[P wl.Proxy](iface string, supported, minimum uint32, newProxy func(*wl.Context) P) (P, uint32, error) {
48+ p, v, err := c.Bind(iface, supported, newProxy)
49+ if err != nil {
50+ var zero P
51+ return zero, 0, &CapabilityError{Name: iface, Cause: err}
52+ }
53+ if v < minimum {
54+ var zero P
55+ return zero, 0, &CapabilityError{Name: iface, Cause: fmt.Errorf("requires version %d, negotiated %d", minimum, v)}
56+ }
57+ return p, v, nil
58+}
59+
60+// ensureCore binds wl_compositor, linux-dmabuf (v4) and drm-syncobj, and the
61+// optional viewporter and fractional-scale managers.
62+func (c *Conn) ensureCore() error {
63+ g := &c.g
64+ var err error
65+ if g.compositor == nil {
66+ if g.compositor, _, err = c.require(wayland.CompositorInterface, 6, 4, wayland.NewCompositor); err != nil {
67+ return err
68+ }
69+ }
70+ if g.dmabuf == nil {
71+ if g.dmabuf, _, err = c.require(linuxdmabuf.LinuxDmabufInterface, 4, 4, linuxdmabuf.NewLinuxDmabuf); err != nil {
72+ return err
73+ }
74+ }
75+ if g.syncobj == nil {
76+ if g.syncobj, _, err = c.require(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, 1, linuxdrmsyncobj.NewWpLinuxDrmSyncobjManager); err != nil {
77+ return err
78+ }
79+ }
80+ if !g.triedViewport {
81+ g.triedViewport = true
82+ v, _, e := c.Bind(viewporter.WpViewporterInterface, 1, viewporter.NewWpViewporter)
83+ if e != nil && !errors.Is(e, ErrGlobalNotFound) {
84+ return e
85+ }
86+ g.viewporter = v
87+ }
88+ if !g.triedFScale {
89+ g.triedFScale = true
90+ v, _, e := c.Bind(fractionalscale.WpFractionalScaleManagerInterface, 1, fractionalscale.NewWpFractionalScaleManager)
91+ if e != nil && !errors.Is(e, ErrGlobalNotFound) {
92+ return e
93+ }
94+ g.fscale = v
95+ }
96+ return nil
97+}
98+
99+func (c *Conn) ensureXdg() error {
100+ if c.g.xdg != nil {
101+ return nil
102+ }
103+ q := c.q
104+ x, _, err := c.require(xdgshell.XdgWmBaseInterface, 6, 1, func(ctx *wl.Context) *xdgshell.XdgWmBase {
105+ x := xdgshell.NewXdgWmBase(ctx)
106+ x.OnPing(func(serial uint32) {
107+ e := event{kind: evPing, serial: serial}
108+ q.post(&e)
109+ })
110+ return x
111+ })
112+ c.g.xdg = x
113+ return err
114+}
115+
116+func (c *Conn) ensureLayerShell() error {
117+ if c.g.layer != nil {
118+ return nil
119+ }
120+ l, v, err := c.require(wlrlayershell.LayerShellInterface, 4, 1, wlrlayershell.NewLayerShell)
121+ c.g.layer, c.g.layerVersion = l, v
122+ return err
123+}
124+
125+func (c *Conn) ensureLockManager() error {
126+ if c.g.lockMgr != nil {
127+ return nil
128+ }
129+ m, _, err := c.require(extsessionlock.ExtSessionLockManagerInterface, 1, 1, extsessionlock.NewExtSessionLockManager)
130+ c.g.lockMgr = m
131+ return err
132+}
M handler.go +35 -0
@@ -23,6 +23,33 @@ type Handler interface {
2323 // a wl_output that could not be bound. Fatal transport errors are returned
2424 // by Dispatch instead.
2525 Error(err error)
26+
27+ // Configure reports the logical size of a surface after the compositor's
28+ // configure was acknowledged (the library acks internally). The first
29+ // call makes the surface presentable. For layer surfaces an axis the
30+ // compositor leaves unchanged keeps its previous size.
31+ Configure(id SurfaceID, width, height int32)
32+ // Scale reports a changed preferred scale: fractional (n/120) when the
33+ // compositor supports wp_fractional_scale, else the integer buffer scale.
34+ // Re-read [Surface.PhysicalSize] and render again.
35+ Scale(id SurfaceID, scale float64)
36+ // Frame reports that the frame callback requested by the last
37+ // [Surface.Present] fired; the next Present is allowed.
38+ Frame(id SurfaceID)
39+ // Closed reports that the compositor closed the surface (toplevel close,
40+ // layer surface closed). Call [Surface.Close]; nothing else happens
41+ // automatically.
42+ Closed(id SurfaceID)
43+ // FeedbackDone reports that a complete linux-dmabuf feedback round was
44+ // applied; [Surface.Feedback] holds the new main device and formats. It
45+ // is sent again whenever the compositor changes its preference.
46+ FeedbackDone(id SurfaceID)
47+ // Locked reports ext_session_lock_v1.locked for the live [Lock]: every
48+ // output is protected. Only now may [Lock.Unlock] be called.
49+ Locked()
50+ // LockFinished reports ext_session_lock_v1.finished: the compositor
51+ // refused or ended the lock. Destroy it with [Lock.Close].
52+ LockFinished()
2653 }
2754
2855 // NopHandler implements every Handler method as a no-op. Embed it in a handler
@@ -34,3 +61,11 @@ func (NopHandler) OutputAdded(*Output) {}
3461 func (NopHandler) OutputRemoved(uint32) {}
3562 func (NopHandler) FDReady(uint64) {}
3663 func (NopHandler) Error(error) {}
64+
65+func (NopHandler) Configure(SurfaceID, int32, int32) {}
66+func (NopHandler) Scale(SurfaceID, float64) {}
67+func (NopHandler) Frame(SurfaceID) {}
68+func (NopHandler) Closed(SurfaceID) {}
69+func (NopHandler) FeedbackDone(SurfaceID) {}
70+func (NopHandler) Locked() {}
71+func (NopHandler) LockFinished() {}
@@ -23,6 +23,33 @@ type Handler interface {
23 // a wl_output that could not be bound. Fatal transport errors are returned23 // a wl_output that could not be bound. Fatal transport errors are returned
24 // by Dispatch instead.24 // by Dispatch instead.
25 Error(err error)25 Error(err error)
26+
27+ // Configure reports the logical size of a surface after the compositor's
28+ // configure was acknowledged (the library acks internally). The first
29+ // call makes the surface presentable. For layer surfaces an axis the
30+ // compositor leaves unchanged keeps its previous size.
31+ Configure(id SurfaceID, width, height int32)
32+ // Scale reports a changed preferred scale: fractional (n/120) when the
33+ // compositor supports wp_fractional_scale, else the integer buffer scale.
34+ // Re-read [Surface.PhysicalSize] and render again.
35+ Scale(id SurfaceID, scale float64)
36+ // Frame reports that the frame callback requested by the last
37+ // [Surface.Present] fired; the next Present is allowed.
38+ Frame(id SurfaceID)
39+ // Closed reports that the compositor closed the surface (toplevel close,
40+ // layer surface closed). Call [Surface.Close]; nothing else happens
41+ // automatically.
42+ Closed(id SurfaceID)
43+ // FeedbackDone reports that a complete linux-dmabuf feedback round was
44+ // applied; [Surface.Feedback] holds the new main device and formats. It
45+ // is sent again whenever the compositor changes its preference.
46+ FeedbackDone(id SurfaceID)
47+ // Locked reports ext_session_lock_v1.locked for the live [Lock]: every
48+ // output is protected. Only now may [Lock.Unlock] be called.
49+ Locked()
50+ // LockFinished reports ext_session_lock_v1.finished: the compositor
51+ // refused or ended the lock. Destroy it with [Lock.Close].
52+ LockFinished()
26 }53 }
27 54
28 // NopHandler implements every Handler method as a no-op. Embed it in a handler55 // NopHandler implements every Handler method as a no-op. Embed it in a handler
@@ -34,3 +61,11 @@ func (NopHandler) OutputAdded(*Output) {}
34 func (NopHandler) OutputRemoved(uint32) {}61 func (NopHandler) OutputRemoved(uint32) {}
35 func (NopHandler) FDReady(uint64) {}62 func (NopHandler) FDReady(uint64) {}
36 func (NopHandler) Error(error) {}63 func (NopHandler) Error(error) {}
64+
65+func (NopHandler) Configure(SurfaceID, int32, int32) {}
66+func (NopHandler) Scale(SurfaceID, float64) {}
67+func (NopHandler) Frame(SurfaceID) {}
68+func (NopHandler) Closed(SurfaceID) {}
69+func (NopHandler) FeedbackDone(SurfaceID) {}
70+func (NopHandler) Locked() {}
71+func (NopHandler) LockFinished() {}
A handler_mock_test.go +489 -0
@@ -0,0 +1,489 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package neferclient
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// NewMockHandler creates a new instance of MockHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func NewMockHandler(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *MockHandler {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &MockHandler{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// MockHandler is an autogenerated mock type for the Handler type
35+type MockHandler struct {
36+ mock.Mock
37+}
38+
39+type MockHandler_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *MockHandler) EXPECT() *MockHandler_Expecter {
44+ return &MockHandler_Expecter{mock: &_m.Mock}
45+}
46+
47+// Closed provides a mock function for the type MockHandler
48+func (_mock *MockHandler) Closed(id SurfaceID) {
49+ _mock.Called(id)
50+ return
51+}
52+
53+// MockHandler_Closed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Closed'
54+type MockHandler_Closed_Call struct {
55+ *mock.Call
56+}
57+
58+// Closed is a helper method to define mock.On call
59+// - id SurfaceID
60+func (_e *MockHandler_Expecter) Closed(id any) *MockHandler_Closed_Call {
61+ return &MockHandler_Closed_Call{Call: _e.mock.On("Closed", id)}
62+}
63+
64+func (_c *MockHandler_Closed_Call) Run(run func(id SurfaceID)) *MockHandler_Closed_Call {
65+ _c.Call.Run(func(args mock.Arguments) {
66+ var arg0 SurfaceID
67+ if args[0] != nil {
68+ arg0 = args[0].(SurfaceID)
69+ }
70+ run(
71+ arg0,
72+ )
73+ })
74+ return _c
75+}
76+
77+func (_c *MockHandler_Closed_Call) Return() *MockHandler_Closed_Call {
78+ _c.Call.Return()
79+ return _c
80+}
81+
82+func (_c *MockHandler_Closed_Call) RunAndReturn(run func(id SurfaceID)) *MockHandler_Closed_Call {
83+ _c.Run(run)
84+ return _c
85+}
86+
87+// Configure provides a mock function for the type MockHandler
88+func (_mock *MockHandler) Configure(id SurfaceID, width int32, height int32) {
89+ _mock.Called(id, width, height)
90+ return
91+}
92+
93+// MockHandler_Configure_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Configure'
94+type MockHandler_Configure_Call struct {
95+ *mock.Call
96+}
97+
98+// Configure is a helper method to define mock.On call
99+// - id SurfaceID
100+// - width int32
101+// - height int32
102+func (_e *MockHandler_Expecter) Configure(id any, width any, height any) *MockHandler_Configure_Call {
103+ return &MockHandler_Configure_Call{Call: _e.mock.On("Configure", id, width, height)}
104+}
105+
106+func (_c *MockHandler_Configure_Call) Run(run func(id SurfaceID, width int32, height int32)) *MockHandler_Configure_Call {
107+ _c.Call.Run(func(args mock.Arguments) {
108+ var arg0 SurfaceID
109+ if args[0] != nil {
110+ arg0 = args[0].(SurfaceID)
111+ }
112+ var arg1 int32
113+ if args[1] != nil {
114+ arg1 = args[1].(int32)
115+ }
116+ var arg2 int32
117+ if args[2] != nil {
118+ arg2 = args[2].(int32)
119+ }
120+ run(
121+ arg0,
122+ arg1,
123+ arg2,
124+ )
125+ })
126+ return _c
127+}
128+
129+func (_c *MockHandler_Configure_Call) Return() *MockHandler_Configure_Call {
130+ _c.Call.Return()
131+ return _c
132+}
133+
134+func (_c *MockHandler_Configure_Call) RunAndReturn(run func(id SurfaceID, width int32, height int32)) *MockHandler_Configure_Call {
135+ _c.Run(run)
136+ return _c
137+}
138+
139+// Error provides a mock function for the type MockHandler
140+func (_mock *MockHandler) Error(err error) {
141+ _mock.Called(err)
142+ return
143+}
144+
145+// MockHandler_Error_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Error'
146+type MockHandler_Error_Call struct {
147+ *mock.Call
148+}
149+
150+// Error is a helper method to define mock.On call
151+// - err error
152+func (_e *MockHandler_Expecter) Error(err any) *MockHandler_Error_Call {
153+ return &MockHandler_Error_Call{Call: _e.mock.On("Error", err)}
154+}
155+
156+func (_c *MockHandler_Error_Call) Run(run func(err error)) *MockHandler_Error_Call {
157+ _c.Call.Run(func(args mock.Arguments) {
158+ var arg0 error
159+ if args[0] != nil {
160+ arg0 = args[0].(error)
161+ }
162+ run(
163+ arg0,
164+ )
165+ })
166+ return _c
167+}
168+
169+func (_c *MockHandler_Error_Call) Return() *MockHandler_Error_Call {
170+ _c.Call.Return()
171+ return _c
172+}
173+
174+func (_c *MockHandler_Error_Call) RunAndReturn(run func(err error)) *MockHandler_Error_Call {
175+ _c.Run(run)
176+ return _c
177+}
178+
179+// FDReady provides a mock function for the type MockHandler
180+func (_mock *MockHandler) FDReady(id uint64) {
181+ _mock.Called(id)
182+ return
183+}
184+
185+// MockHandler_FDReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FDReady'
186+type MockHandler_FDReady_Call struct {
187+ *mock.Call
188+}
189+
190+// FDReady is a helper method to define mock.On call
191+// - id uint64
192+func (_e *MockHandler_Expecter) FDReady(id any) *MockHandler_FDReady_Call {
193+ return &MockHandler_FDReady_Call{Call: _e.mock.On("FDReady", id)}
194+}
195+
196+func (_c *MockHandler_FDReady_Call) Run(run func(id uint64)) *MockHandler_FDReady_Call {
197+ _c.Call.Run(func(args mock.Arguments) {
198+ var arg0 uint64
199+ if args[0] != nil {
200+ arg0 = args[0].(uint64)
201+ }
202+ run(
203+ arg0,
204+ )
205+ })
206+ return _c
207+}
208+
209+func (_c *MockHandler_FDReady_Call) Return() *MockHandler_FDReady_Call {
210+ _c.Call.Return()
211+ return _c
212+}
213+
214+func (_c *MockHandler_FDReady_Call) RunAndReturn(run func(id uint64)) *MockHandler_FDReady_Call {
215+ _c.Run(run)
216+ return _c
217+}
218+
219+// FeedbackDone provides a mock function for the type MockHandler
220+func (_mock *MockHandler) FeedbackDone(id SurfaceID) {
221+ _mock.Called(id)
222+ return
223+}
224+
225+// MockHandler_FeedbackDone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeedbackDone'
226+type MockHandler_FeedbackDone_Call struct {
227+ *mock.Call
228+}
229+
230+// FeedbackDone is a helper method to define mock.On call
231+// - id SurfaceID
232+func (_e *MockHandler_Expecter) FeedbackDone(id any) *MockHandler_FeedbackDone_Call {
233+ return &MockHandler_FeedbackDone_Call{Call: _e.mock.On("FeedbackDone", id)}
234+}
235+
236+func (_c *MockHandler_FeedbackDone_Call) Run(run func(id SurfaceID)) *MockHandler_FeedbackDone_Call {
237+ _c.Call.Run(func(args mock.Arguments) {
238+ var arg0 SurfaceID
239+ if args[0] != nil {
240+ arg0 = args[0].(SurfaceID)
241+ }
242+ run(
243+ arg0,
244+ )
245+ })
246+ return _c
247+}
248+
249+func (_c *MockHandler_FeedbackDone_Call) Return() *MockHandler_FeedbackDone_Call {
250+ _c.Call.Return()
251+ return _c
252+}
253+
254+func (_c *MockHandler_FeedbackDone_Call) RunAndReturn(run func(id SurfaceID)) *MockHandler_FeedbackDone_Call {
255+ _c.Run(run)
256+ return _c
257+}
258+
259+// Frame provides a mock function for the type MockHandler
260+func (_mock *MockHandler) Frame(id SurfaceID) {
261+ _mock.Called(id)
262+ return
263+}
264+
265+// MockHandler_Frame_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Frame'
266+type MockHandler_Frame_Call struct {
267+ *mock.Call
268+}
269+
270+// Frame is a helper method to define mock.On call
271+// - id SurfaceID
272+func (_e *MockHandler_Expecter) Frame(id any) *MockHandler_Frame_Call {
273+ return &MockHandler_Frame_Call{Call: _e.mock.On("Frame", id)}
274+}
275+
276+func (_c *MockHandler_Frame_Call) Run(run func(id SurfaceID)) *MockHandler_Frame_Call {
277+ _c.Call.Run(func(args mock.Arguments) {
278+ var arg0 SurfaceID
279+ if args[0] != nil {
280+ arg0 = args[0].(SurfaceID)
281+ }
282+ run(
283+ arg0,
284+ )
285+ })
286+ return _c
287+}
288+
289+func (_c *MockHandler_Frame_Call) Return() *MockHandler_Frame_Call {
290+ _c.Call.Return()
291+ return _c
292+}
293+
294+func (_c *MockHandler_Frame_Call) RunAndReturn(run func(id SurfaceID)) *MockHandler_Frame_Call {
295+ _c.Run(run)
296+ return _c
297+}
298+
299+// LockFinished provides a mock function for the type MockHandler
300+func (_mock *MockHandler) LockFinished() {
301+ _mock.Called()
302+ return
303+}
304+
305+// MockHandler_LockFinished_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LockFinished'
306+type MockHandler_LockFinished_Call struct {
307+ *mock.Call
308+}
309+
310+// LockFinished is a helper method to define mock.On call
311+func (_e *MockHandler_Expecter) LockFinished() *MockHandler_LockFinished_Call {
312+ return &MockHandler_LockFinished_Call{Call: _e.mock.On("LockFinished")}
313+}
314+
315+func (_c *MockHandler_LockFinished_Call) Run(run func()) *MockHandler_LockFinished_Call {
316+ _c.Call.Run(func(args mock.Arguments) {
317+ run()
318+ })
319+ return _c
320+}
321+
322+func (_c *MockHandler_LockFinished_Call) Return() *MockHandler_LockFinished_Call {
323+ _c.Call.Return()
324+ return _c
325+}
326+
327+func (_c *MockHandler_LockFinished_Call) RunAndReturn(run func()) *MockHandler_LockFinished_Call {
328+ _c.Run(run)
329+ return _c
330+}
331+
332+// Locked provides a mock function for the type MockHandler
333+func (_mock *MockHandler) Locked() {
334+ _mock.Called()
335+ return
336+}
337+
338+// MockHandler_Locked_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Locked'
339+type MockHandler_Locked_Call struct {
340+ *mock.Call
341+}
342+
343+// Locked is a helper method to define mock.On call
344+func (_e *MockHandler_Expecter) Locked() *MockHandler_Locked_Call {
345+ return &MockHandler_Locked_Call{Call: _e.mock.On("Locked")}
346+}
347+
348+func (_c *MockHandler_Locked_Call) Run(run func()) *MockHandler_Locked_Call {
349+ _c.Call.Run(func(args mock.Arguments) {
350+ run()
351+ })
352+ return _c
353+}
354+
355+func (_c *MockHandler_Locked_Call) Return() *MockHandler_Locked_Call {
356+ _c.Call.Return()
357+ return _c
358+}
359+
360+func (_c *MockHandler_Locked_Call) RunAndReturn(run func()) *MockHandler_Locked_Call {
361+ _c.Run(run)
362+ return _c
363+}
364+
365+// OutputAdded provides a mock function for the type MockHandler
366+func (_mock *MockHandler) OutputAdded(out *Output) {
367+ _mock.Called(out)
368+ return
369+}
370+
371+// MockHandler_OutputAdded_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OutputAdded'
372+type MockHandler_OutputAdded_Call struct {
373+ *mock.Call
374+}
375+
376+// OutputAdded is a helper method to define mock.On call
377+// - out *Output
378+func (_e *MockHandler_Expecter) OutputAdded(out any) *MockHandler_OutputAdded_Call {
379+ return &MockHandler_OutputAdded_Call{Call: _e.mock.On("OutputAdded", out)}
380+}
381+
382+func (_c *MockHandler_OutputAdded_Call) Run(run func(out *Output)) *MockHandler_OutputAdded_Call {
383+ _c.Call.Run(func(args mock.Arguments) {
384+ var arg0 *Output
385+ if args[0] != nil {
386+ arg0 = args[0].(*Output)
387+ }
388+ run(
389+ arg0,
390+ )
391+ })
392+ return _c
393+}
394+
395+func (_c *MockHandler_OutputAdded_Call) Return() *MockHandler_OutputAdded_Call {
396+ _c.Call.Return()
397+ return _c
398+}
399+
400+func (_c *MockHandler_OutputAdded_Call) RunAndReturn(run func(out *Output)) *MockHandler_OutputAdded_Call {
401+ _c.Run(run)
402+ return _c
403+}
404+
405+// OutputRemoved provides a mock function for the type MockHandler
406+func (_mock *MockHandler) OutputRemoved(global uint32) {
407+ _mock.Called(global)
408+ return
409+}
410+
411+// MockHandler_OutputRemoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OutputRemoved'
412+type MockHandler_OutputRemoved_Call struct {
413+ *mock.Call
414+}
415+
416+// OutputRemoved is a helper method to define mock.On call
417+// - global uint32
418+func (_e *MockHandler_Expecter) OutputRemoved(global any) *MockHandler_OutputRemoved_Call {
419+ return &MockHandler_OutputRemoved_Call{Call: _e.mock.On("OutputRemoved", global)}
420+}
421+
422+func (_c *MockHandler_OutputRemoved_Call) Run(run func(global uint32)) *MockHandler_OutputRemoved_Call {
423+ _c.Call.Run(func(args mock.Arguments) {
424+ var arg0 uint32
425+ if args[0] != nil {
426+ arg0 = args[0].(uint32)
427+ }
428+ run(
429+ arg0,
430+ )
431+ })
432+ return _c
433+}
434+
435+func (_c *MockHandler_OutputRemoved_Call) Return() *MockHandler_OutputRemoved_Call {
436+ _c.Call.Return()
437+ return _c
438+}
439+
440+func (_c *MockHandler_OutputRemoved_Call) RunAndReturn(run func(global uint32)) *MockHandler_OutputRemoved_Call {
441+ _c.Run(run)
442+ return _c
443+}
444+
445+// Scale provides a mock function for the type MockHandler
446+func (_mock *MockHandler) Scale(id SurfaceID, scale float64) {
447+ _mock.Called(id, scale)
448+ return
449+}
450+
451+// MockHandler_Scale_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Scale'
452+type MockHandler_Scale_Call struct {
453+ *mock.Call
454+}
455+
456+// Scale is a helper method to define mock.On call
457+// - id SurfaceID
458+// - scale float64
459+func (_e *MockHandler_Expecter) Scale(id any, scale any) *MockHandler_Scale_Call {
460+ return &MockHandler_Scale_Call{Call: _e.mock.On("Scale", id, scale)}
461+}
462+
463+func (_c *MockHandler_Scale_Call) Run(run func(id SurfaceID, scale float64)) *MockHandler_Scale_Call {
464+ _c.Call.Run(func(args mock.Arguments) {
465+ var arg0 SurfaceID
466+ if args[0] != nil {
467+ arg0 = args[0].(SurfaceID)
468+ }
469+ var arg1 float64
470+ if args[1] != nil {
471+ arg1 = args[1].(float64)
472+ }
473+ run(
474+ arg0,
475+ arg1,
476+ )
477+ })
478+ return _c
479+}
480+
481+func (_c *MockHandler_Scale_Call) Return() *MockHandler_Scale_Call {
482+ _c.Call.Return()
483+ return _c
484+}
485+
486+func (_c *MockHandler_Scale_Call) RunAndReturn(run func(id SurfaceID, scale float64)) *MockHandler_Scale_Call {
487+ _c.Run(run)
488+ return _c
489+}
@@ -0,0 +1,489 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package neferclient
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// NewMockHandler creates a new instance of MockHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func NewMockHandler(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *MockHandler {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &MockHandler{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// MockHandler is an autogenerated mock type for the Handler type
35+type MockHandler struct {
36+ mock.Mock
37+}
38+
39+type MockHandler_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *MockHandler) EXPECT() *MockHandler_Expecter {
44+ return &MockHandler_Expecter{mock: &_m.Mock}
45+}
46+
47+// Closed provides a mock function for the type MockHandler
48+func (_mock *MockHandler) Closed(id SurfaceID) {
49+ _mock.Called(id)
50+ return
51+}
52+
53+// MockHandler_Closed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Closed'
54+type MockHandler_Closed_Call struct {
55+ *mock.Call
56+}
57+
58+// Closed is a helper method to define mock.On call
59+// - id SurfaceID
60+func (_e *MockHandler_Expecter) Closed(id any) *MockHandler_Closed_Call {
61+ return &MockHandler_Closed_Call{Call: _e.mock.On("Closed", id)}
62+}
63+
64+func (_c *MockHandler_Closed_Call) Run(run func(id SurfaceID)) *MockHandler_Closed_Call {
65+ _c.Call.Run(func(args mock.Arguments) {
66+ var arg0 SurfaceID
67+ if args[0] != nil {
68+ arg0 = args[0].(SurfaceID)
69+ }
70+ run(
71+ arg0,
72+ )
73+ })
74+ return _c
75+}
76+
77+func (_c *MockHandler_Closed_Call) Return() *MockHandler_Closed_Call {
78+ _c.Call.Return()
79+ return _c
80+}
81+
82+func (_c *MockHandler_Closed_Call) RunAndReturn(run func(id SurfaceID)) *MockHandler_Closed_Call {
83+ _c.Run(run)
84+ return _c
85+}
86+
87+// Configure provides a mock function for the type MockHandler
88+func (_mock *MockHandler) Configure(id SurfaceID, width int32, height int32) {
89+ _mock.Called(id, width, height)
90+ return
91+}
92+
93+// MockHandler_Configure_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Configure'
94+type MockHandler_Configure_Call struct {
95+ *mock.Call
96+}
97+
98+// Configure is a helper method to define mock.On call
99+// - id SurfaceID
100+// - width int32
101+// - height int32
102+func (_e *MockHandler_Expecter) Configure(id any, width any, height any) *MockHandler_Configure_Call {
103+ return &MockHandler_Configure_Call{Call: _e.mock.On("Configure", id, width, height)}
104+}
105+
106+func (_c *MockHandler_Configure_Call) Run(run func(id SurfaceID, width int32, height int32)) *MockHandler_Configure_Call {
107+ _c.Call.Run(func(args mock.Arguments) {
108+ var arg0 SurfaceID
109+ if args[0] != nil {
110+ arg0 = args[0].(SurfaceID)
111+ }
112+ var arg1 int32
113+ if args[1] != nil {
114+ arg1 = args[1].(int32)
115+ }
116+ var arg2 int32
117+ if args[2] != nil {
118+ arg2 = args[2].(int32)
119+ }
120+ run(
121+ arg0,
122+ arg1,
123+ arg2,
124+ )
125+ })
126+ return _c
127+}
128+
129+func (_c *MockHandler_Configure_Call) Return() *MockHandler_Configure_Call {
130+ _c.Call.Return()
131+ return _c
132+}
133+
134+func (_c *MockHandler_Configure_Call) RunAndReturn(run func(id SurfaceID, width int32, height int32)) *MockHandler_Configure_Call {
135+ _c.Run(run)
136+ return _c
137+}
138+
139+// Error provides a mock function for the type MockHandler
140+func (_mock *MockHandler) Error(err error) {
141+ _mock.Called(err)
142+ return
143+}
144+
145+// MockHandler_Error_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Error'
146+type MockHandler_Error_Call struct {
147+ *mock.Call
148+}
149+
150+// Error is a helper method to define mock.On call
151+// - err error
152+func (_e *MockHandler_Expecter) Error(err any) *MockHandler_Error_Call {
153+ return &MockHandler_Error_Call{Call: _e.mock.On("Error", err)}
154+}
155+
156+func (_c *MockHandler_Error_Call) Run(run func(err error)) *MockHandler_Error_Call {
157+ _c.Call.Run(func(args mock.Arguments) {
158+ var arg0 error
159+ if args[0] != nil {
160+ arg0 = args[0].(error)
161+ }
162+ run(
163+ arg0,
164+ )
165+ })
166+ return _c
167+}
168+
169+func (_c *MockHandler_Error_Call) Return() *MockHandler_Error_Call {
170+ _c.Call.Return()
171+ return _c
172+}
173+
174+func (_c *MockHandler_Error_Call) RunAndReturn(run func(err error)) *MockHandler_Error_Call {
175+ _c.Run(run)
176+ return _c
177+}
178+
179+// FDReady provides a mock function for the type MockHandler
180+func (_mock *MockHandler) FDReady(id uint64) {
181+ _mock.Called(id)
182+ return
183+}
184+
185+// MockHandler_FDReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FDReady'
186+type MockHandler_FDReady_Call struct {
187+ *mock.Call
188+}
189+
190+// FDReady is a helper method to define mock.On call
191+// - id uint64
192+func (_e *MockHandler_Expecter) FDReady(id any) *MockHandler_FDReady_Call {
193+ return &MockHandler_FDReady_Call{Call: _e.mock.On("FDReady", id)}
194+}
195+
196+func (_c *MockHandler_FDReady_Call) Run(run func(id uint64)) *MockHandler_FDReady_Call {
197+ _c.Call.Run(func(args mock.Arguments) {
198+ var arg0 uint64
199+ if args[0] != nil {
200+ arg0 = args[0].(uint64)
201+ }
202+ run(
203+ arg0,
204+ )
205+ })
206+ return _c
207+}
208+
209+func (_c *MockHandler_FDReady_Call) Return() *MockHandler_FDReady_Call {
210+ _c.Call.Return()
211+ return _c
212+}
213+
214+func (_c *MockHandler_FDReady_Call) RunAndReturn(run func(id uint64)) *MockHandler_FDReady_Call {
215+ _c.Run(run)
216+ return _c
217+}
218+
219+// FeedbackDone provides a mock function for the type MockHandler
220+func (_mock *MockHandler) FeedbackDone(id SurfaceID) {
221+ _mock.Called(id)
222+ return
223+}
224+
225+// MockHandler_FeedbackDone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeedbackDone'
226+type MockHandler_FeedbackDone_Call struct {
227+ *mock.Call
228+}
229+
230+// FeedbackDone is a helper method to define mock.On call
231+// - id SurfaceID
232+func (_e *MockHandler_Expecter) FeedbackDone(id any) *MockHandler_FeedbackDone_Call {
233+ return &MockHandler_FeedbackDone_Call{Call: _e.mock.On("FeedbackDone", id)}
234+}
235+
236+func (_c *MockHandler_FeedbackDone_Call) Run(run func(id SurfaceID)) *MockHandler_FeedbackDone_Call {
237+ _c.Call.Run(func(args mock.Arguments) {
238+ var arg0 SurfaceID
239+ if args[0] != nil {
240+ arg0 = args[0].(SurfaceID)
241+ }
242+ run(
243+ arg0,
244+ )
245+ })
246+ return _c
247+}
248+
249+func (_c *MockHandler_FeedbackDone_Call) Return() *MockHandler_FeedbackDone_Call {
250+ _c.Call.Return()
251+ return _c
252+}
253+
254+func (_c *MockHandler_FeedbackDone_Call) RunAndReturn(run func(id SurfaceID)) *MockHandler_FeedbackDone_Call {
255+ _c.Run(run)
256+ return _c
257+}
258+
259+// Frame provides a mock function for the type MockHandler
260+func (_mock *MockHandler) Frame(id SurfaceID) {
261+ _mock.Called(id)
262+ return
263+}
264+
265+// MockHandler_Frame_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Frame'
266+type MockHandler_Frame_Call struct {
267+ *mock.Call
268+}
269+
270+// Frame is a helper method to define mock.On call
271+// - id SurfaceID
272+func (_e *MockHandler_Expecter) Frame(id any) *MockHandler_Frame_Call {
273+ return &MockHandler_Frame_Call{Call: _e.mock.On("Frame", id)}
274+}
275+
276+func (_c *MockHandler_Frame_Call) Run(run func(id SurfaceID)) *MockHandler_Frame_Call {
277+ _c.Call.Run(func(args mock.Arguments) {
278+ var arg0 SurfaceID
279+ if args[0] != nil {
280+ arg0 = args[0].(SurfaceID)
281+ }
282+ run(
283+ arg0,
284+ )
285+ })
286+ return _c
287+}
288+
289+func (_c *MockHandler_Frame_Call) Return() *MockHandler_Frame_Call {
290+ _c.Call.Return()
291+ return _c
292+}
293+
294+func (_c *MockHandler_Frame_Call) RunAndReturn(run func(id SurfaceID)) *MockHandler_Frame_Call {
295+ _c.Run(run)
296+ return _c
297+}
298+
299+// LockFinished provides a mock function for the type MockHandler
300+func (_mock *MockHandler) LockFinished() {
301+ _mock.Called()
302+ return
303+}
304+
305+// MockHandler_LockFinished_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LockFinished'
306+type MockHandler_LockFinished_Call struct {
307+ *mock.Call
308+}
309+
310+// LockFinished is a helper method to define mock.On call
311+func (_e *MockHandler_Expecter) LockFinished() *MockHandler_LockFinished_Call {
312+ return &MockHandler_LockFinished_Call{Call: _e.mock.On("LockFinished")}
313+}
314+
315+func (_c *MockHandler_LockFinished_Call) Run(run func()) *MockHandler_LockFinished_Call {
316+ _c.Call.Run(func(args mock.Arguments) {
317+ run()
318+ })
319+ return _c
320+}
321+
322+func (_c *MockHandler_LockFinished_Call) Return() *MockHandler_LockFinished_Call {
323+ _c.Call.Return()
324+ return _c
325+}
326+
327+func (_c *MockHandler_LockFinished_Call) RunAndReturn(run func()) *MockHandler_LockFinished_Call {
328+ _c.Run(run)
329+ return _c
330+}
331+
332+// Locked provides a mock function for the type MockHandler
333+func (_mock *MockHandler) Locked() {
334+ _mock.Called()
335+ return
336+}
337+
338+// MockHandler_Locked_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Locked'
339+type MockHandler_Locked_Call struct {
340+ *mock.Call
341+}
342+
343+// Locked is a helper method to define mock.On call
344+func (_e *MockHandler_Expecter) Locked() *MockHandler_Locked_Call {
345+ return &MockHandler_Locked_Call{Call: _e.mock.On("Locked")}
346+}
347+
348+func (_c *MockHandler_Locked_Call) Run(run func()) *MockHandler_Locked_Call {
349+ _c.Call.Run(func(args mock.Arguments) {
350+ run()
351+ })
352+ return _c
353+}
354+
355+func (_c *MockHandler_Locked_Call) Return() *MockHandler_Locked_Call {
356+ _c.Call.Return()
357+ return _c
358+}
359+
360+func (_c *MockHandler_Locked_Call) RunAndReturn(run func()) *MockHandler_Locked_Call {
361+ _c.Run(run)
362+ return _c
363+}
364+
365+// OutputAdded provides a mock function for the type MockHandler
366+func (_mock *MockHandler) OutputAdded(out *Output) {
367+ _mock.Called(out)
368+ return
369+}
370+
371+// MockHandler_OutputAdded_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OutputAdded'
372+type MockHandler_OutputAdded_Call struct {
373+ *mock.Call
374+}
375+
376+// OutputAdded is a helper method to define mock.On call
377+// - out *Output
378+func (_e *MockHandler_Expecter) OutputAdded(out any) *MockHandler_OutputAdded_Call {
379+ return &MockHandler_OutputAdded_Call{Call: _e.mock.On("OutputAdded", out)}
380+}
381+
382+func (_c *MockHandler_OutputAdded_Call) Run(run func(out *Output)) *MockHandler_OutputAdded_Call {
383+ _c.Call.Run(func(args mock.Arguments) {
384+ var arg0 *Output
385+ if args[0] != nil {
386+ arg0 = args[0].(*Output)
387+ }
388+ run(
389+ arg0,
390+ )
391+ })
392+ return _c
393+}
394+
395+func (_c *MockHandler_OutputAdded_Call) Return() *MockHandler_OutputAdded_Call {
396+ _c.Call.Return()
397+ return _c
398+}
399+
400+func (_c *MockHandler_OutputAdded_Call) RunAndReturn(run func(out *Output)) *MockHandler_OutputAdded_Call {
401+ _c.Run(run)
402+ return _c
403+}
404+
405+// OutputRemoved provides a mock function for the type MockHandler
406+func (_mock *MockHandler) OutputRemoved(global uint32) {
407+ _mock.Called(global)
408+ return
409+}
410+
411+// MockHandler_OutputRemoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OutputRemoved'
412+type MockHandler_OutputRemoved_Call struct {
413+ *mock.Call
414+}
415+
416+// OutputRemoved is a helper method to define mock.On call
417+// - global uint32
418+func (_e *MockHandler_Expecter) OutputRemoved(global any) *MockHandler_OutputRemoved_Call {
419+ return &MockHandler_OutputRemoved_Call{Call: _e.mock.On("OutputRemoved", global)}
420+}
421+
422+func (_c *MockHandler_OutputRemoved_Call) Run(run func(global uint32)) *MockHandler_OutputRemoved_Call {
423+ _c.Call.Run(func(args mock.Arguments) {
424+ var arg0 uint32
425+ if args[0] != nil {
426+ arg0 = args[0].(uint32)
427+ }
428+ run(
429+ arg0,
430+ )
431+ })
432+ return _c
433+}
434+
435+func (_c *MockHandler_OutputRemoved_Call) Return() *MockHandler_OutputRemoved_Call {
436+ _c.Call.Return()
437+ return _c
438+}
439+
440+func (_c *MockHandler_OutputRemoved_Call) RunAndReturn(run func(global uint32)) *MockHandler_OutputRemoved_Call {
441+ _c.Run(run)
442+ return _c
443+}
444+
445+// Scale provides a mock function for the type MockHandler
446+func (_mock *MockHandler) Scale(id SurfaceID, scale float64) {
447+ _mock.Called(id, scale)
448+ return
449+}
450+
451+// MockHandler_Scale_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Scale'
452+type MockHandler_Scale_Call struct {
453+ *mock.Call
454+}
455+
456+// Scale is a helper method to define mock.On call
457+// - id SurfaceID
458+// - scale float64
459+func (_e *MockHandler_Expecter) Scale(id any, scale any) *MockHandler_Scale_Call {
460+ return &MockHandler_Scale_Call{Call: _e.mock.On("Scale", id, scale)}
461+}
462+
463+func (_c *MockHandler_Scale_Call) Run(run func(id SurfaceID, scale float64)) *MockHandler_Scale_Call {
464+ _c.Call.Run(func(args mock.Arguments) {
465+ var arg0 SurfaceID
466+ if args[0] != nil {
467+ arg0 = args[0].(SurfaceID)
468+ }
469+ var arg1 float64
470+ if args[1] != nil {
471+ arg1 = args[1].(float64)
472+ }
473+ run(
474+ arg0,
475+ arg1,
476+ )
477+ })
478+ return _c
479+}
480+
481+func (_c *MockHandler_Scale_Call) Return() *MockHandler_Scale_Call {
482+ _c.Call.Return()
483+ return _c
484+}
485+
486+func (_c *MockHandler_Scale_Call) RunAndReturn(run func(id SurfaceID, scale float64)) *MockHandler_Scale_Call {
487+ _c.Run(run)
488+ return _c
489+}
A headless_surface_test.go +106 -0
@@ -0,0 +1,106 @@
1+package neferclient_test
2+
3+import (
4+ "context"
5+ "testing"
6+ "time"
7+
8+ "github.com/stretchr/testify/mock"
9+ "github.com/stretchr/testify/require"
10+
11+ "github.com/bnema/neferclient"
12+ neferclientmocks "github.com/bnema/neferclient/mocks"
13+)
14+
15+func connectHeadless(t *testing.T) *neferclient.Conn {
16+ t.Helper()
17+ socket := startHeadless(t)
18+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
19+ defer cancel()
20+ for {
21+ c, err := neferclient.Connect(ctx, socket)
22+ if err == nil {
23+ t.Cleanup(func() { _ = c.Close() })
24+ for len(c.Outputs()) == 0 { // the virtual output may arrive late
25+ select {
26+ case <-c.Wake():
27+ require.NoError(t, c.Dispatch(nil))
28+ case <-ctx.Done():
29+ t.Fatal("no output announced")
30+ }
31+ }
32+ return c
33+ }
34+ if ctx.Err() != nil {
35+ t.Fatal(err)
36+ }
37+ time.Sleep(5 * time.Millisecond)
38+ }
39+}
40+
41+func TestHeadlessLayerSurface(t *testing.T) {
42+ c := connectHeadless(t)
43+ s, err := c.NewLayerSurface(neferclient.LayerConfig{
44+ Level: neferclient.LayerTop, Anchors: neferclient.AnchorTop | neferclient.AnchorLeft,
45+ Width: 200, Height: 50,
46+ })
47+ require.NoError(t, err)
48+
49+ var configured, feedback bool
50+ var cw, ch int32
51+ h := neferclientmocks.NewMockHandler(t)
52+ h.EXPECT().Configure(s.ID(), mock.Anything, mock.Anything).Run(func(_ neferclient.SurfaceID, w, hh int32) {
53+ configured, cw, ch = true, w, hh
54+ }).Return().Maybe()
55+ h.EXPECT().FeedbackDone(s.ID()).Run(func(neferclient.SurfaceID) { feedback = true }).Return().Maybe()
56+ h.EXPECT().Scale(s.ID(), mock.Anything).Return().Maybe()
57+ dispatchUntil(t, c, h, func() bool { return configured && feedback })
58+
59+ require.Equal(t, int32(200), cw)
60+ require.Equal(t, int32(50), ch)
61+ fb := s.Feedback()
62+ require.NotNil(t, fb)
63+ require.NotZero(t, fb.MainDevice)
64+ require.NotEmpty(t, fb.Formats)
65+ t.Logf("main device %#x, %d formats", fb.MainDevice, len(fb.Formats))
66+ pw, ph, err := s.PhysicalSize()
67+ require.NoError(t, err)
68+ require.Positive(t, pw)
69+ require.Positive(t, ph)
70+ require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 0, Y: 0, Width: 10, Height: 10}}))
71+ require.NoError(t, s.Close())
72+ require.NoError(t, c.Roundtrip())
73+}
74+
75+func TestHeadlessLockUnlock(t *testing.T) {
76+ c := connectHeadless(t)
77+ l, err := c.Lock()
78+ require.NoError(t, err)
79+ require.Error(t, l.Unlock(), "unlock before locked must be refused")
80+
81+ var locked bool
82+ var surfaces []*neferclient.Surface
83+ for _, o := range c.Outputs() {
84+ s, err := l.NewSurface(o.Global)
85+ require.NoError(t, err)
86+ surfaces = append(surfaces, s)
87+ }
88+ h := neferclientmocks.NewMockHandler(t)
89+ h.EXPECT().Locked().Run(func() { locked = true }).Return().Maybe()
90+ h.EXPECT().Configure(mock.Anything, mock.Anything, mock.Anything).Return().Maybe()
91+ h.EXPECT().FeedbackDone(mock.Anything).Return().Maybe()
92+ h.EXPECT().Scale(mock.Anything, mock.Anything).Return().Maybe()
93+ dispatchUntil(t, c, h, func() bool { return locked })
94+ require.True(t, l.Locked())
95+ for _, s := range surfaces {
96+ w, hh, _ := s.Size()
97+ t.Logf("lock surface %d: %dx%d", s.ID(), w, hh)
98+ }
99+ require.Error(t, l.Close(), "a locked session is not closed, only unlocked")
100+ require.NoError(t, l.Unlock())
101+ require.False(t, l.Locked())
102+ for _, s := range surfaces {
103+ require.NoError(t, s.Close())
104+ }
105+ require.NoError(t, c.Roundtrip())
106+}
@@ -0,0 +1,106 @@
1+package neferclient_test
2+
3+import (
4+ "context"
5+ "testing"
6+ "time"
7+
8+ "github.com/stretchr/testify/mock"
9+ "github.com/stretchr/testify/require"
10+
11+ "github.com/bnema/neferclient"
12+ neferclientmocks "github.com/bnema/neferclient/mocks"
13+)
14+
15+func connectHeadless(t *testing.T) *neferclient.Conn {
16+ t.Helper()
17+ socket := startHeadless(t)
18+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
19+ defer cancel()
20+ for {
21+ c, err := neferclient.Connect(ctx, socket)
22+ if err == nil {
23+ t.Cleanup(func() { _ = c.Close() })
24+ for len(c.Outputs()) == 0 { // the virtual output may arrive late
25+ select {
26+ case <-c.Wake():
27+ require.NoError(t, c.Dispatch(nil))
28+ case <-ctx.Done():
29+ t.Fatal("no output announced")
30+ }
31+ }
32+ return c
33+ }
34+ if ctx.Err() != nil {
35+ t.Fatal(err)
36+ }
37+ time.Sleep(5 * time.Millisecond)
38+ }
39+}
40+
41+func TestHeadlessLayerSurface(t *testing.T) {
42+ c := connectHeadless(t)
43+ s, err := c.NewLayerSurface(neferclient.LayerConfig{
44+ Level: neferclient.LayerTop, Anchors: neferclient.AnchorTop | neferclient.AnchorLeft,
45+ Width: 200, Height: 50,
46+ })
47+ require.NoError(t, err)
48+
49+ var configured, feedback bool
50+ var cw, ch int32
51+ h := neferclientmocks.NewMockHandler(t)
52+ h.EXPECT().Configure(s.ID(), mock.Anything, mock.Anything).Run(func(_ neferclient.SurfaceID, w, hh int32) {
53+ configured, cw, ch = true, w, hh
54+ }).Return().Maybe()
55+ h.EXPECT().FeedbackDone(s.ID()).Run(func(neferclient.SurfaceID) { feedback = true }).Return().Maybe()
56+ h.EXPECT().Scale(s.ID(), mock.Anything).Return().Maybe()
57+ dispatchUntil(t, c, h, func() bool { return configured && feedback })
58+
59+ require.Equal(t, int32(200), cw)
60+ require.Equal(t, int32(50), ch)
61+ fb := s.Feedback()
62+ require.NotNil(t, fb)
63+ require.NotZero(t, fb.MainDevice)
64+ require.NotEmpty(t, fb.Formats)
65+ t.Logf("main device %#x, %d formats", fb.MainDevice, len(fb.Formats))
66+ pw, ph, err := s.PhysicalSize()
67+ require.NoError(t, err)
68+ require.Positive(t, pw)
69+ require.Positive(t, ph)
70+ require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 0, Y: 0, Width: 10, Height: 10}}))
71+ require.NoError(t, s.Close())
72+ require.NoError(t, c.Roundtrip())
73+}
74+
75+func TestHeadlessLockUnlock(t *testing.T) {
76+ c := connectHeadless(t)
77+ l, err := c.Lock()
78+ require.NoError(t, err)
79+ require.Error(t, l.Unlock(), "unlock before locked must be refused")
80+
81+ var locked bool
82+ var surfaces []*neferclient.Surface
83+ for _, o := range c.Outputs() {
84+ s, err := l.NewSurface(o.Global)
85+ require.NoError(t, err)
86+ surfaces = append(surfaces, s)
87+ }
88+ h := neferclientmocks.NewMockHandler(t)
89+ h.EXPECT().Locked().Run(func() { locked = true }).Return().Maybe()
90+ h.EXPECT().Configure(mock.Anything, mock.Anything, mock.Anything).Return().Maybe()
91+ h.EXPECT().FeedbackDone(mock.Anything).Return().Maybe()
92+ h.EXPECT().Scale(mock.Anything, mock.Anything).Return().Maybe()
93+ dispatchUntil(t, c, h, func() bool { return locked })
94+ require.True(t, l.Locked())
95+ for _, s := range surfaces {
96+ w, hh, _ := s.Size()
97+ t.Logf("lock surface %d: %dx%d", s.ID(), w, hh)
98+ }
99+ require.Error(t, l.Close(), "a locked session is not closed, only unlocked")
100+ require.NoError(t, l.Unlock())
101+ require.False(t, l.Locked())
102+ for _, s := range surfaces {
103+ require.NoError(t, s.Close())
104+ }
105+ require.NoError(t, c.Roundtrip())
106+}
A lock.go +162 -0
@@ -0,0 +1,162 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/go-wayland-bindings/client/extsessionlock"
8+ "github.com/bnema/wlturbo/wl"
9+)
10+
11+// Lock is an ext-session-lock-v1 session lock. It reports [Handler.Locked]
12+// once every output is protected and [Handler.LockFinished] if the compositor
13+// refuses or ends it. Closing the connection or destroying surfaces never
14+// unlocks; only [Lock.Unlock] after Locked does. Owner goroutine only.
15+type Lock struct {
16+ c *Conn
17+ gen uint32
18+ obj *extsessionlock.ExtSessionLock
19+ locked bool
20+ finished bool
21+ done bool
22+}
23+
24+// Lock requests a session lock. At most one lock is live per connection.
25+// The compositor answers through Handler.Locked or Handler.LockFinished.
26+func (c *Conn) Lock() (*Lock, error) {
27+ if c.closed {
28+ return nil, ErrClosed
29+ }
30+ if c.curLock != nil {
31+ return nil, errors.New("neferclient: session lock already acquired")
32+ }
33+ if err := c.ensureCore(); err != nil {
34+ return nil, err
35+ }
36+ if err := c.ensureLockManager(); err != nil {
37+ return nil, err
38+ }
39+ c.pause() // locked/finished may follow the request immediately
40+ defer c.resume()
41+ c.nextLock++
42+ l := &Lock{c: c, gen: c.nextLock}
43+ q, gen := c.q, l.gen
44+ obj, err := c.g.lockMgr.Lock()
45+ if err != nil {
46+ return nil, err
47+ }
48+ obj.OnLocked(func() {
49+ e := event{kind: evLocked, id: gen}
50+ q.post(&e)
51+ })
52+ obj.OnFinished(func() {
53+ e := event{kind: evLockFinished, id: gen}
54+ q.post(&e)
55+ })
56+ l.obj = obj
57+ c.curLock = l
58+ return l, nil
59+}
60+
61+// Locked reports whether the compositor confirmed the lock (applied by
62+// Dispatch) and it has not finished.
63+func (l *Lock) Locked() bool { return l.locked && !l.finished && !l.done }
64+
65+// NewSurface creates the lock surface covering the output with the given
66+// registry name (see [Output.Global]). It becomes presentable after the first
67+// Handler.Configure; a lock surface is always opaque and takes all input.
68+func (l *Lock) NewSurface(output uint32) (*Surface, error) {
69+ c := l.c
70+ if c.closed {
71+ return nil, ErrClosed
72+ }
73+ if l.done || l.finished {
74+ return nil, errors.New("neferclient: lock ended")
75+ }
76+ e := c.entry(output)
77+ if e == nil || !e.done {
78+ return nil, fmt.Errorf("neferclient: lock output %d unavailable", output)
79+ }
80+ for _, other := range c.surfaces {
81+ if other.lock == l && other.output == output {
82+ return nil, fmt.Errorf("neferclient: output %d already has a lock surface", output)
83+ }
84+ }
85+ c.pause()
86+ defer c.resume()
87+ s, err := c.newSurface(roleLock, 0, 0)
88+ if err != nil {
89+ return nil, err
90+ }
91+ ok := false
92+ defer func() {
93+ if !ok {
94+ s.destroy()
95+ }
96+ }()
97+ if s.lockSurf, err = l.obj.GetLockSurface(s.surf, e.proxy); err != nil {
98+ return nil, err
99+ }
100+ s.lock, s.output = l, output
101+ q, sid := c.q, uint32(s.id)
102+ s.lockSurf.OnConfigure(func(serial, w, h uint32) {
103+ ev := configureEvent(evLockConfigure, sid, serial, w, h)
104+ q.post(&ev)
105+ })
106+ // ext-session-lock forbids the initial empty commit: the configure comes
107+ // with get_lock_surface and the first commit carries a buffer.
108+ if err = s.finishSurface(); err != nil {
109+ return nil, err
110+ }
111+ ok = true
112+ return s, nil
113+}
114+
115+// Unlock sends unlock_and_destroy and roundtrips so the compositor processed
116+// it before the caller closes anything. It is only valid after Handler.Locked.
117+// The lock surfaces must be closed afterwards (or before: both are legal).
118+func (l *Lock) Unlock() error {
119+ if l.done {
120+ return errors.New("neferclient: lock already destroyed")
121+ }
122+ if !l.Locked() {
123+ return errors.New("neferclient: unlock before the locked event")
124+ }
125+ if err := l.obj.UnlockAndDestroy(); err != nil {
126+ return err
127+ }
128+ l.done = true
129+ l.c.curLock = nil
130+ return l.c.Roundtrip()
131+}
132+
133+// Close destroys a lock that never reached Locked (refused or abandoned). A
134+// locked session is only ended by Unlock, so Close fails once Locked was
135+// applied. Because the compositor may have sent locked before it processes the
136+// destroy request (which would then be a protocol error), Close first
137+// roundtrips and looks through the queued events: if a locked event for this
138+// lock is already queued it is treated as applied (Handler.Locked is still
139+// delivered by the next Dispatch) and Close fails with the same error.
140+func (l *Lock) Close() error {
141+ if l.done {
142+ return nil
143+ }
144+ if !l.locked && !l.finished {
145+ if err := l.c.Roundtrip(); err != nil {
146+ return err
147+ }
148+ l.locked = l.locked || l.c.q.hasLocked(l.gen)
149+ }
150+ if l.locked && !l.finished {
151+ return errors.New("neferclient: lock is locked: use Unlock")
152+ }
153+ l.done = true
154+ l.c.curLock = nil
155+ var err error
156+ if l.obj != nil {
157+ err = l.obj.Destroy()
158+ }
159+ return err
160+}
161+
162+var _ wl.Proxy = (*frameCallback)(nil)
@@ -0,0 +1,162 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/go-wayland-bindings/client/extsessionlock"
8+ "github.com/bnema/wlturbo/wl"
9+)
10+
11+// Lock is an ext-session-lock-v1 session lock. It reports [Handler.Locked]
12+// once every output is protected and [Handler.LockFinished] if the compositor
13+// refuses or ends it. Closing the connection or destroying surfaces never
14+// unlocks; only [Lock.Unlock] after Locked does. Owner goroutine only.
15+type Lock struct {
16+ c *Conn
17+ gen uint32
18+ obj *extsessionlock.ExtSessionLock
19+ locked bool
20+ finished bool
21+ done bool
22+}
23+
24+// Lock requests a session lock. At most one lock is live per connection.
25+// The compositor answers through Handler.Locked or Handler.LockFinished.
26+func (c *Conn) Lock() (*Lock, error) {
27+ if c.closed {
28+ return nil, ErrClosed
29+ }
30+ if c.curLock != nil {
31+ return nil, errors.New("neferclient: session lock already acquired")
32+ }
33+ if err := c.ensureCore(); err != nil {
34+ return nil, err
35+ }
36+ if err := c.ensureLockManager(); err != nil {
37+ return nil, err
38+ }
39+ c.pause() // locked/finished may follow the request immediately
40+ defer c.resume()
41+ c.nextLock++
42+ l := &Lock{c: c, gen: c.nextLock}
43+ q, gen := c.q, l.gen
44+ obj, err := c.g.lockMgr.Lock()
45+ if err != nil {
46+ return nil, err
47+ }
48+ obj.OnLocked(func() {
49+ e := event{kind: evLocked, id: gen}
50+ q.post(&e)
51+ })
52+ obj.OnFinished(func() {
53+ e := event{kind: evLockFinished, id: gen}
54+ q.post(&e)
55+ })
56+ l.obj = obj
57+ c.curLock = l
58+ return l, nil
59+}
60+
61+// Locked reports whether the compositor confirmed the lock (applied by
62+// Dispatch) and it has not finished.
63+func (l *Lock) Locked() bool { return l.locked && !l.finished && !l.done }
64+
65+// NewSurface creates the lock surface covering the output with the given
66+// registry name (see [Output.Global]). It becomes presentable after the first
67+// Handler.Configure; a lock surface is always opaque and takes all input.
68+func (l *Lock) NewSurface(output uint32) (*Surface, error) {
69+ c := l.c
70+ if c.closed {
71+ return nil, ErrClosed
72+ }
73+ if l.done || l.finished {
74+ return nil, errors.New("neferclient: lock ended")
75+ }
76+ e := c.entry(output)
77+ if e == nil || !e.done {
78+ return nil, fmt.Errorf("neferclient: lock output %d unavailable", output)
79+ }
80+ for _, other := range c.surfaces {
81+ if other.lock == l && other.output == output {
82+ return nil, fmt.Errorf("neferclient: output %d already has a lock surface", output)
83+ }
84+ }
85+ c.pause()
86+ defer c.resume()
87+ s, err := c.newSurface(roleLock, 0, 0)
88+ if err != nil {
89+ return nil, err
90+ }
91+ ok := false
92+ defer func() {
93+ if !ok {
94+ s.destroy()
95+ }
96+ }()
97+ if s.lockSurf, err = l.obj.GetLockSurface(s.surf, e.proxy); err != nil {
98+ return nil, err
99+ }
100+ s.lock, s.output = l, output
101+ q, sid := c.q, uint32(s.id)
102+ s.lockSurf.OnConfigure(func(serial, w, h uint32) {
103+ ev := configureEvent(evLockConfigure, sid, serial, w, h)
104+ q.post(&ev)
105+ })
106+ // ext-session-lock forbids the initial empty commit: the configure comes
107+ // with get_lock_surface and the first commit carries a buffer.
108+ if err = s.finishSurface(); err != nil {
109+ return nil, err
110+ }
111+ ok = true
112+ return s, nil
113+}
114+
115+// Unlock sends unlock_and_destroy and roundtrips so the compositor processed
116+// it before the caller closes anything. It is only valid after Handler.Locked.
117+// The lock surfaces must be closed afterwards (or before: both are legal).
118+func (l *Lock) Unlock() error {
119+ if l.done {
120+ return errors.New("neferclient: lock already destroyed")
121+ }
122+ if !l.Locked() {
123+ return errors.New("neferclient: unlock before the locked event")
124+ }
125+ if err := l.obj.UnlockAndDestroy(); err != nil {
126+ return err
127+ }
128+ l.done = true
129+ l.c.curLock = nil
130+ return l.c.Roundtrip()
131+}
132+
133+// Close destroys a lock that never reached Locked (refused or abandoned). A
134+// locked session is only ended by Unlock, so Close fails once Locked was
135+// applied. Because the compositor may have sent locked before it processes the
136+// destroy request (which would then be a protocol error), Close first
137+// roundtrips and looks through the queued events: if a locked event for this
138+// lock is already queued it is treated as applied (Handler.Locked is still
139+// delivered by the next Dispatch) and Close fails with the same error.
140+func (l *Lock) Close() error {
141+ if l.done {
142+ return nil
143+ }
144+ if !l.locked && !l.finished {
145+ if err := l.c.Roundtrip(); err != nil {
146+ return err
147+ }
148+ l.locked = l.locked || l.c.q.hasLocked(l.gen)
149+ }
150+ if l.locked && !l.finished {
151+ return errors.New("neferclient: lock is locked: use Unlock")
152+ }
153+ l.done = true
154+ l.c.curLock = nil
155+ var err error
156+ if l.obj != nil {
157+ err = l.obj.Destroy()
158+ }
159+ return err
160+}
161+
162+var _ wl.Proxy = (*frameCallback)(nil)
M mocks/handler_mock.go +284 -0
@@ -45,6 +45,98 @@ func (_m *MockHandler) EXPECT() *MockHandler_Expecter {
4545 return &MockHandler_Expecter{mock: &_m.Mock}
4646 }
4747
48+// Closed provides a mock function for the type MockHandler
49+func (_mock *MockHandler) Closed(id neferclient.SurfaceID) {
50+ _mock.Called(id)
51+ return
52+}
53+
54+// MockHandler_Closed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Closed'
55+type MockHandler_Closed_Call struct {
56+ *mock.Call
57+}
58+
59+// Closed is a helper method to define mock.On call
60+// - id neferclient.SurfaceID
61+func (_e *MockHandler_Expecter) Closed(id any) *MockHandler_Closed_Call {
62+ return &MockHandler_Closed_Call{Call: _e.mock.On("Closed", id)}
63+}
64+
65+func (_c *MockHandler_Closed_Call) Run(run func(id neferclient.SurfaceID)) *MockHandler_Closed_Call {
66+ _c.Call.Run(func(args mock.Arguments) {
67+ var arg0 neferclient.SurfaceID
68+ if args[0] != nil {
69+ arg0 = args[0].(neferclient.SurfaceID)
70+ }
71+ run(
72+ arg0,
73+ )
74+ })
75+ return _c
76+}
77+
78+func (_c *MockHandler_Closed_Call) Return() *MockHandler_Closed_Call {
79+ _c.Call.Return()
80+ return _c
81+}
82+
83+func (_c *MockHandler_Closed_Call) RunAndReturn(run func(id neferclient.SurfaceID)) *MockHandler_Closed_Call {
84+ _c.Run(run)
85+ return _c
86+}
87+
88+// Configure provides a mock function for the type MockHandler
89+func (_mock *MockHandler) Configure(id neferclient.SurfaceID, width int32, height int32) {
90+ _mock.Called(id, width, height)
91+ return
92+}
93+
94+// MockHandler_Configure_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Configure'
95+type MockHandler_Configure_Call struct {
96+ *mock.Call
97+}
98+
99+// Configure is a helper method to define mock.On call
100+// - id neferclient.SurfaceID
101+// - width int32
102+// - height int32
103+func (_e *MockHandler_Expecter) Configure(id any, width any, height any) *MockHandler_Configure_Call {
104+ return &MockHandler_Configure_Call{Call: _e.mock.On("Configure", id, width, height)}
105+}
106+
107+func (_c *MockHandler_Configure_Call) Run(run func(id neferclient.SurfaceID, width int32, height int32)) *MockHandler_Configure_Call {
108+ _c.Call.Run(func(args mock.Arguments) {
109+ var arg0 neferclient.SurfaceID
110+ if args[0] != nil {
111+ arg0 = args[0].(neferclient.SurfaceID)
112+ }
113+ var arg1 int32
114+ if args[1] != nil {
115+ arg1 = args[1].(int32)
116+ }
117+ var arg2 int32
118+ if args[2] != nil {
119+ arg2 = args[2].(int32)
120+ }
121+ run(
122+ arg0,
123+ arg1,
124+ arg2,
125+ )
126+ })
127+ return _c
128+}
129+
130+func (_c *MockHandler_Configure_Call) Return() *MockHandler_Configure_Call {
131+ _c.Call.Return()
132+ return _c
133+}
134+
135+func (_c *MockHandler_Configure_Call) RunAndReturn(run func(id neferclient.SurfaceID, width int32, height int32)) *MockHandler_Configure_Call {
136+ _c.Run(run)
137+ return _c
138+}
139+
48140 // Error provides a mock function for the type MockHandler
49141 func (_mock *MockHandler) Error(err error) {
50142 _mock.Called(err)
@@ -125,6 +217,152 @@ func (_c *MockHandler_FDReady_Call) RunAndReturn(run func(id uint64)) *MockHandl
125217 return _c
126218 }
127219
220+// FeedbackDone provides a mock function for the type MockHandler
221+func (_mock *MockHandler) FeedbackDone(id neferclient.SurfaceID) {
222+ _mock.Called(id)
223+ return
224+}
225+
226+// MockHandler_FeedbackDone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeedbackDone'
227+type MockHandler_FeedbackDone_Call struct {
228+ *mock.Call
229+}
230+
231+// FeedbackDone is a helper method to define mock.On call
232+// - id neferclient.SurfaceID
233+func (_e *MockHandler_Expecter) FeedbackDone(id any) *MockHandler_FeedbackDone_Call {
234+ return &MockHandler_FeedbackDone_Call{Call: _e.mock.On("FeedbackDone", id)}
235+}
236+
237+func (_c *MockHandler_FeedbackDone_Call) Run(run func(id neferclient.SurfaceID)) *MockHandler_FeedbackDone_Call {
238+ _c.Call.Run(func(args mock.Arguments) {
239+ var arg0 neferclient.SurfaceID
240+ if args[0] != nil {
241+ arg0 = args[0].(neferclient.SurfaceID)
242+ }
243+ run(
244+ arg0,
245+ )
246+ })
247+ return _c
248+}
249+
250+func (_c *MockHandler_FeedbackDone_Call) Return() *MockHandler_FeedbackDone_Call {
251+ _c.Call.Return()
252+ return _c
253+}
254+
255+func (_c *MockHandler_FeedbackDone_Call) RunAndReturn(run func(id neferclient.SurfaceID)) *MockHandler_FeedbackDone_Call {
256+ _c.Run(run)
257+ return _c
258+}
259+
260+// Frame provides a mock function for the type MockHandler
261+func (_mock *MockHandler) Frame(id neferclient.SurfaceID) {
262+ _mock.Called(id)
263+ return
264+}
265+
266+// MockHandler_Frame_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Frame'
267+type MockHandler_Frame_Call struct {
268+ *mock.Call
269+}
270+
271+// Frame is a helper method to define mock.On call
272+// - id neferclient.SurfaceID
273+func (_e *MockHandler_Expecter) Frame(id any) *MockHandler_Frame_Call {
274+ return &MockHandler_Frame_Call{Call: _e.mock.On("Frame", id)}
275+}
276+
277+func (_c *MockHandler_Frame_Call) Run(run func(id neferclient.SurfaceID)) *MockHandler_Frame_Call {
278+ _c.Call.Run(func(args mock.Arguments) {
279+ var arg0 neferclient.SurfaceID
280+ if args[0] != nil {
281+ arg0 = args[0].(neferclient.SurfaceID)
282+ }
283+ run(
284+ arg0,
285+ )
286+ })
287+ return _c
288+}
289+
290+func (_c *MockHandler_Frame_Call) Return() *MockHandler_Frame_Call {
291+ _c.Call.Return()
292+ return _c
293+}
294+
295+func (_c *MockHandler_Frame_Call) RunAndReturn(run func(id neferclient.SurfaceID)) *MockHandler_Frame_Call {
296+ _c.Run(run)
297+ return _c
298+}
299+
300+// LockFinished provides a mock function for the type MockHandler
301+func (_mock *MockHandler) LockFinished() {
302+ _mock.Called()
303+ return
304+}
305+
306+// MockHandler_LockFinished_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LockFinished'
307+type MockHandler_LockFinished_Call struct {
308+ *mock.Call
309+}
310+
311+// LockFinished is a helper method to define mock.On call
312+func (_e *MockHandler_Expecter) LockFinished() *MockHandler_LockFinished_Call {
313+ return &MockHandler_LockFinished_Call{Call: _e.mock.On("LockFinished")}
314+}
315+
316+func (_c *MockHandler_LockFinished_Call) Run(run func()) *MockHandler_LockFinished_Call {
317+ _c.Call.Run(func(args mock.Arguments) {
318+ run()
319+ })
320+ return _c
321+}
322+
323+func (_c *MockHandler_LockFinished_Call) Return() *MockHandler_LockFinished_Call {
324+ _c.Call.Return()
325+ return _c
326+}
327+
328+func (_c *MockHandler_LockFinished_Call) RunAndReturn(run func()) *MockHandler_LockFinished_Call {
329+ _c.Run(run)
330+ return _c
331+}
332+
333+// Locked provides a mock function for the type MockHandler
334+func (_mock *MockHandler) Locked() {
335+ _mock.Called()
336+ return
337+}
338+
339+// MockHandler_Locked_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Locked'
340+type MockHandler_Locked_Call struct {
341+ *mock.Call
342+}
343+
344+// Locked is a helper method to define mock.On call
345+func (_e *MockHandler_Expecter) Locked() *MockHandler_Locked_Call {
346+ return &MockHandler_Locked_Call{Call: _e.mock.On("Locked")}
347+}
348+
349+func (_c *MockHandler_Locked_Call) Run(run func()) *MockHandler_Locked_Call {
350+ _c.Call.Run(func(args mock.Arguments) {
351+ run()
352+ })
353+ return _c
354+}
355+
356+func (_c *MockHandler_Locked_Call) Return() *MockHandler_Locked_Call {
357+ _c.Call.Return()
358+ return _c
359+}
360+
361+func (_c *MockHandler_Locked_Call) RunAndReturn(run func()) *MockHandler_Locked_Call {
362+ _c.Run(run)
363+ return _c
364+}
365+
128366 // OutputAdded provides a mock function for the type MockHandler
129367 func (_mock *MockHandler) OutputAdded(out *neferclient.Output) {
130368 _mock.Called(out)
@@ -204,3 +442,49 @@ func (_c *MockHandler_OutputRemoved_Call) RunAndReturn(run func(global uint32))
204442 _c.Run(run)
205443 return _c
206444 }
445+
446+// Scale provides a mock function for the type MockHandler
447+func (_mock *MockHandler) Scale(id neferclient.SurfaceID, scale float64) {
448+ _mock.Called(id, scale)
449+ return
450+}
451+
452+// MockHandler_Scale_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Scale'
453+type MockHandler_Scale_Call struct {
454+ *mock.Call
455+}
456+
457+// Scale is a helper method to define mock.On call
458+// - id neferclient.SurfaceID
459+// - scale float64
460+func (_e *MockHandler_Expecter) Scale(id any, scale any) *MockHandler_Scale_Call {
461+ return &MockHandler_Scale_Call{Call: _e.mock.On("Scale", id, scale)}
462+}
463+
464+func (_c *MockHandler_Scale_Call) Run(run func(id neferclient.SurfaceID, scale float64)) *MockHandler_Scale_Call {
465+ _c.Call.Run(func(args mock.Arguments) {
466+ var arg0 neferclient.SurfaceID
467+ if args[0] != nil {
468+ arg0 = args[0].(neferclient.SurfaceID)
469+ }
470+ var arg1 float64
471+ if args[1] != nil {
472+ arg1 = args[1].(float64)
473+ }
474+ run(
475+ arg0,
476+ arg1,
477+ )
478+ })
479+ return _c
480+}
481+
482+func (_c *MockHandler_Scale_Call) Return() *MockHandler_Scale_Call {
483+ _c.Call.Return()
484+ return _c
485+}
486+
487+func (_c *MockHandler_Scale_Call) RunAndReturn(run func(id neferclient.SurfaceID, scale float64)) *MockHandler_Scale_Call {
488+ _c.Run(run)
489+ return _c
490+}
@@ -45,6 +45,98 @@ func (_m *MockHandler) EXPECT() *MockHandler_Expecter {
45 return &MockHandler_Expecter{mock: &_m.Mock}45 return &MockHandler_Expecter{mock: &_m.Mock}
46 }46 }
47 47
48+// Closed provides a mock function for the type MockHandler
49+func (_mock *MockHandler) Closed(id neferclient.SurfaceID) {
50+ _mock.Called(id)
51+ return
52+}
53+
54+// MockHandler_Closed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Closed'
55+type MockHandler_Closed_Call struct {
56+ *mock.Call
57+}
58+
59+// Closed is a helper method to define mock.On call
60+// - id neferclient.SurfaceID
61+func (_e *MockHandler_Expecter) Closed(id any) *MockHandler_Closed_Call {
62+ return &MockHandler_Closed_Call{Call: _e.mock.On("Closed", id)}
63+}
64+
65+func (_c *MockHandler_Closed_Call) Run(run func(id neferclient.SurfaceID)) *MockHandler_Closed_Call {
66+ _c.Call.Run(func(args mock.Arguments) {
67+ var arg0 neferclient.SurfaceID
68+ if args[0] != nil {
69+ arg0 = args[0].(neferclient.SurfaceID)
70+ }
71+ run(
72+ arg0,
73+ )
74+ })
75+ return _c
76+}
77+
78+func (_c *MockHandler_Closed_Call) Return() *MockHandler_Closed_Call {
79+ _c.Call.Return()
80+ return _c
81+}
82+
83+func (_c *MockHandler_Closed_Call) RunAndReturn(run func(id neferclient.SurfaceID)) *MockHandler_Closed_Call {
84+ _c.Run(run)
85+ return _c
86+}
87+
88+// Configure provides a mock function for the type MockHandler
89+func (_mock *MockHandler) Configure(id neferclient.SurfaceID, width int32, height int32) {
90+ _mock.Called(id, width, height)
91+ return
92+}
93+
94+// MockHandler_Configure_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Configure'
95+type MockHandler_Configure_Call struct {
96+ *mock.Call
97+}
98+
99+// Configure is a helper method to define mock.On call
100+// - id neferclient.SurfaceID
101+// - width int32
102+// - height int32
103+func (_e *MockHandler_Expecter) Configure(id any, width any, height any) *MockHandler_Configure_Call {
104+ return &MockHandler_Configure_Call{Call: _e.mock.On("Configure", id, width, height)}
105+}
106+
107+func (_c *MockHandler_Configure_Call) Run(run func(id neferclient.SurfaceID, width int32, height int32)) *MockHandler_Configure_Call {
108+ _c.Call.Run(func(args mock.Arguments) {
109+ var arg0 neferclient.SurfaceID
110+ if args[0] != nil {
111+ arg0 = args[0].(neferclient.SurfaceID)
112+ }
113+ var arg1 int32
114+ if args[1] != nil {
115+ arg1 = args[1].(int32)
116+ }
117+ var arg2 int32
118+ if args[2] != nil {
119+ arg2 = args[2].(int32)
120+ }
121+ run(
122+ arg0,
123+ arg1,
124+ arg2,
125+ )
126+ })
127+ return _c
128+}
129+
130+func (_c *MockHandler_Configure_Call) Return() *MockHandler_Configure_Call {
131+ _c.Call.Return()
132+ return _c
133+}
134+
135+func (_c *MockHandler_Configure_Call) RunAndReturn(run func(id neferclient.SurfaceID, width int32, height int32)) *MockHandler_Configure_Call {
136+ _c.Run(run)
137+ return _c
138+}
139+
48 // Error provides a mock function for the type MockHandler140 // Error provides a mock function for the type MockHandler
49 func (_mock *MockHandler) Error(err error) {141 func (_mock *MockHandler) Error(err error) {
50 _mock.Called(err)142 _mock.Called(err)
@@ -125,6 +217,152 @@ func (_c *MockHandler_FDReady_Call) RunAndReturn(run func(id uint64)) *MockHandl
125 return _c217 return _c
126 }218 }
127 219
220+// FeedbackDone provides a mock function for the type MockHandler
221+func (_mock *MockHandler) FeedbackDone(id neferclient.SurfaceID) {
222+ _mock.Called(id)
223+ return
224+}
225+
226+// MockHandler_FeedbackDone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeedbackDone'
227+type MockHandler_FeedbackDone_Call struct {
228+ *mock.Call
229+}
230+
231+// FeedbackDone is a helper method to define mock.On call
232+// - id neferclient.SurfaceID
233+func (_e *MockHandler_Expecter) FeedbackDone(id any) *MockHandler_FeedbackDone_Call {
234+ return &MockHandler_FeedbackDone_Call{Call: _e.mock.On("FeedbackDone", id)}
235+}
236+
237+func (_c *MockHandler_FeedbackDone_Call) Run(run func(id neferclient.SurfaceID)) *MockHandler_FeedbackDone_Call {
238+ _c.Call.Run(func(args mock.Arguments) {
239+ var arg0 neferclient.SurfaceID
240+ if args[0] != nil {
241+ arg0 = args[0].(neferclient.SurfaceID)
242+ }
243+ run(
244+ arg0,
245+ )
246+ })
247+ return _c
248+}
249+
250+func (_c *MockHandler_FeedbackDone_Call) Return() *MockHandler_FeedbackDone_Call {
251+ _c.Call.Return()
252+ return _c
253+}
254+
255+func (_c *MockHandler_FeedbackDone_Call) RunAndReturn(run func(id neferclient.SurfaceID)) *MockHandler_FeedbackDone_Call {
256+ _c.Run(run)
257+ return _c
258+}
259+
260+// Frame provides a mock function for the type MockHandler
261+func (_mock *MockHandler) Frame(id neferclient.SurfaceID) {
262+ _mock.Called(id)
263+ return
264+}
265+
266+// MockHandler_Frame_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Frame'
267+type MockHandler_Frame_Call struct {
268+ *mock.Call
269+}
270+
271+// Frame is a helper method to define mock.On call
272+// - id neferclient.SurfaceID
273+func (_e *MockHandler_Expecter) Frame(id any) *MockHandler_Frame_Call {
274+ return &MockHandler_Frame_Call{Call: _e.mock.On("Frame", id)}
275+}
276+
277+func (_c *MockHandler_Frame_Call) Run(run func(id neferclient.SurfaceID)) *MockHandler_Frame_Call {
278+ _c.Call.Run(func(args mock.Arguments) {
279+ var arg0 neferclient.SurfaceID
280+ if args[0] != nil {
281+ arg0 = args[0].(neferclient.SurfaceID)
282+ }
283+ run(
284+ arg0,
285+ )
286+ })
287+ return _c
288+}
289+
290+func (_c *MockHandler_Frame_Call) Return() *MockHandler_Frame_Call {
291+ _c.Call.Return()
292+ return _c
293+}
294+
295+func (_c *MockHandler_Frame_Call) RunAndReturn(run func(id neferclient.SurfaceID)) *MockHandler_Frame_Call {
296+ _c.Run(run)
297+ return _c
298+}
299+
300+// LockFinished provides a mock function for the type MockHandler
301+func (_mock *MockHandler) LockFinished() {
302+ _mock.Called()
303+ return
304+}
305+
306+// MockHandler_LockFinished_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LockFinished'
307+type MockHandler_LockFinished_Call struct {
308+ *mock.Call
309+}
310+
311+// LockFinished is a helper method to define mock.On call
312+func (_e *MockHandler_Expecter) LockFinished() *MockHandler_LockFinished_Call {
313+ return &MockHandler_LockFinished_Call{Call: _e.mock.On("LockFinished")}
314+}
315+
316+func (_c *MockHandler_LockFinished_Call) Run(run func()) *MockHandler_LockFinished_Call {
317+ _c.Call.Run(func(args mock.Arguments) {
318+ run()
319+ })
320+ return _c
321+}
322+
323+func (_c *MockHandler_LockFinished_Call) Return() *MockHandler_LockFinished_Call {
324+ _c.Call.Return()
325+ return _c
326+}
327+
328+func (_c *MockHandler_LockFinished_Call) RunAndReturn(run func()) *MockHandler_LockFinished_Call {
329+ _c.Run(run)
330+ return _c
331+}
332+
333+// Locked provides a mock function for the type MockHandler
334+func (_mock *MockHandler) Locked() {
335+ _mock.Called()
336+ return
337+}
338+
339+// MockHandler_Locked_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Locked'
340+type MockHandler_Locked_Call struct {
341+ *mock.Call
342+}
343+
344+// Locked is a helper method to define mock.On call
345+func (_e *MockHandler_Expecter) Locked() *MockHandler_Locked_Call {
346+ return &MockHandler_Locked_Call{Call: _e.mock.On("Locked")}
347+}
348+
349+func (_c *MockHandler_Locked_Call) Run(run func()) *MockHandler_Locked_Call {
350+ _c.Call.Run(func(args mock.Arguments) {
351+ run()
352+ })
353+ return _c
354+}
355+
356+func (_c *MockHandler_Locked_Call) Return() *MockHandler_Locked_Call {
357+ _c.Call.Return()
358+ return _c
359+}
360+
361+func (_c *MockHandler_Locked_Call) RunAndReturn(run func()) *MockHandler_Locked_Call {
362+ _c.Run(run)
363+ return _c
364+}
365+
128 // OutputAdded provides a mock function for the type MockHandler366 // OutputAdded provides a mock function for the type MockHandler
129 func (_mock *MockHandler) OutputAdded(out *neferclient.Output) {367 func (_mock *MockHandler) OutputAdded(out *neferclient.Output) {
130 _mock.Called(out)368 _mock.Called(out)
@@ -204,3 +442,49 @@ func (_c *MockHandler_OutputRemoved_Call) RunAndReturn(run func(global uint32))
204 _c.Run(run)442 _c.Run(run)
205 return _c443 return _c
206 }444 }
445+
446+// Scale provides a mock function for the type MockHandler
447+func (_mock *MockHandler) Scale(id neferclient.SurfaceID, scale float64) {
448+ _mock.Called(id, scale)
449+ return
450+}
451+
452+// MockHandler_Scale_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Scale'
453+type MockHandler_Scale_Call struct {
454+ *mock.Call
455+}
456+
457+// Scale is a helper method to define mock.On call
458+// - id neferclient.SurfaceID
459+// - scale float64
460+func (_e *MockHandler_Expecter) Scale(id any, scale any) *MockHandler_Scale_Call {
461+ return &MockHandler_Scale_Call{Call: _e.mock.On("Scale", id, scale)}
462+}
463+
464+func (_c *MockHandler_Scale_Call) Run(run func(id neferclient.SurfaceID, scale float64)) *MockHandler_Scale_Call {
465+ _c.Call.Run(func(args mock.Arguments) {
466+ var arg0 neferclient.SurfaceID
467+ if args[0] != nil {
468+ arg0 = args[0].(neferclient.SurfaceID)
469+ }
470+ var arg1 float64
471+ if args[1] != nil {
472+ arg1 = args[1].(float64)
473+ }
474+ run(
475+ arg0,
476+ arg1,
477+ )
478+ })
479+ return _c
480+}
481+
482+func (_c *MockHandler_Scale_Call) Return() *MockHandler_Scale_Call {
483+ _c.Call.Return()
484+ return _c
485+}
486+
487+func (_c *MockHandler_Scale_Call) RunAndReturn(run func(id neferclient.SurfaceID, scale float64)) *MockHandler_Scale_Call {
488+ _c.Run(run)
489+ return _c
490+}
A present.go +291 -0
@@ -0,0 +1,291 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/go-wayland-bindings/client/wayland"
8+ "github.com/bnema/wlturbo/wl"
9+ "golang.org/x/sys/unix"
10+)
11+
12+// importedBuffer is a wl_buffer with the size it was imported at.
13+type importedBuffer struct {
14+ buf *wayland.Buffer
15+ w, h int32
16+}
17+
18+// Plane is one DMA-BUF plane of a [Buffer]. FD stays owned by the caller.
19+type Plane struct {
20+ FD int
21+ Offset uint32
22+ Stride uint32
23+}
24+
25+// Buffer describes a DMA-BUF to import as a wl_buffer.
26+type Buffer struct {
27+ Width, Height int32 // physical pixels
28+ FourCC uint32
29+ Modifier uint64
30+ Planes [4]Plane
31+ PlaneCount int
32+}
33+
34+// Present describes one commit: which imported buffer, the explicit-sync
35+// timelines and points, and the damage in buffer pixels.
36+type Present struct {
37+ Buffer uint64
38+ AcquireTimeline uint64
39+ ReleaseTimeline uint64
40+ AcquirePoint uint64
41+ ReleasePoint uint64
42+ Damage []Rect // nil or empty damages the whole buffer
43+ Opaque bool // the whole surface is opaque
44+}
45+
46+// ImportBuffer creates a wl_buffer from b under the caller's id. The plane
47+// descriptors are duplicated internally; the caller keeps ownership of its own.
48+func (s *Surface) ImportBuffer(id uint64, b *Buffer) (err error) {
49+ if s.closed {
50+ return ErrClosed
51+ }
52+ if _, ok := s.buffers[id]; ok {
53+ return fmt.Errorf("neferclient: buffer %d already imported", id)
54+ }
55+ if b == nil || b.Width <= 0 || b.Height <= 0 || b.PlaneCount < 1 || b.PlaneCount > len(b.Planes) {
56+ return errors.New("neferclient: invalid buffer description")
57+ }
58+ dups := [4]int{-1, -1, -1, -1}
59+ sent := 0
60+ defer func() {
61+ for _, fd := range dups[sent:b.PlaneCount] { // not consumed by a request
62+ if fd >= 0 {
63+ _ = unix.Close(fd)
64+ }
65+ }
66+ }()
67+ for i := range b.PlaneCount {
68+ fd, err := unix.FcntlInt(uintptr(b.Planes[i].FD), unix.F_DUPFD_CLOEXEC, 0)
69+ if err != nil {
70+ return fmt.Errorf("neferclient: dup plane %d: %w", i, err)
71+ }
72+ dups[i] = fd
73+ }
74+ params, err := s.c.g.dmabuf.CreateParams()
75+ if err != nil {
76+ return err
77+ }
78+ defer func() { err = errors.Join(err, params.Destroy()) }()
79+ hi, lo := uint32(b.Modifier>>32), uint32(b.Modifier)
80+ for i := range b.PlaneCount {
81+ // On success the request closes the duplicate; on failure it stays ours.
82+ if err = params.Add(dups[i], uint32(i), b.Planes[i].Offset, b.Planes[i].Stride, hi, lo); err != nil {
83+ return err
84+ }
85+ sent++
86+ }
87+ buf, err := params.CreateImmed(b.Width, b.Height, b.FourCC, 0)
88+ if err != nil {
89+ return err
90+ }
91+ s.buffers[id] = importedBuffer{buf: buf, w: b.Width, h: b.Height}
92+ return nil
93+}
94+
95+// DestroyBuffer destroys an imported wl_buffer. The compositor may still read
96+// it until its release point signals; see the explicit-sync rules.
97+func (s *Surface) DestroyBuffer(id uint64) error {
98+ buf, ok := s.buffers[id]
99+ if !ok {
100+ return fmt.Errorf("neferclient: unknown buffer %d", id)
101+ }
102+ delete(s.buffers, id)
103+ return buf.buf.Destroy()
104+}
105+
106+// ImportTimeline imports a DRM syncobj timeline. fd is duplicated internally.
107+func (s *Surface) ImportTimeline(id uint64, fd int) error {
108+ if s.closed {
109+ return ErrClosed
110+ }
111+ if _, ok := s.timelines[id]; ok {
112+ return fmt.Errorf("neferclient: timeline %d already imported", id)
113+ }
114+ dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
115+ if err != nil {
116+ return fmt.Errorf("neferclient: dup timeline: %w", err)
117+ }
118+ t, err := s.c.g.syncobj.ImportTimeline(dup)
119+ if err != nil {
120+ _ = unix.Close(dup) // not consumed on error
121+ return err
122+ }
123+ s.timelines[id] = t
124+ return nil
125+}
126+
127+// Present commits an imported buffer. It refuses before the first configure,
128+// before the first complete dmabuf feedback and while the previous frame
129+// callback is pending.
130+func (s *Surface) Present(p *Present) error {
131+ if s.closed {
132+ return ErrClosed
133+ }
134+ if p == nil {
135+ return errors.New("neferclient: nil present")
136+ }
137+ if !s.configured {
138+ return errors.New("neferclient: present before configure")
139+ }
140+ if !s.fbDone {
141+ return errors.New("neferclient: dmabuf feedback incomplete")
142+ }
143+ if !s.frameReady {
144+ return errors.New("neferclient: frame callback pending")
145+ }
146+ buf, ok := s.buffers[p.Buffer]
147+ if !ok {
148+ return fmt.Errorf("neferclient: unknown buffer %d", p.Buffer)
149+ }
150+ if p.AcquireTimeline == p.ReleaseTimeline && p.AcquirePoint >= p.ReleasePoint {
151+ return fmt.Errorf("neferclient: acquire point %d must precede release point %d on one timeline", p.AcquirePoint, p.ReleasePoint)
152+ }
153+ for _, id := range [...]uint64{p.AcquireTimeline, p.ReleaseTimeline} {
154+ if _, ok := s.timelines[id]; !ok {
155+ return fmt.Errorf("neferclient: unknown timeline %d", id)
156+ }
157+ }
158+ pw, ph, err := s.PhysicalSize()
159+ if err != nil {
160+ return err
161+ }
162+ // Without a viewport (and always on lock surfaces) the compositor
163+ // requires the buffer to match the surface exactly.
164+ if (s.viewport == nil || s.role == roleLock) && (buf.w != pw || buf.h != ph) {
165+ return fmt.Errorf("neferclient: buffer %dx%d does not match surface %dx%d", buf.w, buf.h, pw, ph)
166+ }
167+ r := s.req
168+ if s.viewport != nil {
169+ if s.sentDestW != s.width || s.sentDestH != s.height {
170+ if err = r.setDestination(s.width, s.height); err != nil {
171+ return err
172+ }
173+ s.sentDestW, s.sentDestH = s.width, s.height
174+ }
175+ } else if s.sentBufScale != int32(s.scale) {
176+ if err = r.setBufferScale(int32(s.scale)); err != nil {
177+ return err
178+ }
179+ s.sentBufScale = int32(s.scale)
180+ }
181+ if p.Opaque {
182+ if !s.opaqueSet || s.opaqueW != s.width || s.opaqueH != s.height {
183+ if err = r.setOpaqueRegion(s.width, s.height, true); err != nil {
184+ return err
185+ }
186+ s.opaqueSet, s.opaqueW, s.opaqueH = true, s.width, s.height
187+ }
188+ } else if s.opaqueSet {
189+ if err = r.setOpaqueRegion(0, 0, false); err != nil {
190+ return err
191+ }
192+ s.opaqueSet = false
193+ }
194+ if err = r.setAcquire(p.AcquireTimeline, p.AcquirePoint); err != nil {
195+ return err
196+ }
197+ if err = r.setRelease(p.ReleaseTimeline, p.ReleasePoint); err != nil {
198+ return err
199+ }
200+ if err = r.attach(p.Buffer); err != nil {
201+ return err
202+ }
203+ if len(p.Damage) == 0 {
204+ err = r.damageBuffer(Rect{0, 0, pw, ph})
205+ }
206+ for _, d := range p.Damage {
207+ if err != nil {
208+ break
209+ }
210+ err = r.damageBuffer(d)
211+ }
212+ if err != nil {
213+ return err
214+ }
215+ if err = r.frame(); err != nil {
216+ return err
217+ }
218+ if err = r.commit(); err != nil {
219+ return err
220+ }
221+ s.frameReady, s.presented = false, true
222+ return nil
223+}
224+
225+// surfaceRequests is the seam between the Present ordering rules and the
226+// wire: the production implementation sends the protocol requests.
227+type surfaceRequests interface {
228+ setDestination(w, h int32) error
229+ setBufferScale(n int32) error
230+ setOpaqueRegion(w, h int32, opaque bool) error
231+ setAcquire(timeline, point uint64) error
232+ setRelease(timeline, point uint64) error
233+ attach(buffer uint64) error
234+ damageBuffer(r Rect) error
235+ frame() error
236+ commit() error
237+}
238+
239+// wireRequests sends the requests. skipFrame is set by tests that isolate the
240+// request path from the frame callback registration.
241+type wireRequests struct {
242+ s *Surface
243+ skipFrame bool
244+}
245+
246+func (r wireRequests) setDestination(w, h int32) error { return r.s.viewport.SetDestination(w, h) }
247+func (r wireRequests) setBufferScale(n int32) error { return r.s.surf.SetBufferScale(n) }
248+
249+func (r wireRequests) setOpaqueRegion(w, h int32, opaque bool) error {
250+ s := r.s
251+ if !opaque {
252+ return s.surf.SetOpaqueRegion(nil)
253+ }
254+ region, err := s.c.g.compositor.CreateRegion()
255+ if err != nil {
256+ return err
257+ }
258+ if err = region.Add(0, 0, w, h); err == nil {
259+ err = s.surf.SetOpaqueRegion(region)
260+ }
261+ _ = region.Destroy()
262+ return err
263+}
264+
265+func (r wireRequests) setAcquire(timeline, point uint64) error {
266+ return r.s.sync.SetAcquirePoint(r.s.timelines[timeline], uint32(point>>32), uint32(point))
267+}
268+
269+func (r wireRequests) setRelease(timeline, point uint64) error {
270+ return r.s.sync.SetReleasePoint(r.s.timelines[timeline], uint32(point>>32), uint32(point))
271+}
272+
273+func (r wireRequests) attach(buffer uint64) error {
274+ return r.s.surf.Attach(r.s.buffers[buffer].buf, 0, 0)
275+}
276+
277+func (r wireRequests) damageBuffer(d Rect) error {
278+ return r.s.surf.DamageBuffer(d.X, d.Y, d.Width, d.Height)
279+}
280+
281+// frame requests the frame callback on the surface's single reusable
282+// callback object: only one is ever pending.
283+func (r wireRequests) frame() error {
284+ s := r.s
285+ if r.skipFrame {
286+ return nil
287+ }
288+ return s.c.wlctx.RequestArgs(wl.Request{Proxy: s.surf, Opcode: 3, Name: "wl_surface.frame", Child: s.cb}, wl.ArgObject(s.cb))
289+}
290+
291+func (r wireRequests) commit() error { return r.s.surf.Commit() }
@@ -0,0 +1,291 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/go-wayland-bindings/client/wayland"
8+ "github.com/bnema/wlturbo/wl"
9+ "golang.org/x/sys/unix"
10+)
11+
12+// importedBuffer is a wl_buffer with the size it was imported at.
13+type importedBuffer struct {
14+ buf *wayland.Buffer
15+ w, h int32
16+}
17+
18+// Plane is one DMA-BUF plane of a [Buffer]. FD stays owned by the caller.
19+type Plane struct {
20+ FD int
21+ Offset uint32
22+ Stride uint32
23+}
24+
25+// Buffer describes a DMA-BUF to import as a wl_buffer.
26+type Buffer struct {
27+ Width, Height int32 // physical pixels
28+ FourCC uint32
29+ Modifier uint64
30+ Planes [4]Plane
31+ PlaneCount int
32+}
33+
34+// Present describes one commit: which imported buffer, the explicit-sync
35+// timelines and points, and the damage in buffer pixels.
36+type Present struct {
37+ Buffer uint64
38+ AcquireTimeline uint64
39+ ReleaseTimeline uint64
40+ AcquirePoint uint64
41+ ReleasePoint uint64
42+ Damage []Rect // nil or empty damages the whole buffer
43+ Opaque bool // the whole surface is opaque
44+}
45+
46+// ImportBuffer creates a wl_buffer from b under the caller's id. The plane
47+// descriptors are duplicated internally; the caller keeps ownership of its own.
48+func (s *Surface) ImportBuffer(id uint64, b *Buffer) (err error) {
49+ if s.closed {
50+ return ErrClosed
51+ }
52+ if _, ok := s.buffers[id]; ok {
53+ return fmt.Errorf("neferclient: buffer %d already imported", id)
54+ }
55+ if b == nil || b.Width <= 0 || b.Height <= 0 || b.PlaneCount < 1 || b.PlaneCount > len(b.Planes) {
56+ return errors.New("neferclient: invalid buffer description")
57+ }
58+ dups := [4]int{-1, -1, -1, -1}
59+ sent := 0
60+ defer func() {
61+ for _, fd := range dups[sent:b.PlaneCount] { // not consumed by a request
62+ if fd >= 0 {
63+ _ = unix.Close(fd)
64+ }
65+ }
66+ }()
67+ for i := range b.PlaneCount {
68+ fd, err := unix.FcntlInt(uintptr(b.Planes[i].FD), unix.F_DUPFD_CLOEXEC, 0)
69+ if err != nil {
70+ return fmt.Errorf("neferclient: dup plane %d: %w", i, err)
71+ }
72+ dups[i] = fd
73+ }
74+ params, err := s.c.g.dmabuf.CreateParams()
75+ if err != nil {
76+ return err
77+ }
78+ defer func() { err = errors.Join(err, params.Destroy()) }()
79+ hi, lo := uint32(b.Modifier>>32), uint32(b.Modifier)
80+ for i := range b.PlaneCount {
81+ // On success the request closes the duplicate; on failure it stays ours.
82+ if err = params.Add(dups[i], uint32(i), b.Planes[i].Offset, b.Planes[i].Stride, hi, lo); err != nil {
83+ return err
84+ }
85+ sent++
86+ }
87+ buf, err := params.CreateImmed(b.Width, b.Height, b.FourCC, 0)
88+ if err != nil {
89+ return err
90+ }
91+ s.buffers[id] = importedBuffer{buf: buf, w: b.Width, h: b.Height}
92+ return nil
93+}
94+
95+// DestroyBuffer destroys an imported wl_buffer. The compositor may still read
96+// it until its release point signals; see the explicit-sync rules.
97+func (s *Surface) DestroyBuffer(id uint64) error {
98+ buf, ok := s.buffers[id]
99+ if !ok {
100+ return fmt.Errorf("neferclient: unknown buffer %d", id)
101+ }
102+ delete(s.buffers, id)
103+ return buf.buf.Destroy()
104+}
105+
106+// ImportTimeline imports a DRM syncobj timeline. fd is duplicated internally.
107+func (s *Surface) ImportTimeline(id uint64, fd int) error {
108+ if s.closed {
109+ return ErrClosed
110+ }
111+ if _, ok := s.timelines[id]; ok {
112+ return fmt.Errorf("neferclient: timeline %d already imported", id)
113+ }
114+ dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
115+ if err != nil {
116+ return fmt.Errorf("neferclient: dup timeline: %w", err)
117+ }
118+ t, err := s.c.g.syncobj.ImportTimeline(dup)
119+ if err != nil {
120+ _ = unix.Close(dup) // not consumed on error
121+ return err
122+ }
123+ s.timelines[id] = t
124+ return nil
125+}
126+
127+// Present commits an imported buffer. It refuses before the first configure,
128+// before the first complete dmabuf feedback and while the previous frame
129+// callback is pending.
130+func (s *Surface) Present(p *Present) error {
131+ if s.closed {
132+ return ErrClosed
133+ }
134+ if p == nil {
135+ return errors.New("neferclient: nil present")
136+ }
137+ if !s.configured {
138+ return errors.New("neferclient: present before configure")
139+ }
140+ if !s.fbDone {
141+ return errors.New("neferclient: dmabuf feedback incomplete")
142+ }
143+ if !s.frameReady {
144+ return errors.New("neferclient: frame callback pending")
145+ }
146+ buf, ok := s.buffers[p.Buffer]
147+ if !ok {
148+ return fmt.Errorf("neferclient: unknown buffer %d", p.Buffer)
149+ }
150+ if p.AcquireTimeline == p.ReleaseTimeline && p.AcquirePoint >= p.ReleasePoint {
151+ return fmt.Errorf("neferclient: acquire point %d must precede release point %d on one timeline", p.AcquirePoint, p.ReleasePoint)
152+ }
153+ for _, id := range [...]uint64{p.AcquireTimeline, p.ReleaseTimeline} {
154+ if _, ok := s.timelines[id]; !ok {
155+ return fmt.Errorf("neferclient: unknown timeline %d", id)
156+ }
157+ }
158+ pw, ph, err := s.PhysicalSize()
159+ if err != nil {
160+ return err
161+ }
162+ // Without a viewport (and always on lock surfaces) the compositor
163+ // requires the buffer to match the surface exactly.
164+ if (s.viewport == nil || s.role == roleLock) && (buf.w != pw || buf.h != ph) {
165+ return fmt.Errorf("neferclient: buffer %dx%d does not match surface %dx%d", buf.w, buf.h, pw, ph)
166+ }
167+ r := s.req
168+ if s.viewport != nil {
169+ if s.sentDestW != s.width || s.sentDestH != s.height {
170+ if err = r.setDestination(s.width, s.height); err != nil {
171+ return err
172+ }
173+ s.sentDestW, s.sentDestH = s.width, s.height
174+ }
175+ } else if s.sentBufScale != int32(s.scale) {
176+ if err = r.setBufferScale(int32(s.scale)); err != nil {
177+ return err
178+ }
179+ s.sentBufScale = int32(s.scale)
180+ }
181+ if p.Opaque {
182+ if !s.opaqueSet || s.opaqueW != s.width || s.opaqueH != s.height {
183+ if err = r.setOpaqueRegion(s.width, s.height, true); err != nil {
184+ return err
185+ }
186+ s.opaqueSet, s.opaqueW, s.opaqueH = true, s.width, s.height
187+ }
188+ } else if s.opaqueSet {
189+ if err = r.setOpaqueRegion(0, 0, false); err != nil {
190+ return err
191+ }
192+ s.opaqueSet = false
193+ }
194+ if err = r.setAcquire(p.AcquireTimeline, p.AcquirePoint); err != nil {
195+ return err
196+ }
197+ if err = r.setRelease(p.ReleaseTimeline, p.ReleasePoint); err != nil {
198+ return err
199+ }
200+ if err = r.attach(p.Buffer); err != nil {
201+ return err
202+ }
203+ if len(p.Damage) == 0 {
204+ err = r.damageBuffer(Rect{0, 0, pw, ph})
205+ }
206+ for _, d := range p.Damage {
207+ if err != nil {
208+ break
209+ }
210+ err = r.damageBuffer(d)
211+ }
212+ if err != nil {
213+ return err
214+ }
215+ if err = r.frame(); err != nil {
216+ return err
217+ }
218+ if err = r.commit(); err != nil {
219+ return err
220+ }
221+ s.frameReady, s.presented = false, true
222+ return nil
223+}
224+
225+// surfaceRequests is the seam between the Present ordering rules and the
226+// wire: the production implementation sends the protocol requests.
227+type surfaceRequests interface {
228+ setDestination(w, h int32) error
229+ setBufferScale(n int32) error
230+ setOpaqueRegion(w, h int32, opaque bool) error
231+ setAcquire(timeline, point uint64) error
232+ setRelease(timeline, point uint64) error
233+ attach(buffer uint64) error
234+ damageBuffer(r Rect) error
235+ frame() error
236+ commit() error
237+}
238+
239+// wireRequests sends the requests. skipFrame is set by tests that isolate the
240+// request path from the frame callback registration.
241+type wireRequests struct {
242+ s *Surface
243+ skipFrame bool
244+}
245+
246+func (r wireRequests) setDestination(w, h int32) error { return r.s.viewport.SetDestination(w, h) }
247+func (r wireRequests) setBufferScale(n int32) error { return r.s.surf.SetBufferScale(n) }
248+
249+func (r wireRequests) setOpaqueRegion(w, h int32, opaque bool) error {
250+ s := r.s
251+ if !opaque {
252+ return s.surf.SetOpaqueRegion(nil)
253+ }
254+ region, err := s.c.g.compositor.CreateRegion()
255+ if err != nil {
256+ return err
257+ }
258+ if err = region.Add(0, 0, w, h); err == nil {
259+ err = s.surf.SetOpaqueRegion(region)
260+ }
261+ _ = region.Destroy()
262+ return err
263+}
264+
265+func (r wireRequests) setAcquire(timeline, point uint64) error {
266+ return r.s.sync.SetAcquirePoint(r.s.timelines[timeline], uint32(point>>32), uint32(point))
267+}
268+
269+func (r wireRequests) setRelease(timeline, point uint64) error {
270+ return r.s.sync.SetReleasePoint(r.s.timelines[timeline], uint32(point>>32), uint32(point))
271+}
272+
273+func (r wireRequests) attach(buffer uint64) error {
274+ return r.s.surf.Attach(r.s.buffers[buffer].buf, 0, 0)
275+}
276+
277+func (r wireRequests) damageBuffer(d Rect) error {
278+ return r.s.surf.DamageBuffer(d.X, d.Y, d.Width, d.Height)
279+}
280+
281+// frame requests the frame callback on the surface's single reusable
282+// callback object: only one is ever pending.
283+func (r wireRequests) frame() error {
284+ s := r.s
285+ if r.skipFrame {
286+ return nil
287+ }
288+ return s.c.wlctx.RequestArgs(wl.Request{Proxy: s.surf, Opcode: 3, Name: "wl_surface.frame", Child: s.cb}, wl.ArgObject(s.cb))
289+}
290+
291+func (r wireRequests) commit() error { return r.s.surf.Commit() }
M queue.go +72 -1
@@ -2,6 +2,8 @@ package neferclient
22
33 import (
44 "sync"
5+
6+ "golang.org/x/sys/unix"
57 )
68
79 const (
@@ -19,6 +21,24 @@ const (
1921 evOutputMode // flags, a = width, b = height, global
2022 evOutputDone // global
2123 evFDReady // fd (epoll data)
24+
25+ // Surface events. id is the wl_surface object id (SurfaceID).
26+ evPing // serial (xdg_wm_base)
27+ evXdgToplevel // id, a = width, b = height (before the matching evXdgConfigure)
28+ evXdgConfigure // id, serial
29+ evLayerConfigure // id, serial, a = width, b = height
30+ evLockConfigure // id, serial, a = width, b = height
31+ evClosed // id
32+ evFrame // id
33+ evPreferredScale // id, a = fractional scale in 120ths
34+ evBufferScale // id, a = integer factor
35+ evFeedbackTable // id, fd = owned descriptor, flags = size
36+ evFeedbackMainDevice // id, dev
37+ evFeedbackFormats // id, name = one chunk of tranche format indices
38+ evFeedbackDone // id
39+ evLocked // id = lock generation
40+ evLockFinished // id = lock generation
41+ evBadEvent // id, a = badEvent code: the reader met a malformed event
2242 )
2343
2444 // event is the only thing the reader and the epoll goroutine hand to the
@@ -30,11 +50,29 @@ type event struct {
3050 global uint32
3151 version uint32
3252 flags uint32
33- fd int32
53+ id uint32 // wl_surface object id for surface events
54+ serial uint32
55+ fd int32 // epoll data, or a received descriptor owned by the event
3456 a, b int32
57+ dev uint64
3558 name [maxNameLen]byte
3659 }
3760
61+// hasFD reports whether the event owns a received descriptor that must be
62+// closed if the event is dropped.
63+func (e *event) hasFD() bool { return e.kind == evFeedbackTable && e.fd >= 0 }
64+
65+func (e *event) closeFD() {
66+ if e.hasFD() {
67+ _ = unix.Close(int(e.fd))
68+ e.fd = -1
69+ }
70+}
71+
72+func (e *event) setBytes(b []byte) {
73+ e.nameLen = uint8(copy(e.name[:], b))
74+}
75+
3876 func (e *event) setName(s string) {
3977 e.nameLen = uint8(copy(e.name[:], s))
4078 }
@@ -87,6 +125,7 @@ func (q *queue) post(ev *event) {
87125 switch {
88126 case q.stopped:
89127 q.mu.Unlock()
128+ ev.closeFD()
90129 return
91130 case q.pausing:
92131 q.held = append(q.held, *ev)
@@ -111,6 +150,7 @@ func (q *queue) pop(ev *event) bool {
111150 return false
112151 }
113152 *ev = q.ring[q.head]
153+ q.ring[q.head].kind = 0 // ownership of a carried descriptor moved to ev
114154 q.head = (q.head + 1) % ringSize
115155 full := q.n == ringSize
116156 q.n--
@@ -240,6 +280,37 @@ func (q *queue) takeHeld() []event {
240280 return held
241281 }
242282
283+// hasLocked reports whether an evLocked for lock generation gen is queued or
284+// parked.
285+func (q *queue) hasLocked(gen uint32) bool {
286+ q.mu.Lock()
287+ defer q.mu.Unlock()
288+ for i := range q.n {
289+ if e := &q.ring[(q.head+i)%ringSize]; e.kind == evLocked && e.id == gen {
290+ return true
291+ }
292+ }
293+ for i := range q.held {
294+ if q.held[i].kind == evLocked && q.held[i].id == gen {
295+ return true
296+ }
297+ }
298+ return false
299+}
300+
301+// closeFDs closes the descriptors owned by events nobody will drain. Only
302+// valid once both producers have stopped.
303+func (q *queue) closeFDs() {
304+ q.mu.Lock()
305+ for i := range q.n {
306+ q.ring[(q.head+i)%ringSize].closeFD()
307+ }
308+ for i := range q.held {
309+ q.held[i].closeFD()
310+ }
311+ q.mu.Unlock()
312+}
313+
243314 // length reports how many events are queued.
244315 func (q *queue) length() int {
245316 q.mu.Lock()
@@ -2,6 +2,8 @@ package neferclient
2 2
3 import (3 import (
4 "sync"4 "sync"
5+
6+ "golang.org/x/sys/unix"
5 )7 )
6 8
7 const (9 const (
@@ -19,6 +21,24 @@ const (
19 evOutputMode // flags, a = width, b = height, global21 evOutputMode // flags, a = width, b = height, global
20 evOutputDone // global22 evOutputDone // global
21 evFDReady // fd (epoll data)23 evFDReady // fd (epoll data)
24+
25+ // Surface events. id is the wl_surface object id (SurfaceID).
26+ evPing // serial (xdg_wm_base)
27+ evXdgToplevel // id, a = width, b = height (before the matching evXdgConfigure)
28+ evXdgConfigure // id, serial
29+ evLayerConfigure // id, serial, a = width, b = height
30+ evLockConfigure // id, serial, a = width, b = height
31+ evClosed // id
32+ evFrame // id
33+ evPreferredScale // id, a = fractional scale in 120ths
34+ evBufferScale // id, a = integer factor
35+ evFeedbackTable // id, fd = owned descriptor, flags = size
36+ evFeedbackMainDevice // id, dev
37+ evFeedbackFormats // id, name = one chunk of tranche format indices
38+ evFeedbackDone // id
39+ evLocked // id = lock generation
40+ evLockFinished // id = lock generation
41+ evBadEvent // id, a = badEvent code: the reader met a malformed event
22 )42 )
23 43
24 // event is the only thing the reader and the epoll goroutine hand to the44 // event is the only thing the reader and the epoll goroutine hand to the
@@ -30,11 +50,29 @@ type event struct {
30 global uint3250 global uint32
31 version uint3251 version uint32
32 flags uint3252 flags uint32
33- fd int3253+ id uint32 // wl_surface object id for surface events
54+ serial uint32
55+ fd int32 // epoll data, or a received descriptor owned by the event
34 a, b int3256 a, b int32
57+ dev uint64
35 name [maxNameLen]byte58 name [maxNameLen]byte
36 }59 }
37 60
61+// hasFD reports whether the event owns a received descriptor that must be
62+// closed if the event is dropped.
63+func (e *event) hasFD() bool { return e.kind == evFeedbackTable && e.fd >= 0 }
64+
65+func (e *event) closeFD() {
66+ if e.hasFD() {
67+ _ = unix.Close(int(e.fd))
68+ e.fd = -1
69+ }
70+}
71+
72+func (e *event) setBytes(b []byte) {
73+ e.nameLen = uint8(copy(e.name[:], b))
74+}
75+
38 func (e *event) setName(s string) {76 func (e *event) setName(s string) {
39 e.nameLen = uint8(copy(e.name[:], s))77 e.nameLen = uint8(copy(e.name[:], s))
40 }78 }
@@ -87,6 +125,7 @@ func (q *queue) post(ev *event) {
87 switch {125 switch {
88 case q.stopped:126 case q.stopped:
89 q.mu.Unlock()127 q.mu.Unlock()
128+ ev.closeFD()
90 return129 return
91 case q.pausing:130 case q.pausing:
92 q.held = append(q.held, *ev)131 q.held = append(q.held, *ev)
@@ -111,6 +150,7 @@ func (q *queue) pop(ev *event) bool {
111 return false150 return false
112 }151 }
113 *ev = q.ring[q.head]152 *ev = q.ring[q.head]
153+ q.ring[q.head].kind = 0 // ownership of a carried descriptor moved to ev
114 q.head = (q.head + 1) % ringSize154 q.head = (q.head + 1) % ringSize
115 full := q.n == ringSize155 full := q.n == ringSize
116 q.n--156 q.n--
@@ -240,6 +280,37 @@ func (q *queue) takeHeld() []event {
240 return held280 return held
241 }281 }
242 282
283+// hasLocked reports whether an evLocked for lock generation gen is queued or
284+// parked.
285+func (q *queue) hasLocked(gen uint32) bool {
286+ q.mu.Lock()
287+ defer q.mu.Unlock()
288+ for i := range q.n {
289+ if e := &q.ring[(q.head+i)%ringSize]; e.kind == evLocked && e.id == gen {
290+ return true
291+ }
292+ }
293+ for i := range q.held {
294+ if q.held[i].kind == evLocked && q.held[i].id == gen {
295+ return true
296+ }
297+ }
298+ return false
299+}
300+
301+// closeFDs closes the descriptors owned by events nobody will drain. Only
302+// valid once both producers have stopped.
303+func (q *queue) closeFDs() {
304+ q.mu.Lock()
305+ for i := range q.n {
306+ q.ring[(q.head+i)%ringSize].closeFD()
307+ }
308+ for i := range q.held {
309+ q.held[i].closeFD()
310+ }
311+ q.mu.Unlock()
312+}
313+
243 // length reports how many events are queued.314 // length reports how many events are queued.
244 func (q *queue) length() int {315 func (q *queue) length() int {
245 q.mu.Lock()316 q.mu.Lock()
A surface.go +776 -0
@@ -0,0 +1,776 @@
1+package neferclient
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "fmt"
7+ "math"
8+ "slices"
9+ "strings"
10+
11+ "github.com/bnema/go-wayland-bindings/client/extsessionlock"
12+ "github.com/bnema/go-wayland-bindings/client/fractionalscale"
13+ "github.com/bnema/go-wayland-bindings/client/linuxdmabuf"
14+ "github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj"
15+ "github.com/bnema/go-wayland-bindings/client/viewporter"
16+ "github.com/bnema/go-wayland-bindings/client/wayland"
17+ "github.com/bnema/go-wayland-bindings/client/wlrlayershell"
18+ "github.com/bnema/go-wayland-bindings/client/xdgshell"
19+ "github.com/bnema/wlturbo/wl"
20+ "golang.org/x/sys/unix"
21+)
22+
23+// LayerLevel is the wlr-layer-shell stacking layer. The zero value is invalid.
24+type LayerLevel uint8
25+
26+const (
27+ LayerBackground LayerLevel = iota + 1
28+ LayerBottom
29+ LayerTop
30+ LayerOverlay
31+)
32+
33+// Anchor is a bit set of output edges a layer surface attaches to.
34+type Anchor uint8
35+
36+const (
37+ AnchorTop Anchor = 1 << iota
38+ AnchorBottom
39+ AnchorLeft
40+ AnchorRight
41+)
42+
43+const anchorAll = AnchorTop | AnchorBottom | AnchorLeft | AnchorRight
44+
45+// KeyboardMode is the layer surface keyboard interactivity. The zero value is
46+// KeyboardNone. KeyboardOnDemand needs zwlr_layer_shell_v1 version 4.
47+type KeyboardMode uint8
48+
49+const (
50+ KeyboardNone KeyboardMode = iota
51+ KeyboardExclusive
52+ KeyboardOnDemand
53+)
54+
55+// maxLayerDimension bounds a compositor-assigned layer surface extent.
56+const maxLayerDimension = 16384
57+
58+// DefaultLayerNamespace is used when [LayerConfig.Namespace] is empty.
59+const DefaultLayerNamespace = "neferclient"
60+
61+// LayerConfig requests a wlr-layer-shell surface. Width and Height give the
62+// initial logical size; an axis anchored to both opposite edges is sized by
63+// the compositor and reported through Handler.Configure.
64+type LayerConfig struct {
65+ Output string // wl_output name; empty lets the compositor choose
66+ Namespace string // defaults to [DefaultLayerNamespace]
67+ Level LayerLevel
68+ Anchors Anchor
69+ Keyboard KeyboardMode
70+ ExclusiveZone int32 // >0 reserves, 0 neutral, -1 ignores other zones
71+ Margin [4]int32 // top, right, bottom, left
72+ // InputRects, when non-nil, restricts pointer input to these rectangles
73+ // (logical pixels). An empty non-nil slice makes the surface click-through.
74+ InputRects []Rect
75+ // Width and Height are the requested logical size. They are required on
76+ // every axis that is not anchored to both opposite edges.
77+ Width, Height int32
78+}
79+
80+type surfaceRole uint8
81+
82+const (
83+ roleToplevel surfaceRole = iota + 1
84+ roleLayer
85+ roleLock
86+)
87+
88+// Surface is one wl_surface with its role and the DMA-BUF presentation
89+// objects. Every method belongs to the owner goroutine.
90+type Surface struct {
91+ c *Conn
92+ id SurfaceID
93+ role surfaceRole
94+
95+ surf *wayland.Surface
96+ viewport *viewporter.WpViewport
97+ frac *fractionalscale.WpFractionalScale
98+ sync *linuxdrmsyncobj.WpLinuxDrmSyncobjSurface
99+ feedback *linuxdmabuf.LinuxDmabufFeedback
100+ xdg *xdgshell.XdgSurface
101+ toplevel *xdgshell.XdgToplevel
102+ layer *wlrlayershell.LayerSurface
103+ lockSurf *extsessionlock.ExtSessionLockSurface
104+ lock *Lock
105+
106+ width, height int32
107+ scale float64
108+ pendW, pendH int32 // toplevel size announced before the xdg configure
109+ configured bool
110+ frameReady bool
111+ closed bool
112+
113+ // destination/opaque state last sent; they are double-buffered surface
114+ // state that persists across commits, so unchanged values are not resent.
115+ sentDestW, sentDestH int32
116+ sentBufScale int32 // starts at 1, the protocol default
117+ presented bool // a Present committed: the surface is mapped
118+ output uint32 // lock surfaces: the covered wl_output global
119+ opaqueSet bool
120+ opaqueW, opaqueH int32
121+
122+ cb *frameCallback
123+ req surfaceRequests
124+
125+ inputRects []Rect
126+ inputCustom bool
127+
128+ buffers map[uint64]importedBuffer
129+ timelines map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline
130+
131+ fb Feedback
132+ table []Format // current format table
133+ pending Feedback // round in progress; Formats reused across rounds
134+ fbDone bool
135+}
136+
137+// frameCallback is a reusable wl_callback: the library keeps a single frame
138+// callback pending per surface, so one proxy object serves all of them.
139+type frameCallback struct {
140+ wl.BaseProxy
141+ q *queue
142+ id SurfaceID
143+}
144+
145+func (f *frameCallback) EventSignature(op uint16) (string, bool) {
146+ if op == 0 {
147+ return "uint,", true
148+ }
149+ return "", false
150+}
151+
152+func (f *frameCallback) Dispatch(ev *wl.Event) {
153+ if ev.Opcode != 0 {
154+ return
155+ }
156+ _ = ev.Uint32()
157+ f.Context().Unregister(f)
158+ e := event{kind: evFrame, id: uint32(f.id)}
159+ f.q.post(&e)
160+}
161+
162+// ID returns the surface's identifier used in Handler callbacks.
163+func (s *Surface) ID() SurfaceID { return s.id }
164+
165+// Size returns the current logical size and scale (1 until announced). Before
166+// the first configure the size is the requested one (zero for lock surfaces).
167+func (s *Surface) Size() (w, h int32, scale float64) { return s.width, s.height, s.scale }
168+
169+// Feedback returns the latest complete dmabuf feedback, or nil before the
170+// first Handler.FeedbackDone. The value and its slice stay valid until the
171+// next FeedbackDone.
172+func (s *Surface) Feedback() *Feedback {
173+ if !s.fbDone {
174+ return nil
175+ }
176+ return &s.fb
177+}
178+
179+// PhysicalSize returns the buffer size in pixels for the current logical size
180+// and scale.
181+func (s *Surface) PhysicalSize() (int32, int32, error) {
182+ if s.scale <= 0 || s.width <= 0 || s.height <= 0 {
183+ return 0, 0, errors.New("neferclient: invalid surface geometry")
184+ }
185+ if s.scale != math.Trunc(s.scale) && s.viewport == nil {
186+ return 0, 0, &CapabilityError{Name: viewporter.WpViewporterInterface, Cause: fmt.Errorf("fractional scale %g requires viewporter", s.scale)}
187+ }
188+ // Ignore sub-nanopixel floating noise at exact integer boundaries.
189+ x, y := math.Ceil(float64(s.width)*s.scale-1e-9), math.Ceil(float64(s.height)*s.scale-1e-9)
190+ if x > math.MaxInt32 || y > math.MaxInt32 {
191+ return 0, 0, errors.New("neferclient: buffer dimensions overflow")
192+ }
193+ return int32(x), int32(y), nil
194+}
195+
196+func validateLayer(o *LayerConfig) error {
197+ switch {
198+ case o.Level < LayerBackground || o.Level > LayerOverlay:
199+ return fmt.Errorf("neferclient: invalid layer level %d", o.Level)
200+ case o.Anchors&^anchorAll != 0:
201+ return fmt.Errorf("neferclient: invalid layer anchors %#x", uint8(o.Anchors))
202+ case o.Keyboard > KeyboardOnDemand:
203+ return fmt.Errorf("neferclient: invalid keyboard mode %d", o.Keyboard)
204+ case o.ExclusiveZone < -1:
205+ return fmt.Errorf("neferclient: invalid exclusive zone %d", o.ExclusiveZone)
206+ case strings.ContainsRune(o.Namespace, 0), strings.ContainsRune(o.Output, 0):
207+ return errors.New("neferclient: layer namespace or output contains NUL")
208+ case o.Width < 0 || o.Height < 0:
209+ return errors.New("neferclient: negative layer size")
210+ }
211+ if o.Anchors&(AnchorLeft|AnchorRight) != AnchorLeft|AnchorRight && o.Width == 0 ||
212+ o.Anchors&(AnchorTop|AnchorBottom) != AnchorTop|AnchorBottom && o.Height == 0 {
213+ return errors.New("neferclient: layer size required on axes not anchored to both edges")
214+ }
215+ return validateRects(o.InputRects)
216+}
217+
218+func validateRects(rects []Rect) error {
219+ for i, r := range rects {
220+ if r.Width <= 0 || r.Height <= 0 {
221+ return fmt.Errorf("neferclient: input rect %d has non-positive size %dx%d", i, r.Width, r.Height)
222+ }
223+ if int64(r.X)+int64(r.Width) > math.MaxInt32 || int64(r.Y)+int64(r.Height) > math.MaxInt32 {
224+ return fmt.Errorf("neferclient: input rect %d overflows", i)
225+ }
226+ }
227+ return nil
228+}
229+
230+// newSurface creates the wl_surface and the add-on objects shared by every
231+// role. The reader must be paused: handlers are installed after the requests.
232+func (c *Conn) newSurface(role surfaceRole, w, h int32) (*Surface, error) {
233+ if c.closed {
234+ return nil, ErrClosed
235+ }
236+ if err := c.ensureCore(); err != nil {
237+ return nil, err
238+ }
239+ g := &c.g
240+ c.nextSurf++
241+ s := &Surface{c: c, id: c.nextSurf, role: role, width: w, height: h, scale: 1, sentBufScale: 1,
242+ buffers: map[uint64]importedBuffer{}, timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{}}
243+ ok := false
244+ defer func() {
245+ if !ok {
246+ s.destroy()
247+ }
248+ }()
249+ var err error
250+ if s.surf, err = g.compositor.CreateSurface(); err != nil {
251+ return nil, err
252+ }
253+ q, sid := c.q, uint32(s.id)
254+ s.surf.OnPreferredBufferScale(func(f int32) {
255+ e := event{kind: evBufferScale, id: sid, a: f}
256+ q.post(&e)
257+ })
258+ if g.viewporter != nil {
259+ if s.viewport, err = g.viewporter.GetViewport(s.surf); err != nil {
260+ return nil, err
261+ }
262+ }
263+ if g.fscale != nil {
264+ if s.frac, err = g.fscale.GetFractionalScale(s.surf); err != nil {
265+ return nil, err
266+ }
267+ s.frac.OnPreferredScale(func(n uint32) {
268+ e := event{kind: evPreferredScale, id: sid, a: int32(n)}
269+ q.post(&e)
270+ })
271+ }
272+ // The role objects come next (callers); sync surface and feedback after.
273+ s.cb = &frameCallback{q: q, id: s.id}
274+ s.cb.SetContext(c.wlctx)
275+ s.req = wireRequests{s: s}
276+ c.surfaces[s.id] = s
277+ ok = true
278+ return s, nil
279+}
280+
281+// finishSurface creates the explicit-sync and feedback objects once the role
282+// exists.
283+func (s *Surface) finishSurface() error {
284+ c := s.c
285+ var err error
286+ if s.sync, err = c.g.syncobj.GetSurface(s.surf); err != nil {
287+ return err
288+ }
289+ if s.feedback, err = c.g.dmabuf.GetDefaultFeedback(); err != nil {
290+ return err
291+ }
292+ s.watchFeedback()
293+ return nil
294+}
295+
296+func (s *Surface) watchFeedback() {
297+ q, sid := s.c.q, uint32(s.id)
298+ s.feedback.OnFormatTable(func(fd *wl.OwnedFD, size uint32) {
299+ n, err := fd.Take()
300+ if err != nil {
301+ e := event{kind: evBadEvent, id: sid, a: badFeedbackFD}
302+ q.post(&e)
303+ return
304+ }
305+ e := event{kind: evFeedbackTable, id: sid, fd: int32(n), flags: size}
306+ q.post(&e)
307+ })
308+ s.feedback.OnMainDevice(func(dev []byte) {
309+ if len(dev) != 8 {
310+ e := event{kind: evBadEvent, id: sid, a: badMainDevice}
311+ q.post(&e)
312+ return
313+ }
314+ e := event{kind: evFeedbackMainDevice, id: sid, dev: binary.NativeEndian.Uint64(dev)}
315+ q.post(&e)
316+ })
317+ s.feedback.OnTrancheFormats(func(data []byte) {
318+ if len(data)%2 != 0 {
319+ e := event{kind: evBadEvent, id: sid, a: badTranche}
320+ q.post(&e)
321+ return
322+ }
323+ for len(data) > 0 { // chunks of up to maxNameLen/2 indices
324+ n := min(len(data), maxNameLen)
325+ e := event{kind: evFeedbackFormats, id: sid}
326+ e.setBytes(data[:n])
327+ q.post(&e)
328+ data = data[n:]
329+ }
330+ })
331+ s.feedback.OnDone(func() {
332+ e := event{kind: evFeedbackDone, id: sid}
333+ q.post(&e)
334+ })
335+}
336+
337+// Codes of evBadEvent.
338+const (
339+ badFeedbackFD int32 = iota + 1
340+ badMainDevice
341+ badTranche
342+ badConfigure
343+)
344+
345+var badEventErrors = [...]string{
346+ badFeedbackFD: "neferclient: dmabuf format table descriptor unavailable",
347+ badMainDevice: "neferclient: invalid dmabuf main_device length",
348+ badTranche: "neferclient: odd dmabuf tranche format list",
349+ badConfigure: "neferclient: configure size out of range",
350+}
351+
352+// configureEvent builds a layer or lock configure event, or a badConfigure
353+// event when the compositor announced an out-of-range size.
354+func configureEvent(kind eventKind, sid, serial, w, h uint32) event {
355+ if w > maxLayerDimension || h > maxLayerDimension {
356+ return event{kind: evBadEvent, id: sid, a: badConfigure}
357+ }
358+ return event{kind: kind, id: sid, serial: serial, a: int32(w), b: int32(h)}
359+}
360+
361+// NewLayerSurface creates a wlr-layer-shell surface. It becomes presentable
362+// after the first Handler.Configure.
363+func (c *Conn) NewLayerSurface(cfg LayerConfig) (*Surface, error) {
364+ if c.closed {
365+ return nil, ErrClosed
366+ }
367+ if err := validateLayer(&cfg); err != nil {
368+ return nil, err
369+ }
370+ if err := c.ensureLayerShell(); err != nil {
371+ return nil, err
372+ }
373+ if cfg.Keyboard == KeyboardOnDemand && c.g.layerVersion < 4 {
374+ return nil, &CapabilityError{Name: wlrlayershell.LayerShellInterface, Cause: fmt.Errorf("on-demand keyboard requires version 4, negotiated %d", c.g.layerVersion)}
375+ }
376+ var out *wayland.Output
377+ if cfg.Output != "" {
378+ for _, o := range c.outputs {
379+ if o.done && o.pub.Name == cfg.Output {
380+ out = o.proxy
381+ break
382+ }
383+ }
384+ if out == nil {
385+ return nil, &CapabilityError{Name: wayland.OutputInterface, Cause: fmt.Errorf("output %q not found", cfg.Output)}
386+ }
387+ }
388+ c.pause()
389+ defer c.resume()
390+ s, err := c.newSurface(roleLayer, cfg.Width, cfg.Height)
391+ if err != nil {
392+ return nil, err
393+ }
394+ ok := false
395+ defer func() {
396+ if !ok {
397+ s.destroy()
398+ }
399+ }()
400+ ns := cfg.Namespace
401+ if ns == "" {
402+ ns = DefaultLayerNamespace
403+ }
404+ level := [...]uint32{LayerBackground: wlrlayershell.LAYER_BACKGROUND, LayerBottom: wlrlayershell.LAYER_BOTTOM,
405+ LayerTop: wlrlayershell.LAYER_TOP, LayerOverlay: wlrlayershell.LAYER_OVERLAY}[cfg.Level]
406+ if s.layer, err = c.g.layer.GetLayerSurface(s.surf, out, level, ns); err != nil {
407+ return nil, err
408+ }
409+ q, sid := c.q, uint32(s.id)
410+ s.layer.OnClosed(func() {
411+ e := event{kind: evClosed, id: sid}
412+ q.post(&e)
413+ })
414+ s.layer.OnConfigure(func(serial, w, h uint32) {
415+ e := configureEvent(evLayerConfigure, sid, serial, w, h)
416+ q.post(&e)
417+ })
418+ // Opposite anchors let the compositor assign that axis (size 0).
419+ reqW, reqH := uint32(cfg.Width), uint32(cfg.Height)
420+ if cfg.Anchors&(AnchorLeft|AnchorRight) == AnchorLeft|AnchorRight {
421+ reqW = 0
422+ }
423+ if cfg.Anchors&(AnchorTop|AnchorBottom) == AnchorTop|AnchorBottom {
424+ reqH = 0
425+ }
426+ var anchor uint32
427+ for _, m := range [...][2]uint32{{uint32(AnchorTop), wlrlayershell.ANCHOR_TOP}, {uint32(AnchorBottom), wlrlayershell.ANCHOR_BOTTOM},
428+ {uint32(AnchorLeft), wlrlayershell.ANCHOR_LEFT}, {uint32(AnchorRight), wlrlayershell.ANCHOR_RIGHT}} {
429+ if uint32(cfg.Anchors)&m[0] != 0 {
430+ anchor |= m[1]
431+ }
432+ }
433+ for _, e := range []error{
434+ s.layer.SetSize(reqW, reqH),
435+ s.layer.SetAnchor(anchor),
436+ s.layer.SetExclusiveZone(cfg.ExclusiveZone),
437+ s.layer.SetMargin(cfg.Margin[0], cfg.Margin[1], cfg.Margin[2], cfg.Margin[3]),
438+ s.layer.SetKeyboardInteractivity(uint32(cfg.Keyboard)),
439+ } {
440+ if e != nil {
441+ return nil, e
442+ }
443+ }
444+ if cfg.InputRects != nil {
445+ if _, err = s.stageInputRects(cfg.InputRects); err != nil {
446+ return nil, err
447+ }
448+ }
449+ if err = s.finishSurface(); err != nil {
450+ return nil, err
451+ }
452+ if err = s.surf.Commit(); err != nil { // initial empty commit asks for configure
453+ return nil, err
454+ }
455+ ok = true
456+ return s, nil
457+}
458+
459+// NewToplevel creates an xdg-toplevel window with the given title and initial
460+// logical size. It becomes presentable after the first Handler.Configure.
461+func (c *Conn) NewToplevel(title string, w, h int32) (*Surface, error) {
462+ if c.closed {
463+ return nil, ErrClosed
464+ }
465+ if w <= 0 || h <= 0 {
466+ return nil, fmt.Errorf("neferclient: invalid logical size %dx%d", w, h)
467+ }
468+ if err := c.ensureXdg(); err != nil {
469+ return nil, err
470+ }
471+ c.pause()
472+ defer c.resume()
473+ s, err := c.newSurface(roleToplevel, w, h)
474+ if err != nil {
475+ return nil, err
476+ }
477+ ok := false
478+ defer func() {
479+ if !ok {
480+ s.destroy()
481+ }
482+ }()
483+ if s.xdg, err = c.g.xdg.GetXdgSurface(s.surf); err != nil {
484+ return nil, err
485+ }
486+ if s.toplevel, err = s.xdg.GetToplevel(); err != nil {
487+ return nil, err
488+ }
489+ q, sid := c.q, uint32(s.id)
490+ s.toplevel.OnClose(func() {
491+ e := event{kind: evClosed, id: sid}
492+ q.post(&e)
493+ })
494+ s.toplevel.OnConfigure(func(w, h int32, _ []byte) {
495+ e := event{kind: evXdgToplevel, id: sid, a: w, b: h}
496+ q.post(&e)
497+ })
498+ s.xdg.OnConfigure(func(serial uint32) {
499+ e := event{kind: evXdgConfigure, id: sid, serial: serial}
500+ q.post(&e)
501+ })
502+ if err = s.toplevel.SetTitle(title); err != nil {
503+ return nil, err
504+ }
505+ if err = s.finishSurface(); err != nil {
506+ return nil, err
507+ }
508+ if err = s.surf.Commit(); err != nil {
509+ return nil, err
510+ }
511+ ok = true
512+ return s, nil
513+}
514+
515+// applySurface handles every surface, lock and xdg_wm_base event on the owner.
516+func (c *Conn) applySurface(ev *event, h Handler) {
517+ defer ev.closeFD()
518+ switch ev.kind {
519+ case evPing:
520+ if c.g.xdg != nil {
521+ if err := c.g.xdg.Pong(ev.serial); err != nil {
522+ h.Error(err)
523+ }
524+ }
525+ return
526+ case evLocked, evLockFinished:
527+ l := c.curLock
528+ if l == nil || l.gen != ev.id {
529+ return
530+ }
531+ if ev.kind == evLocked {
532+ l.locked = true
533+ h.Locked()
534+ } else {
535+ l.finished = true
536+ h.LockFinished()
537+ }
538+ return
539+ }
540+ s := c.surfaces[SurfaceID(ev.id)]
541+ if s == nil || s.closed {
542+ return // closed while the event was queued
543+ }
544+ id := s.id
545+ switch ev.kind {
546+ case evBadEvent:
547+ h.Error(errors.New(badEventErrors[ev.a]))
548+ case evClosed:
549+ h.Closed(id)
550+ case evXdgToplevel:
551+ s.pendW, s.pendH = ev.a, ev.b
552+ case evXdgConfigure:
553+ if s.pendW > 0 && s.pendH > 0 {
554+ s.width, s.height = s.pendW, s.pendH
555+ }
556+ s.configure(s.xdg.AckConfigure(ev.serial), h)
557+ case evLayerConfigure:
558+ if ev.a > 0 {
559+ s.width = ev.a
560+ }
561+ if ev.b > 0 {
562+ s.height = ev.b
563+ }
564+ s.configure(s.layer.AckConfigure(ev.serial), h)
565+ case evLockConfigure:
566+ s.width, s.height = ev.a, ev.b
567+ s.configure(s.lockSurf.AckConfigure(ev.serial), h)
568+ case evFrame:
569+ s.frameReady = true
570+ h.Frame(id)
571+ case evPreferredScale:
572+ if ev.a <= 0 {
573+ h.Error(errors.New("neferclient: invalid fractional scale zero"))
574+ return
575+ }
576+ s.setScale(float64(ev.a)/120, h)
577+ case evBufferScale:
578+ if s.frac == nil && ev.a > 0 {
579+ s.setScale(float64(ev.a), h)
580+ }
581+ case evFeedbackTable:
582+ fd := int(ev.fd)
583+ ev.fd = -1 // readTable consumes fd; the deferred closeFD must not touch the number again
584+ if err := s.readTable(fd, ev.flags); err != nil {
585+ h.Error(err)
586+ }
587+ case evFeedbackMainDevice:
588+ s.pending.MainDevice = ev.dev
589+ case evFeedbackFormats:
590+ b := ev.nameBytes()
591+ for i := 0; i+1 < len(b); i += 2 {
592+ idx := int(binary.NativeEndian.Uint16(b[i:]))
593+ if idx >= len(s.table) {
594+ h.Error(fmt.Errorf("neferclient: dmabuf format table index %d out of range", idx))
595+ continue
596+ }
597+ s.pending.Formats = append(s.pending.Formats, s.table[idx])
598+ }
599+ case evFeedbackDone:
600+ s.fb, s.pending = s.pending, Feedback{Formats: s.fb.Formats[:0]}
601+ s.fbDone = true
602+ h.FeedbackDone(id)
603+ }
604+}
605+
606+func (s *Surface) configure(ackErr error, h Handler) {
607+ if ackErr != nil {
608+ h.Error(ackErr)
609+ return
610+ }
611+ if !s.configured {
612+ s.configured, s.frameReady = true, true
613+ }
614+ h.Configure(s.id, s.width, s.height)
615+}
616+
617+func (s *Surface) setScale(v float64, h Handler) {
618+ if v == s.scale {
619+ return
620+ }
621+ s.scale = v
622+ h.Scale(s.id, v)
623+}
624+
625+// readTable snapshots the format table with pread (no mapping of
626+// compositor-owned memory) and always consumes fd.
627+func (s *Surface) readTable(fd int, size uint32) error {
628+ defer unix.Close(fd)
629+ if size%16 != 0 || size > 16<<20 {
630+ return fmt.Errorf("neferclient: invalid dmabuf format table size %d", size)
631+ }
632+ c := s.c
633+ c.tableBuf = slices.Grow(c.tableBuf[:0], int(size))[:size]
634+ b := c.tableBuf
635+ for off := 0; off < len(b); {
636+ n, err := unix.Pread(fd, b[off:], int64(off))
637+ if err == unix.EINTR {
638+ continue
639+ }
640+ if err != nil {
641+ return fmt.Errorf("neferclient: read dmabuf format table: %w", err)
642+ }
643+ if n == 0 {
644+ return errors.New("neferclient: truncated dmabuf format table")
645+ }
646+ off += n
647+ }
648+ s.table = s.table[:0]
649+ for i := 0; i < len(b); i += 16 {
650+ s.table = append(s.table, Format{binary.NativeEndian.Uint32(b[i:]), binary.NativeEndian.Uint64(b[i+8:])})
651+ }
652+ return nil
653+}
654+
655+// SetInputRegion sets the input region (logical pixels). nil restores the
656+// whole surface; a non-nil empty slice passes all input through. The region is
657+// double-buffered state: it is committed immediately only once the surface
658+// presented a buffer; before that the next Present applies it (committing
659+// earlier would map a surface without a buffer, which lock surfaces forbid and
660+// layer surfaces answer with an error). Lock surfaces always take all input
661+// and refuse it.
662+func (s *Surface) SetInputRegion(rects []Rect) error {
663+ if s.closed {
664+ return ErrClosed
665+ }
666+ if s.role == roleLock {
667+ return errors.New("neferclient: lock surfaces take all input")
668+ }
669+ changed, err := s.stageInputRects(rects)
670+ if err != nil || !changed || !s.presented {
671+ return err
672+ }
673+ return s.surf.Commit()
674+}
675+
676+func (s *Surface) stageInputRects(rects []Rect) (bool, error) {
677+ if err := validateRects(rects); err != nil {
678+ return false, err
679+ }
680+ if (rects != nil) == s.inputCustom && slices.Equal(rects, s.inputRects) {
681+ return false, nil
682+ }
683+ if rects == nil {
684+ if err := s.surf.SetInputRegion(nil); err != nil {
685+ return false, err
686+ }
687+ } else {
688+ region, err := s.c.g.compositor.CreateRegion()
689+ if err != nil {
690+ return false, err
691+ }
692+ for _, r := range rects {
693+ if err = region.Add(r.X, r.Y, r.Width, r.Height); err != nil {
694+ break
695+ }
696+ }
697+ if err == nil {
698+ err = s.surf.SetInputRegion(region)
699+ }
700+ _ = region.Destroy()
701+ if err != nil {
702+ return false, err
703+ }
704+ }
705+ s.inputCustom = rects != nil
706+ s.inputRects = append(s.inputRects[:0], rects...)
707+ return true, nil
708+}
709+
710+// Close destroys the surface, its buffers, timelines and role objects,
711+// children before parents. It is safe to call more than once. The caller must
712+// stop using buffers it imported first (the compositor keeps them until it
713+// processes the destruction).
714+func (s *Surface) Close() error {
715+ if s == nil || s.closed {
716+ return nil
717+ }
718+ if s.c.closed {
719+ s.closed = true
720+ return nil
721+ }
722+ return s.destroy()
723+}
724+
725+func (s *Surface) destroy() error {
726+ // TODO(wlturbo): a frame callback pending here is unregistered locally, so
727+ // its later done/delete_id reaches an unknown object. Fixing it needs
728+ // server-side delete_id handling in wlturbo (see TestCloseWithPendingFrame).
729+ s.closed = true
730+ delete(s.c.surfaces, s.id)
731+ var errs []error
732+ add := func(err error) {
733+ if err != nil {
734+ errs = append(errs, err)
735+ }
736+ }
737+ for id, b := range s.buffers {
738+ add(b.buf.Destroy())
739+ delete(s.buffers, id)
740+ }
741+ for id, t := range s.timelines {
742+ add(t.Destroy())
743+ delete(s.timelines, id)
744+ }
745+ if s.feedback != nil {
746+ add(s.feedback.Destroy())
747+ }
748+ if s.sync != nil {
749+ add(s.sync.Destroy())
750+ }
751+ if s.lockSurf != nil {
752+ add(s.lockSurf.Destroy())
753+ }
754+ if s.layer != nil {
755+ add(s.layer.Destroy())
756+ }
757+ if s.toplevel != nil {
758+ add(s.toplevel.Destroy())
759+ }
760+ if s.xdg != nil {
761+ add(s.xdg.Destroy())
762+ }
763+ if s.frac != nil {
764+ add(s.frac.Destroy())
765+ }
766+ if s.viewport != nil {
767+ add(s.viewport.Destroy())
768+ }
769+ if s.surf != nil {
770+ add(s.surf.Destroy())
771+ }
772+ if s.cb != nil {
773+ s.c.wlctx.Unregister(s.cb)
774+ }
775+ return errors.Join(errs...)
776+}
@@ -0,0 +1,776 @@
1+package neferclient
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "fmt"
7+ "math"
8+ "slices"
9+ "strings"
10+
11+ "github.com/bnema/go-wayland-bindings/client/extsessionlock"
12+ "github.com/bnema/go-wayland-bindings/client/fractionalscale"
13+ "github.com/bnema/go-wayland-bindings/client/linuxdmabuf"
14+ "github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj"
15+ "github.com/bnema/go-wayland-bindings/client/viewporter"
16+ "github.com/bnema/go-wayland-bindings/client/wayland"
17+ "github.com/bnema/go-wayland-bindings/client/wlrlayershell"
18+ "github.com/bnema/go-wayland-bindings/client/xdgshell"
19+ "github.com/bnema/wlturbo/wl"
20+ "golang.org/x/sys/unix"
21+)
22+
23+// LayerLevel is the wlr-layer-shell stacking layer. The zero value is invalid.
24+type LayerLevel uint8
25+
26+const (
27+ LayerBackground LayerLevel = iota + 1
28+ LayerBottom
29+ LayerTop
30+ LayerOverlay
31+)
32+
33+// Anchor is a bit set of output edges a layer surface attaches to.
34+type Anchor uint8
35+
36+const (
37+ AnchorTop Anchor = 1 << iota
38+ AnchorBottom
39+ AnchorLeft
40+ AnchorRight
41+)
42+
43+const anchorAll = AnchorTop | AnchorBottom | AnchorLeft | AnchorRight
44+
45+// KeyboardMode is the layer surface keyboard interactivity. The zero value is
46+// KeyboardNone. KeyboardOnDemand needs zwlr_layer_shell_v1 version 4.
47+type KeyboardMode uint8
48+
49+const (
50+ KeyboardNone KeyboardMode = iota
51+ KeyboardExclusive
52+ KeyboardOnDemand
53+)
54+
55+// maxLayerDimension bounds a compositor-assigned layer surface extent.
56+const maxLayerDimension = 16384
57+
58+// DefaultLayerNamespace is used when [LayerConfig.Namespace] is empty.
59+const DefaultLayerNamespace = "neferclient"
60+
61+// LayerConfig requests a wlr-layer-shell surface. Width and Height give the
62+// initial logical size; an axis anchored to both opposite edges is sized by
63+// the compositor and reported through Handler.Configure.
64+type LayerConfig struct {
65+ Output string // wl_output name; empty lets the compositor choose
66+ Namespace string // defaults to [DefaultLayerNamespace]
67+ Level LayerLevel
68+ Anchors Anchor
69+ Keyboard KeyboardMode
70+ ExclusiveZone int32 // >0 reserves, 0 neutral, -1 ignores other zones
71+ Margin [4]int32 // top, right, bottom, left
72+ // InputRects, when non-nil, restricts pointer input to these rectangles
73+ // (logical pixels). An empty non-nil slice makes the surface click-through.
74+ InputRects []Rect
75+ // Width and Height are the requested logical size. They are required on
76+ // every axis that is not anchored to both opposite edges.
77+ Width, Height int32
78+}
79+
80+type surfaceRole uint8
81+
82+const (
83+ roleToplevel surfaceRole = iota + 1
84+ roleLayer
85+ roleLock
86+)
87+
88+// Surface is one wl_surface with its role and the DMA-BUF presentation
89+// objects. Every method belongs to the owner goroutine.
90+type Surface struct {
91+ c *Conn
92+ id SurfaceID
93+ role surfaceRole
94+
95+ surf *wayland.Surface
96+ viewport *viewporter.WpViewport
97+ frac *fractionalscale.WpFractionalScale
98+ sync *linuxdrmsyncobj.WpLinuxDrmSyncobjSurface
99+ feedback *linuxdmabuf.LinuxDmabufFeedback
100+ xdg *xdgshell.XdgSurface
101+ toplevel *xdgshell.XdgToplevel
102+ layer *wlrlayershell.LayerSurface
103+ lockSurf *extsessionlock.ExtSessionLockSurface
104+ lock *Lock
105+
106+ width, height int32
107+ scale float64
108+ pendW, pendH int32 // toplevel size announced before the xdg configure
109+ configured bool
110+ frameReady bool
111+ closed bool
112+
113+ // destination/opaque state last sent; they are double-buffered surface
114+ // state that persists across commits, so unchanged values are not resent.
115+ sentDestW, sentDestH int32
116+ sentBufScale int32 // starts at 1, the protocol default
117+ presented bool // a Present committed: the surface is mapped
118+ output uint32 // lock surfaces: the covered wl_output global
119+ opaqueSet bool
120+ opaqueW, opaqueH int32
121+
122+ cb *frameCallback
123+ req surfaceRequests
124+
125+ inputRects []Rect
126+ inputCustom bool
127+
128+ buffers map[uint64]importedBuffer
129+ timelines map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline
130+
131+ fb Feedback
132+ table []Format // current format table
133+ pending Feedback // round in progress; Formats reused across rounds
134+ fbDone bool
135+}
136+
137+// frameCallback is a reusable wl_callback: the library keeps a single frame
138+// callback pending per surface, so one proxy object serves all of them.
139+type frameCallback struct {
140+ wl.BaseProxy
141+ q *queue
142+ id SurfaceID
143+}
144+
145+func (f *frameCallback) EventSignature(op uint16) (string, bool) {
146+ if op == 0 {
147+ return "uint,", true
148+ }
149+ return "", false
150+}
151+
152+func (f *frameCallback) Dispatch(ev *wl.Event) {
153+ if ev.Opcode != 0 {
154+ return
155+ }
156+ _ = ev.Uint32()
157+ f.Context().Unregister(f)
158+ e := event{kind: evFrame, id: uint32(f.id)}
159+ f.q.post(&e)
160+}
161+
162+// ID returns the surface's identifier used in Handler callbacks.
163+func (s *Surface) ID() SurfaceID { return s.id }
164+
165+// Size returns the current logical size and scale (1 until announced). Before
166+// the first configure the size is the requested one (zero for lock surfaces).
167+func (s *Surface) Size() (w, h int32, scale float64) { return s.width, s.height, s.scale }
168+
169+// Feedback returns the latest complete dmabuf feedback, or nil before the
170+// first Handler.FeedbackDone. The value and its slice stay valid until the
171+// next FeedbackDone.
172+func (s *Surface) Feedback() *Feedback {
173+ if !s.fbDone {
174+ return nil
175+ }
176+ return &s.fb
177+}
178+
179+// PhysicalSize returns the buffer size in pixels for the current logical size
180+// and scale.
181+func (s *Surface) PhysicalSize() (int32, int32, error) {
182+ if s.scale <= 0 || s.width <= 0 || s.height <= 0 {
183+ return 0, 0, errors.New("neferclient: invalid surface geometry")
184+ }
185+ if s.scale != math.Trunc(s.scale) && s.viewport == nil {
186+ return 0, 0, &CapabilityError{Name: viewporter.WpViewporterInterface, Cause: fmt.Errorf("fractional scale %g requires viewporter", s.scale)}
187+ }
188+ // Ignore sub-nanopixel floating noise at exact integer boundaries.
189+ x, y := math.Ceil(float64(s.width)*s.scale-1e-9), math.Ceil(float64(s.height)*s.scale-1e-9)
190+ if x > math.MaxInt32 || y > math.MaxInt32 {
191+ return 0, 0, errors.New("neferclient: buffer dimensions overflow")
192+ }
193+ return int32(x), int32(y), nil
194+}
195+
196+func validateLayer(o *LayerConfig) error {
197+ switch {
198+ case o.Level < LayerBackground || o.Level > LayerOverlay:
199+ return fmt.Errorf("neferclient: invalid layer level %d", o.Level)
200+ case o.Anchors&^anchorAll != 0:
201+ return fmt.Errorf("neferclient: invalid layer anchors %#x", uint8(o.Anchors))
202+ case o.Keyboard > KeyboardOnDemand:
203+ return fmt.Errorf("neferclient: invalid keyboard mode %d", o.Keyboard)
204+ case o.ExclusiveZone < -1:
205+ return fmt.Errorf("neferclient: invalid exclusive zone %d", o.ExclusiveZone)
206+ case strings.ContainsRune(o.Namespace, 0), strings.ContainsRune(o.Output, 0):
207+ return errors.New("neferclient: layer namespace or output contains NUL")
208+ case o.Width < 0 || o.Height < 0:
209+ return errors.New("neferclient: negative layer size")
210+ }
211+ if o.Anchors&(AnchorLeft|AnchorRight) != AnchorLeft|AnchorRight && o.Width == 0 ||
212+ o.Anchors&(AnchorTop|AnchorBottom) != AnchorTop|AnchorBottom && o.Height == 0 {
213+ return errors.New("neferclient: layer size required on axes not anchored to both edges")
214+ }
215+ return validateRects(o.InputRects)
216+}
217+
218+func validateRects(rects []Rect) error {
219+ for i, r := range rects {
220+ if r.Width <= 0 || r.Height <= 0 {
221+ return fmt.Errorf("neferclient: input rect %d has non-positive size %dx%d", i, r.Width, r.Height)
222+ }
223+ if int64(r.X)+int64(r.Width) > math.MaxInt32 || int64(r.Y)+int64(r.Height) > math.MaxInt32 {
224+ return fmt.Errorf("neferclient: input rect %d overflows", i)
225+ }
226+ }
227+ return nil
228+}
229+
230+// newSurface creates the wl_surface and the add-on objects shared by every
231+// role. The reader must be paused: handlers are installed after the requests.
232+func (c *Conn) newSurface(role surfaceRole, w, h int32) (*Surface, error) {
233+ if c.closed {
234+ return nil, ErrClosed
235+ }
236+ if err := c.ensureCore(); err != nil {
237+ return nil, err
238+ }
239+ g := &c.g
240+ c.nextSurf++
241+ s := &Surface{c: c, id: c.nextSurf, role: role, width: w, height: h, scale: 1, sentBufScale: 1,
242+ buffers: map[uint64]importedBuffer{}, timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{}}
243+ ok := false
244+ defer func() {
245+ if !ok {
246+ s.destroy()
247+ }
248+ }()
249+ var err error
250+ if s.surf, err = g.compositor.CreateSurface(); err != nil {
251+ return nil, err
252+ }
253+ q, sid := c.q, uint32(s.id)
254+ s.surf.OnPreferredBufferScale(func(f int32) {
255+ e := event{kind: evBufferScale, id: sid, a: f}
256+ q.post(&e)
257+ })
258+ if g.viewporter != nil {
259+ if s.viewport, err = g.viewporter.GetViewport(s.surf); err != nil {
260+ return nil, err
261+ }
262+ }
263+ if g.fscale != nil {
264+ if s.frac, err = g.fscale.GetFractionalScale(s.surf); err != nil {
265+ return nil, err
266+ }
267+ s.frac.OnPreferredScale(func(n uint32) {
268+ e := event{kind: evPreferredScale, id: sid, a: int32(n)}
269+ q.post(&e)
270+ })
271+ }
272+ // The role objects come next (callers); sync surface and feedback after.
273+ s.cb = &frameCallback{q: q, id: s.id}
274+ s.cb.SetContext(c.wlctx)
275+ s.req = wireRequests{s: s}
276+ c.surfaces[s.id] = s
277+ ok = true
278+ return s, nil
279+}
280+
281+// finishSurface creates the explicit-sync and feedback objects once the role
282+// exists.
283+func (s *Surface) finishSurface() error {
284+ c := s.c
285+ var err error
286+ if s.sync, err = c.g.syncobj.GetSurface(s.surf); err != nil {
287+ return err
288+ }
289+ if s.feedback, err = c.g.dmabuf.GetDefaultFeedback(); err != nil {
290+ return err
291+ }
292+ s.watchFeedback()
293+ return nil
294+}
295+
296+func (s *Surface) watchFeedback() {
297+ q, sid := s.c.q, uint32(s.id)
298+ s.feedback.OnFormatTable(func(fd *wl.OwnedFD, size uint32) {
299+ n, err := fd.Take()
300+ if err != nil {
301+ e := event{kind: evBadEvent, id: sid, a: badFeedbackFD}
302+ q.post(&e)
303+ return
304+ }
305+ e := event{kind: evFeedbackTable, id: sid, fd: int32(n), flags: size}
306+ q.post(&e)
307+ })
308+ s.feedback.OnMainDevice(func(dev []byte) {
309+ if len(dev) != 8 {
310+ e := event{kind: evBadEvent, id: sid, a: badMainDevice}
311+ q.post(&e)
312+ return
313+ }
314+ e := event{kind: evFeedbackMainDevice, id: sid, dev: binary.NativeEndian.Uint64(dev)}
315+ q.post(&e)
316+ })
317+ s.feedback.OnTrancheFormats(func(data []byte) {
318+ if len(data)%2 != 0 {
319+ e := event{kind: evBadEvent, id: sid, a: badTranche}
320+ q.post(&e)
321+ return
322+ }
323+ for len(data) > 0 { // chunks of up to maxNameLen/2 indices
324+ n := min(len(data), maxNameLen)
325+ e := event{kind: evFeedbackFormats, id: sid}
326+ e.setBytes(data[:n])
327+ q.post(&e)
328+ data = data[n:]
329+ }
330+ })
331+ s.feedback.OnDone(func() {
332+ e := event{kind: evFeedbackDone, id: sid}
333+ q.post(&e)
334+ })
335+}
336+
337+// Codes of evBadEvent.
338+const (
339+ badFeedbackFD int32 = iota + 1
340+ badMainDevice
341+ badTranche
342+ badConfigure
343+)
344+
345+var badEventErrors = [...]string{
346+ badFeedbackFD: "neferclient: dmabuf format table descriptor unavailable",
347+ badMainDevice: "neferclient: invalid dmabuf main_device length",
348+ badTranche: "neferclient: odd dmabuf tranche format list",
349+ badConfigure: "neferclient: configure size out of range",
350+}
351+
352+// configureEvent builds a layer or lock configure event, or a badConfigure
353+// event when the compositor announced an out-of-range size.
354+func configureEvent(kind eventKind, sid, serial, w, h uint32) event {
355+ if w > maxLayerDimension || h > maxLayerDimension {
356+ return event{kind: evBadEvent, id: sid, a: badConfigure}
357+ }
358+ return event{kind: kind, id: sid, serial: serial, a: int32(w), b: int32(h)}
359+}
360+
361+// NewLayerSurface creates a wlr-layer-shell surface. It becomes presentable
362+// after the first Handler.Configure.
363+func (c *Conn) NewLayerSurface(cfg LayerConfig) (*Surface, error) {
364+ if c.closed {
365+ return nil, ErrClosed
366+ }
367+ if err := validateLayer(&cfg); err != nil {
368+ return nil, err
369+ }
370+ if err := c.ensureLayerShell(); err != nil {
371+ return nil, err
372+ }
373+ if cfg.Keyboard == KeyboardOnDemand && c.g.layerVersion < 4 {
374+ return nil, &CapabilityError{Name: wlrlayershell.LayerShellInterface, Cause: fmt.Errorf("on-demand keyboard requires version 4, negotiated %d", c.g.layerVersion)}
375+ }
376+ var out *wayland.Output
377+ if cfg.Output != "" {
378+ for _, o := range c.outputs {
379+ if o.done && o.pub.Name == cfg.Output {
380+ out = o.proxy
381+ break
382+ }
383+ }
384+ if out == nil {
385+ return nil, &CapabilityError{Name: wayland.OutputInterface, Cause: fmt.Errorf("output %q not found", cfg.Output)}
386+ }
387+ }
388+ c.pause()
389+ defer c.resume()
390+ s, err := c.newSurface(roleLayer, cfg.Width, cfg.Height)
391+ if err != nil {
392+ return nil, err
393+ }
394+ ok := false
395+ defer func() {
396+ if !ok {
397+ s.destroy()
398+ }
399+ }()
400+ ns := cfg.Namespace
401+ if ns == "" {
402+ ns = DefaultLayerNamespace
403+ }
404+ level := [...]uint32{LayerBackground: wlrlayershell.LAYER_BACKGROUND, LayerBottom: wlrlayershell.LAYER_BOTTOM,
405+ LayerTop: wlrlayershell.LAYER_TOP, LayerOverlay: wlrlayershell.LAYER_OVERLAY}[cfg.Level]
406+ if s.layer, err = c.g.layer.GetLayerSurface(s.surf, out, level, ns); err != nil {
407+ return nil, err
408+ }
409+ q, sid := c.q, uint32(s.id)
410+ s.layer.OnClosed(func() {
411+ e := event{kind: evClosed, id: sid}
412+ q.post(&e)
413+ })
414+ s.layer.OnConfigure(func(serial, w, h uint32) {
415+ e := configureEvent(evLayerConfigure, sid, serial, w, h)
416+ q.post(&e)
417+ })
418+ // Opposite anchors let the compositor assign that axis (size 0).
419+ reqW, reqH := uint32(cfg.Width), uint32(cfg.Height)
420+ if cfg.Anchors&(AnchorLeft|AnchorRight) == AnchorLeft|AnchorRight {
421+ reqW = 0
422+ }
423+ if cfg.Anchors&(AnchorTop|AnchorBottom) == AnchorTop|AnchorBottom {
424+ reqH = 0
425+ }
426+ var anchor uint32
427+ for _, m := range [...][2]uint32{{uint32(AnchorTop), wlrlayershell.ANCHOR_TOP}, {uint32(AnchorBottom), wlrlayershell.ANCHOR_BOTTOM},
428+ {uint32(AnchorLeft), wlrlayershell.ANCHOR_LEFT}, {uint32(AnchorRight), wlrlayershell.ANCHOR_RIGHT}} {
429+ if uint32(cfg.Anchors)&m[0] != 0 {
430+ anchor |= m[1]
431+ }
432+ }
433+ for _, e := range []error{
434+ s.layer.SetSize(reqW, reqH),
435+ s.layer.SetAnchor(anchor),
436+ s.layer.SetExclusiveZone(cfg.ExclusiveZone),
437+ s.layer.SetMargin(cfg.Margin[0], cfg.Margin[1], cfg.Margin[2], cfg.Margin[3]),
438+ s.layer.SetKeyboardInteractivity(uint32(cfg.Keyboard)),
439+ } {
440+ if e != nil {
441+ return nil, e
442+ }
443+ }
444+ if cfg.InputRects != nil {
445+ if _, err = s.stageInputRects(cfg.InputRects); err != nil {
446+ return nil, err
447+ }
448+ }
449+ if err = s.finishSurface(); err != nil {
450+ return nil, err
451+ }
452+ if err = s.surf.Commit(); err != nil { // initial empty commit asks for configure
453+ return nil, err
454+ }
455+ ok = true
456+ return s, nil
457+}
458+
459+// NewToplevel creates an xdg-toplevel window with the given title and initial
460+// logical size. It becomes presentable after the first Handler.Configure.
461+func (c *Conn) NewToplevel(title string, w, h int32) (*Surface, error) {
462+ if c.closed {
463+ return nil, ErrClosed
464+ }
465+ if w <= 0 || h <= 0 {
466+ return nil, fmt.Errorf("neferclient: invalid logical size %dx%d", w, h)
467+ }
468+ if err := c.ensureXdg(); err != nil {
469+ return nil, err
470+ }
471+ c.pause()
472+ defer c.resume()
473+ s, err := c.newSurface(roleToplevel, w, h)
474+ if err != nil {
475+ return nil, err
476+ }
477+ ok := false
478+ defer func() {
479+ if !ok {
480+ s.destroy()
481+ }
482+ }()
483+ if s.xdg, err = c.g.xdg.GetXdgSurface(s.surf); err != nil {
484+ return nil, err
485+ }
486+ if s.toplevel, err = s.xdg.GetToplevel(); err != nil {
487+ return nil, err
488+ }
489+ q, sid := c.q, uint32(s.id)
490+ s.toplevel.OnClose(func() {
491+ e := event{kind: evClosed, id: sid}
492+ q.post(&e)
493+ })
494+ s.toplevel.OnConfigure(func(w, h int32, _ []byte) {
495+ e := event{kind: evXdgToplevel, id: sid, a: w, b: h}
496+ q.post(&e)
497+ })
498+ s.xdg.OnConfigure(func(serial uint32) {
499+ e := event{kind: evXdgConfigure, id: sid, serial: serial}
500+ q.post(&e)
501+ })
502+ if err = s.toplevel.SetTitle(title); err != nil {
503+ return nil, err
504+ }
505+ if err = s.finishSurface(); err != nil {
506+ return nil, err
507+ }
508+ if err = s.surf.Commit(); err != nil {
509+ return nil, err
510+ }
511+ ok = true
512+ return s, nil
513+}
514+
515+// applySurface handles every surface, lock and xdg_wm_base event on the owner.
516+func (c *Conn) applySurface(ev *event, h Handler) {
517+ defer ev.closeFD()
518+ switch ev.kind {
519+ case evPing:
520+ if c.g.xdg != nil {
521+ if err := c.g.xdg.Pong(ev.serial); err != nil {
522+ h.Error(err)
523+ }
524+ }
525+ return
526+ case evLocked, evLockFinished:
527+ l := c.curLock
528+ if l == nil || l.gen != ev.id {
529+ return
530+ }
531+ if ev.kind == evLocked {
532+ l.locked = true
533+ h.Locked()
534+ } else {
535+ l.finished = true
536+ h.LockFinished()
537+ }
538+ return
539+ }
540+ s := c.surfaces[SurfaceID(ev.id)]
541+ if s == nil || s.closed {
542+ return // closed while the event was queued
543+ }
544+ id := s.id
545+ switch ev.kind {
546+ case evBadEvent:
547+ h.Error(errors.New(badEventErrors[ev.a]))
548+ case evClosed:
549+ h.Closed(id)
550+ case evXdgToplevel:
551+ s.pendW, s.pendH = ev.a, ev.b
552+ case evXdgConfigure:
553+ if s.pendW > 0 && s.pendH > 0 {
554+ s.width, s.height = s.pendW, s.pendH
555+ }
556+ s.configure(s.xdg.AckConfigure(ev.serial), h)
557+ case evLayerConfigure:
558+ if ev.a > 0 {
559+ s.width = ev.a
560+ }
561+ if ev.b > 0 {
562+ s.height = ev.b
563+ }
564+ s.configure(s.layer.AckConfigure(ev.serial), h)
565+ case evLockConfigure:
566+ s.width, s.height = ev.a, ev.b
567+ s.configure(s.lockSurf.AckConfigure(ev.serial), h)
568+ case evFrame:
569+ s.frameReady = true
570+ h.Frame(id)
571+ case evPreferredScale:
572+ if ev.a <= 0 {
573+ h.Error(errors.New("neferclient: invalid fractional scale zero"))
574+ return
575+ }
576+ s.setScale(float64(ev.a)/120, h)
577+ case evBufferScale:
578+ if s.frac == nil && ev.a > 0 {
579+ s.setScale(float64(ev.a), h)
580+ }
581+ case evFeedbackTable:
582+ fd := int(ev.fd)
583+ ev.fd = -1 // readTable consumes fd; the deferred closeFD must not touch the number again
584+ if err := s.readTable(fd, ev.flags); err != nil {
585+ h.Error(err)
586+ }
587+ case evFeedbackMainDevice:
588+ s.pending.MainDevice = ev.dev
589+ case evFeedbackFormats:
590+ b := ev.nameBytes()
591+ for i := 0; i+1 < len(b); i += 2 {
592+ idx := int(binary.NativeEndian.Uint16(b[i:]))
593+ if idx >= len(s.table) {
594+ h.Error(fmt.Errorf("neferclient: dmabuf format table index %d out of range", idx))
595+ continue
596+ }
597+ s.pending.Formats = append(s.pending.Formats, s.table[idx])
598+ }
599+ case evFeedbackDone:
600+ s.fb, s.pending = s.pending, Feedback{Formats: s.fb.Formats[:0]}
601+ s.fbDone = true
602+ h.FeedbackDone(id)
603+ }
604+}
605+
606+func (s *Surface) configure(ackErr error, h Handler) {
607+ if ackErr != nil {
608+ h.Error(ackErr)
609+ return
610+ }
611+ if !s.configured {
612+ s.configured, s.frameReady = true, true
613+ }
614+ h.Configure(s.id, s.width, s.height)
615+}
616+
617+func (s *Surface) setScale(v float64, h Handler) {
618+ if v == s.scale {
619+ return
620+ }
621+ s.scale = v
622+ h.Scale(s.id, v)
623+}
624+
625+// readTable snapshots the format table with pread (no mapping of
626+// compositor-owned memory) and always consumes fd.
627+func (s *Surface) readTable(fd int, size uint32) error {
628+ defer unix.Close(fd)
629+ if size%16 != 0 || size > 16<<20 {
630+ return fmt.Errorf("neferclient: invalid dmabuf format table size %d", size)
631+ }
632+ c := s.c
633+ c.tableBuf = slices.Grow(c.tableBuf[:0], int(size))[:size]
634+ b := c.tableBuf
635+ for off := 0; off < len(b); {
636+ n, err := unix.Pread(fd, b[off:], int64(off))
637+ if err == unix.EINTR {
638+ continue
639+ }
640+ if err != nil {
641+ return fmt.Errorf("neferclient: read dmabuf format table: %w", err)
642+ }
643+ if n == 0 {
644+ return errors.New("neferclient: truncated dmabuf format table")
645+ }
646+ off += n
647+ }
648+ s.table = s.table[:0]
649+ for i := 0; i < len(b); i += 16 {
650+ s.table = append(s.table, Format{binary.NativeEndian.Uint32(b[i:]), binary.NativeEndian.Uint64(b[i+8:])})
651+ }
652+ return nil
653+}
654+
655+// SetInputRegion sets the input region (logical pixels). nil restores the
656+// whole surface; a non-nil empty slice passes all input through. The region is
657+// double-buffered state: it is committed immediately only once the surface
658+// presented a buffer; before that the next Present applies it (committing
659+// earlier would map a surface without a buffer, which lock surfaces forbid and
660+// layer surfaces answer with an error). Lock surfaces always take all input
661+// and refuse it.
662+func (s *Surface) SetInputRegion(rects []Rect) error {
663+ if s.closed {
664+ return ErrClosed
665+ }
666+ if s.role == roleLock {
667+ return errors.New("neferclient: lock surfaces take all input")
668+ }
669+ changed, err := s.stageInputRects(rects)
670+ if err != nil || !changed || !s.presented {
671+ return err
672+ }
673+ return s.surf.Commit()
674+}
675+
676+func (s *Surface) stageInputRects(rects []Rect) (bool, error) {
677+ if err := validateRects(rects); err != nil {
678+ return false, err
679+ }
680+ if (rects != nil) == s.inputCustom && slices.Equal(rects, s.inputRects) {
681+ return false, nil
682+ }
683+ if rects == nil {
684+ if err := s.surf.SetInputRegion(nil); err != nil {
685+ return false, err
686+ }
687+ } else {
688+ region, err := s.c.g.compositor.CreateRegion()
689+ if err != nil {
690+ return false, err
691+ }
692+ for _, r := range rects {
693+ if err = region.Add(r.X, r.Y, r.Width, r.Height); err != nil {
694+ break
695+ }
696+ }
697+ if err == nil {
698+ err = s.surf.SetInputRegion(region)
699+ }
700+ _ = region.Destroy()
701+ if err != nil {
702+ return false, err
703+ }
704+ }
705+ s.inputCustom = rects != nil
706+ s.inputRects = append(s.inputRects[:0], rects...)
707+ return true, nil
708+}
709+
710+// Close destroys the surface, its buffers, timelines and role objects,
711+// children before parents. It is safe to call more than once. The caller must
712+// stop using buffers it imported first (the compositor keeps them until it
713+// processes the destruction).
714+func (s *Surface) Close() error {
715+ if s == nil || s.closed {
716+ return nil
717+ }
718+ if s.c.closed {
719+ s.closed = true
720+ return nil
721+ }
722+ return s.destroy()
723+}
724+
725+func (s *Surface) destroy() error {
726+ // TODO(wlturbo): a frame callback pending here is unregistered locally, so
727+ // its later done/delete_id reaches an unknown object. Fixing it needs
728+ // server-side delete_id handling in wlturbo (see TestCloseWithPendingFrame).
729+ s.closed = true
730+ delete(s.c.surfaces, s.id)
731+ var errs []error
732+ add := func(err error) {
733+ if err != nil {
734+ errs = append(errs, err)
735+ }
736+ }
737+ for id, b := range s.buffers {
738+ add(b.buf.Destroy())
739+ delete(s.buffers, id)
740+ }
741+ for id, t := range s.timelines {
742+ add(t.Destroy())
743+ delete(s.timelines, id)
744+ }
745+ if s.feedback != nil {
746+ add(s.feedback.Destroy())
747+ }
748+ if s.sync != nil {
749+ add(s.sync.Destroy())
750+ }
751+ if s.lockSurf != nil {
752+ add(s.lockSurf.Destroy())
753+ }
754+ if s.layer != nil {
755+ add(s.layer.Destroy())
756+ }
757+ if s.toplevel != nil {
758+ add(s.toplevel.Destroy())
759+ }
760+ if s.xdg != nil {
761+ add(s.xdg.Destroy())
762+ }
763+ if s.frac != nil {
764+ add(s.frac.Destroy())
765+ }
766+ if s.viewport != nil {
767+ add(s.viewport.Destroy())
768+ }
769+ if s.surf != nil {
770+ add(s.surf.Destroy())
771+ }
772+ if s.cb != nil {
773+ s.c.wlctx.Unregister(s.cb)
774+ }
775+ return errors.Join(errs...)
776+}
A surface_internal_test.go +292 -0
@@ -0,0 +1,292 @@
1+package neferclient
2+
3+import (
4+ "encoding/binary"
5+ "strings"
6+ "testing"
7+
8+ "github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj"
9+ "github.com/bnema/go-wayland-bindings/client/viewporter"
10+ "github.com/stretchr/testify/mock"
11+ "github.com/stretchr/testify/require"
12+ "golang.org/x/sys/unix"
13+)
14+
15+// presentable returns a configured surface that sends through req.
16+func presentable(req surfaceRequests, scale float64) *Surface {
17+ return &Surface{id: 1, req: req, width: 100, height: 50, scale: scale, sentBufScale: 1, role: roleToplevel,
18+ configured: true, frameReady: true, fbDone: true,
19+ buffers: map[uint64]importedBuffer{1: {w: 100, h: 50}},
20+ timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{1: nil, 2: nil},
21+ viewport: &viewporter.WpViewport{}}
22+}
23+
24+func TestPresentOrdering(t *testing.T) {
25+ m := newMocksurfaceRequests(t)
26+ s := presentable(m, 1.5)
27+ // NotBefore makes testify fail a call that arrives before its predecessor.
28+ dest := m.EXPECT().setDestination(int32(100), int32(50)).Return(nil).Once()
29+ opaque := m.EXPECT().setOpaqueRegion(int32(100), int32(50), true).Return(nil).Once().NotBefore(dest)
30+ acquire := m.EXPECT().setAcquire(uint64(1), uint64(7)).Return(nil).Once().NotBefore(opaque)
31+ release := m.EXPECT().setRelease(uint64(2), uint64(8)).Return(nil).Once().NotBefore(acquire)
32+ attach := m.EXPECT().attach(uint64(1)).Return(nil).Once().NotBefore(release)
33+ damage := m.EXPECT().damageBuffer(Rect{1, 2, 3, 4}).Return(nil).Once().NotBefore(attach)
34+ frame := m.EXPECT().frame().Return(nil).Once().NotBefore(damage)
35+ m.EXPECT().commit().Return(nil).Once().NotBefore(frame)
36+
37+ p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2, AcquirePoint: 7, ReleasePoint: 8,
38+ Damage: []Rect{{1, 2, 3, 4}}, Opaque: true}
39+ require.NoError(t, s.Present(p))
40+ require.False(t, s.frameReady)
41+ require.ErrorContains(t, s.Present(p), "frame callback pending") // no further request: strict mock
42+}
43+
44+func TestPresentSkipsUnchangedState(t *testing.T) {
45+ m := newMocksurfaceRequests(t)
46+ s := presentable(m, 2)
47+ m.EXPECT().setDestination(int32(100), int32(50)).Return(nil).Once()
48+ m.EXPECT().setOpaqueRegion(int32(100), int32(50), true).Return(nil).Once()
49+ m.EXPECT().setAcquire(mock.Anything, mock.Anything).Return(nil).Twice()
50+ m.EXPECT().setRelease(mock.Anything, mock.Anything).Return(nil).Twice()
51+ m.EXPECT().attach(uint64(1)).Return(nil).Twice()
52+ m.EXPECT().damageBuffer(Rect{0, 0, 200, 100}).Return(nil).Twice() // no damage: whole physical buffer
53+ m.EXPECT().frame().Return(nil).Twice()
54+ m.EXPECT().commit().Return(nil).Twice()
55+ p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2, Opaque: true}
56+ require.NoError(t, s.Present(p))
57+ s.frameReady = true
58+ require.NoError(t, s.Present(p)) // destination and opaque region are not resent
59+}
60+
61+func TestPresentRefusals(t *testing.T) {
62+ m := newMocksurfaceRequests(t) // strict: a refused Present sends nothing
63+ p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2}
64+ for name, mutate := range map[string]func(*Surface){
65+ "before configure": func(s *Surface) { s.configured = false },
66+ "before feedback": func(s *Surface) { s.fbDone = false },
67+ "unknown buffer": func(s *Surface) { delete(s.buffers, 1) },
68+ "unknown timeline": func(s *Surface) { delete(s.timelines, 2) },
69+ "closed": func(s *Surface) { s.closed = true },
70+ "bad geometry": func(s *Surface) { s.scale = 0 },
71+ "fractional without viewporter": func(s *Surface) {
72+ s.scale, s.viewport = 1.5, nil
73+ },
74+ } {
75+ s := presentable(m, 1)
76+ mutate(s)
77+ require.Error(t, s.Present(p), name)
78+ }
79+}
80+
81+func TestPhysicalSize(t *testing.T) {
82+ s := presentable(nil, 1.25)
83+ w, h, err := s.PhysicalSize()
84+ require.NoError(t, err)
85+ require.Equal(t, [2]int32{125, 63}, [2]int32{w, h}) // 62.5 rounds up
86+}
87+
88+func tableFD(t *testing.T, entries []Format) int {
89+ t.Helper()
90+ fd, err := unix.MemfdCreate("table", unix.MFD_CLOEXEC)
91+ require.NoError(t, err)
92+ var b []byte
93+ for _, e := range entries {
94+ b = binary.NativeEndian.AppendUint32(b, e.FourCC)
95+ b = binary.NativeEndian.AppendUint32(b, 0)
96+ b = binary.NativeEndian.AppendUint64(b, e.Modifier)
97+ }
98+ _, err = unix.Write(fd, b)
99+ require.NoError(t, err)
100+ return fd
101+}
102+
103+func postRound(c *Conn, s *Surface, fd int, size uint32, device uint64, indices ...uint16) {
104+ id := uint32(s.id)
105+ c.q.post(&event{kind: evFeedbackTable, id: id, fd: int32(fd), flags: size})
106+ c.q.post(&event{kind: evFeedbackMainDevice, id: id, dev: device})
107+ e := event{kind: evFeedbackFormats, id: id}
108+ for _, i := range indices {
109+ binary.NativeEndian.PutUint16(e.name[e.nameLen:], i)
110+ e.nameLen += 2
111+ }
112+ c.q.post(&e)
113+ c.q.post(&event{kind: evFeedbackDone, id: id})
114+}
115+
116+func newOwnerConn() (*Conn, *Surface) {
117+ c := &Conn{q: newQueue(false), surfaces: map[SurfaceID]*Surface{}, watched: map[int32]uint64{}, efd: -1, epfd: -1}
118+ s := presentable(nil, 1)
119+ c.surfaces[s.id], s.c, s.fbDone = s, c, false
120+ return c, s
121+}
122+
123+func TestFeedbackParsedOnOwner(t *testing.T) {
124+ c, s := newOwnerConn()
125+ table := []Format{{0x34325258, 0}, {0x34325241, 7}, {0x34325258, 1 << 40}}
126+ postRound(c, s, tableFD(t, table), 16*3, 0xe280, 2, 0)
127+ h := NewMockHandler(t)
128+ h.EXPECT().FeedbackDone(s.id).Return().Once()
129+ require.NoError(t, c.Dispatch(h))
130+ fb := s.Feedback()
131+ require.NotNil(t, fb)
132+ require.Equal(t, uint64(0xe280), fb.MainDevice)
133+ require.Equal(t, []Format{table[2], table[0]}, fb.Formats, "tranche order")
134+
135+ // A second round replaces the first without leaking the first one's formats.
136+ h.EXPECT().FeedbackDone(s.id).Return().Once()
137+ postRound(c, s, tableFD(t, table), 16*3, 0xe281, 1)
138+ require.NoError(t, c.Dispatch(h))
139+ require.Equal(t, &Feedback{MainDevice: 0xe281, Formats: []Format{table[1]}}, s.Feedback())
140+}
141+
142+func TestFeedbackBadIndexAndSize(t *testing.T) {
143+ c, s := newOwnerConn()
144+ postRound(c, s, tableFD(t, []Format{{1, 1}}), 16, 5, 3)
145+ postRound(c, s, tableFD(t, nil), 15, 5)
146+ h := NewMockHandler(t)
147+ h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "out of range") })).Return().Once()
148+ h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "invalid dmabuf format table size") })).Return().Once()
149+ h.EXPECT().FeedbackDone(s.id).Return().Twice()
150+ require.NoError(t, c.Dispatch(h))
151+}
152+
153+func fdOpen(fd int) bool { _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); return err == nil }
154+
155+func TestCloseClosesUndrainedFeedbackFDs(t *testing.T) {
156+ c, s := newOwnerConn()
157+ ringed := tableFD(t, nil)
158+ c.q.post(&event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(ringed)})
159+ held := tableFD(t, nil)
160+ c.q.pausing = true
161+ c.q.post(&event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(held)}) // parked in held
162+ require.True(t, fdOpen(ringed))
163+ require.True(t, fdOpen(held))
164+ c.q.closeFDs()
165+ require.False(t, fdOpen(ringed), "ring event's descriptor")
166+ require.False(t, fdOpen(held), "parked event's descriptor")
167+
168+ // An event that is drained owns its descriptor no more: closeFDs must not
169+ // touch a number the process may have reused.
170+ drained := tableFD(t, nil)
171+ c.q.post(&event{kind: evFeedbackTable, id: 99, fd: int32(drained)}) // pausing: held
172+ c.q.pausing = false
173+ c.q.endPause()
174+ var ev event
175+ for c.q.pop(&ev) { // drain everything: the earlier events and this one
176+ }
177+ c.q.closeFDs()
178+ require.True(t, fdOpen(drained))
179+ _ = unix.Close(drained)
180+}
181+
182+func TestEventForClosedSurfaceDropsFD(t *testing.T) {
183+ c, s := newOwnerConn()
184+ fd := tableFD(t, nil)
185+ s.closed = true
186+ c.q.post(&event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(fd)})
187+ require.NoError(t, c.Dispatch(nil))
188+ require.False(t, fdOpen(fd))
189+}
190+
191+// GO-001: the descriptor is consumed by readTable; the event must not close
192+// the number a second time, which by then may belong to someone else.
193+func TestFeedbackTableFDNotClosedTwice(t *testing.T) {
194+ c, s := newOwnerConn()
195+ fd := tableFD(t, []Format{{1, 1}})
196+ ev := event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(fd), flags: 16}
197+ c.applySurface(&ev, NewMockHandler(t))
198+ require.False(t, fdOpen(fd), "readTable consumed it")
199+ require.Equal(t, int32(-1), ev.fd, "ownership left the event")
200+ other := tableFD(t, nil) // the kernel hands out the freed number again
201+ require.Equal(t, fd, other)
202+ ev.closeFD() // what a second close of the event would do
203+ require.True(t, fdOpen(other), "an unrelated descriptor that reuses the number stays open")
204+ _ = unix.Close(other)
205+}
206+
207+func TestLockRules(t *testing.T) {
208+ c := &Conn{q: newQueue(false), surfaces: map[SurfaceID]*Surface{}}
209+ l := &Lock{c: c, gen: 1}
210+ c.curLock = l
211+ require.ErrorContains(t, l.Unlock(), "before the locked event")
212+ c.q.post(&event{kind: evLocked, id: 2}) // another generation: ignored
213+ require.NoError(t, c.Dispatch(nil))
214+ require.False(t, l.Locked())
215+ c.q.post(&event{kind: evLocked, id: 1})
216+ require.NoError(t, c.Dispatch(nil))
217+ require.True(t, l.Locked())
218+ require.ErrorContains(t, l.Close(), "use Unlock")
219+ c.q.post(&event{kind: evLockFinished, id: 1})
220+ require.NoError(t, c.Dispatch(nil))
221+ require.False(t, l.Locked())
222+ require.ErrorContains(t, l.Unlock(), "before the locked event")
223+}
224+
225+// GO-004: the buffer scale returns to 1 when the output scale drops, with no
226+// viewport to carry the size.
227+func TestPresentResetsBufferScale(t *testing.T) {
228+ m := newMocksurfaceRequests(t)
229+ s := presentable(m, 2)
230+ s.viewport = nil
231+ s.buffers[1] = importedBuffer{w: 200, h: 100}
232+ m.EXPECT().setBufferScale(int32(2)).Return(nil).Once()
233+ m.EXPECT().setBufferScale(int32(1)).Return(nil).Once()
234+ m.EXPECT().setAcquire(mock.Anything, mock.Anything).Return(nil).Twice()
235+ m.EXPECT().setRelease(mock.Anything, mock.Anything).Return(nil).Twice()
236+ m.EXPECT().attach(uint64(1)).Return(nil).Twice()
237+ m.EXPECT().damageBuffer(mock.Anything).Return(nil).Twice()
238+ m.EXPECT().frame().Return(nil).Twice()
239+ m.EXPECT().commit().Return(nil).Twice()
240+ p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2}
241+ require.NoError(t, s.Present(p))
242+ s.scale, s.frameReady = 1, true
243+ s.buffers[1] = importedBuffer{w: 100, h: 50}
244+ require.NoError(t, s.Present(p))
245+}
246+
247+// GO-005: size mismatches, timeline point order and a nil request are refused
248+// before anything is sent (the mock is strict).
249+func TestPresentRefusesInconsistentRequests(t *testing.T) {
250+ m := newMocksurfaceRequests(t)
251+ good := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2}
252+ for name, tc := range map[string]struct {
253+ mutate func(*Surface)
254+ p *Present
255+ }{
256+ "nil": {func(*Surface) {}, nil},
257+ "mismatch no viewport": {func(s *Surface) { s.viewport = nil; s.buffers[1] = importedBuffer{w: 99, h: 50} }, good},
258+ "mismatch lock role": {func(s *Surface) { s.role = roleLock; s.buffers[1] = importedBuffer{w: 100, h: 49} }, good},
259+ "same timeline acquire not before release": {func(*Surface) {},
260+ &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 1, AcquirePoint: 5, ReleasePoint: 5}},
261+ } {
262+ s := presentable(m, 1)
263+ tc.mutate(s)
264+ require.Error(t, s.Present(tc.p), name)
265+ }
266+ // With a viewport a different buffer size is legal (scaled by the compositor).
267+ s := presentable(m, 1)
268+ s.buffers[1] = importedBuffer{w: 50, h: 25}
269+ m.EXPECT().setDestination(int32(100), int32(50)).Return(nil).Once()
270+ m.EXPECT().setAcquire(mock.Anything, mock.Anything).Return(nil).Once()
271+ m.EXPECT().setRelease(mock.Anything, mock.Anything).Return(nil).Once()
272+ m.EXPECT().attach(uint64(1)).Return(nil).Once()
273+ m.EXPECT().damageBuffer(mock.Anything).Return(nil).Once()
274+ m.EXPECT().frame().Return(nil).Once()
275+ m.EXPECT().commit().Return(nil).Once()
276+ require.NoError(t, s.Present(good))
277+}
278+
279+// GO-008: lock and layer configure sizes share the bounds check.
280+func TestConfigureEventBounds(t *testing.T) {
281+ for _, kind := range []eventKind{evLayerConfigure, evLockConfigure} {
282+ ev := configureEvent(kind, 3, 9, maxLayerDimension, 1)
283+ require.Equal(t, event{kind: kind, id: 3, serial: 9, a: maxLayerDimension, b: 1}, ev)
284+ require.Equal(t, evBadEvent, configureEvent(kind, 3, 9, maxLayerDimension+1, 1).kind)
285+ require.Equal(t, evBadEvent, configureEvent(kind, 3, 9, 1, 1<<31).kind, "no wrap to a negative int32")
286+ }
287+ c, _ := newOwnerConn()
288+ c.q.post(&event{kind: evBadEvent, id: 1, a: badConfigure})
289+ h := NewMockHandler(t)
290+ h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "configure size") })).Return().Once()
291+ require.NoError(t, c.Dispatch(h))
292+}
@@ -0,0 +1,292 @@
1+package neferclient
2+
3+import (
4+ "encoding/binary"
5+ "strings"
6+ "testing"
7+
8+ "github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj"
9+ "github.com/bnema/go-wayland-bindings/client/viewporter"
10+ "github.com/stretchr/testify/mock"
11+ "github.com/stretchr/testify/require"
12+ "golang.org/x/sys/unix"
13+)
14+
15+// presentable returns a configured surface that sends through req.
16+func presentable(req surfaceRequests, scale float64) *Surface {
17+ return &Surface{id: 1, req: req, width: 100, height: 50, scale: scale, sentBufScale: 1, role: roleToplevel,
18+ configured: true, frameReady: true, fbDone: true,
19+ buffers: map[uint64]importedBuffer{1: {w: 100, h: 50}},
20+ timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{1: nil, 2: nil},
21+ viewport: &viewporter.WpViewport{}}
22+}
23+
24+func TestPresentOrdering(t *testing.T) {
25+ m := newMocksurfaceRequests(t)
26+ s := presentable(m, 1.5)
27+ // NotBefore makes testify fail a call that arrives before its predecessor.
28+ dest := m.EXPECT().setDestination(int32(100), int32(50)).Return(nil).Once()
29+ opaque := m.EXPECT().setOpaqueRegion(int32(100), int32(50), true).Return(nil).Once().NotBefore(dest)
30+ acquire := m.EXPECT().setAcquire(uint64(1), uint64(7)).Return(nil).Once().NotBefore(opaque)
31+ release := m.EXPECT().setRelease(uint64(2), uint64(8)).Return(nil).Once().NotBefore(acquire)
32+ attach := m.EXPECT().attach(uint64(1)).Return(nil).Once().NotBefore(release)
33+ damage := m.EXPECT().damageBuffer(Rect{1, 2, 3, 4}).Return(nil).Once().NotBefore(attach)
34+ frame := m.EXPECT().frame().Return(nil).Once().NotBefore(damage)
35+ m.EXPECT().commit().Return(nil).Once().NotBefore(frame)
36+
37+ p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2, AcquirePoint: 7, ReleasePoint: 8,
38+ Damage: []Rect{{1, 2, 3, 4}}, Opaque: true}
39+ require.NoError(t, s.Present(p))
40+ require.False(t, s.frameReady)
41+ require.ErrorContains(t, s.Present(p), "frame callback pending") // no further request: strict mock
42+}
43+
44+func TestPresentSkipsUnchangedState(t *testing.T) {
45+ m := newMocksurfaceRequests(t)
46+ s := presentable(m, 2)
47+ m.EXPECT().setDestination(int32(100), int32(50)).Return(nil).Once()
48+ m.EXPECT().setOpaqueRegion(int32(100), int32(50), true).Return(nil).Once()
49+ m.EXPECT().setAcquire(mock.Anything, mock.Anything).Return(nil).Twice()
50+ m.EXPECT().setRelease(mock.Anything, mock.Anything).Return(nil).Twice()
51+ m.EXPECT().attach(uint64(1)).Return(nil).Twice()
52+ m.EXPECT().damageBuffer(Rect{0, 0, 200, 100}).Return(nil).Twice() // no damage: whole physical buffer
53+ m.EXPECT().frame().Return(nil).Twice()
54+ m.EXPECT().commit().Return(nil).Twice()
55+ p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2, Opaque: true}
56+ require.NoError(t, s.Present(p))
57+ s.frameReady = true
58+ require.NoError(t, s.Present(p)) // destination and opaque region are not resent
59+}
60+
61+func TestPresentRefusals(t *testing.T) {
62+ m := newMocksurfaceRequests(t) // strict: a refused Present sends nothing
63+ p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2}
64+ for name, mutate := range map[string]func(*Surface){
65+ "before configure": func(s *Surface) { s.configured = false },
66+ "before feedback": func(s *Surface) { s.fbDone = false },
67+ "unknown buffer": func(s *Surface) { delete(s.buffers, 1) },
68+ "unknown timeline": func(s *Surface) { delete(s.timelines, 2) },
69+ "closed": func(s *Surface) { s.closed = true },
70+ "bad geometry": func(s *Surface) { s.scale = 0 },
71+ "fractional without viewporter": func(s *Surface) {
72+ s.scale, s.viewport = 1.5, nil
73+ },
74+ } {
75+ s := presentable(m, 1)
76+ mutate(s)
77+ require.Error(t, s.Present(p), name)
78+ }
79+}
80+
81+func TestPhysicalSize(t *testing.T) {
82+ s := presentable(nil, 1.25)
83+ w, h, err := s.PhysicalSize()
84+ require.NoError(t, err)
85+ require.Equal(t, [2]int32{125, 63}, [2]int32{w, h}) // 62.5 rounds up
86+}
87+
88+func tableFD(t *testing.T, entries []Format) int {
89+ t.Helper()
90+ fd, err := unix.MemfdCreate("table", unix.MFD_CLOEXEC)
91+ require.NoError(t, err)
92+ var b []byte
93+ for _, e := range entries {
94+ b = binary.NativeEndian.AppendUint32(b, e.FourCC)
95+ b = binary.NativeEndian.AppendUint32(b, 0)
96+ b = binary.NativeEndian.AppendUint64(b, e.Modifier)
97+ }
98+ _, err = unix.Write(fd, b)
99+ require.NoError(t, err)
100+ return fd
101+}
102+
103+func postRound(c *Conn, s *Surface, fd int, size uint32, device uint64, indices ...uint16) {
104+ id := uint32(s.id)
105+ c.q.post(&event{kind: evFeedbackTable, id: id, fd: int32(fd), flags: size})
106+ c.q.post(&event{kind: evFeedbackMainDevice, id: id, dev: device})
107+ e := event{kind: evFeedbackFormats, id: id}
108+ for _, i := range indices {
109+ binary.NativeEndian.PutUint16(e.name[e.nameLen:], i)
110+ e.nameLen += 2
111+ }
112+ c.q.post(&e)
113+ c.q.post(&event{kind: evFeedbackDone, id: id})
114+}
115+
116+func newOwnerConn() (*Conn, *Surface) {
117+ c := &Conn{q: newQueue(false), surfaces: map[SurfaceID]*Surface{}, watched: map[int32]uint64{}, efd: -1, epfd: -1}
118+ s := presentable(nil, 1)
119+ c.surfaces[s.id], s.c, s.fbDone = s, c, false
120+ return c, s
121+}
122+
123+func TestFeedbackParsedOnOwner(t *testing.T) {
124+ c, s := newOwnerConn()
125+ table := []Format{{0x34325258, 0}, {0x34325241, 7}, {0x34325258, 1 << 40}}
126+ postRound(c, s, tableFD(t, table), 16*3, 0xe280, 2, 0)
127+ h := NewMockHandler(t)
128+ h.EXPECT().FeedbackDone(s.id).Return().Once()
129+ require.NoError(t, c.Dispatch(h))
130+ fb := s.Feedback()
131+ require.NotNil(t, fb)
132+ require.Equal(t, uint64(0xe280), fb.MainDevice)
133+ require.Equal(t, []Format{table[2], table[0]}, fb.Formats, "tranche order")
134+
135+ // A second round replaces the first without leaking the first one's formats.
136+ h.EXPECT().FeedbackDone(s.id).Return().Once()
137+ postRound(c, s, tableFD(t, table), 16*3, 0xe281, 1)
138+ require.NoError(t, c.Dispatch(h))
139+ require.Equal(t, &Feedback{MainDevice: 0xe281, Formats: []Format{table[1]}}, s.Feedback())
140+}
141+
142+func TestFeedbackBadIndexAndSize(t *testing.T) {
143+ c, s := newOwnerConn()
144+ postRound(c, s, tableFD(t, []Format{{1, 1}}), 16, 5, 3)
145+ postRound(c, s, tableFD(t, nil), 15, 5)
146+ h := NewMockHandler(t)
147+ h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "out of range") })).Return().Once()
148+ h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "invalid dmabuf format table size") })).Return().Once()
149+ h.EXPECT().FeedbackDone(s.id).Return().Twice()
150+ require.NoError(t, c.Dispatch(h))
151+}
152+
153+func fdOpen(fd int) bool { _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); return err == nil }
154+
155+func TestCloseClosesUndrainedFeedbackFDs(t *testing.T) {
156+ c, s := newOwnerConn()
157+ ringed := tableFD(t, nil)
158+ c.q.post(&event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(ringed)})
159+ held := tableFD(t, nil)
160+ c.q.pausing = true
161+ c.q.post(&event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(held)}) // parked in held
162+ require.True(t, fdOpen(ringed))
163+ require.True(t, fdOpen(held))
164+ c.q.closeFDs()
165+ require.False(t, fdOpen(ringed), "ring event's descriptor")
166+ require.False(t, fdOpen(held), "parked event's descriptor")
167+
168+ // An event that is drained owns its descriptor no more: closeFDs must not
169+ // touch a number the process may have reused.
170+ drained := tableFD(t, nil)
171+ c.q.post(&event{kind: evFeedbackTable, id: 99, fd: int32(drained)}) // pausing: held
172+ c.q.pausing = false
173+ c.q.endPause()
174+ var ev event
175+ for c.q.pop(&ev) { // drain everything: the earlier events and this one
176+ }
177+ c.q.closeFDs()
178+ require.True(t, fdOpen(drained))
179+ _ = unix.Close(drained)
180+}
181+
182+func TestEventForClosedSurfaceDropsFD(t *testing.T) {
183+ c, s := newOwnerConn()
184+ fd := tableFD(t, nil)
185+ s.closed = true
186+ c.q.post(&event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(fd)})
187+ require.NoError(t, c.Dispatch(nil))
188+ require.False(t, fdOpen(fd))
189+}
190+
191+// GO-001: the descriptor is consumed by readTable; the event must not close
192+// the number a second time, which by then may belong to someone else.
193+func TestFeedbackTableFDNotClosedTwice(t *testing.T) {
194+ c, s := newOwnerConn()
195+ fd := tableFD(t, []Format{{1, 1}})
196+ ev := event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(fd), flags: 16}
197+ c.applySurface(&ev, NewMockHandler(t))
198+ require.False(t, fdOpen(fd), "readTable consumed it")
199+ require.Equal(t, int32(-1), ev.fd, "ownership left the event")
200+ other := tableFD(t, nil) // the kernel hands out the freed number again
201+ require.Equal(t, fd, other)
202+ ev.closeFD() // what a second close of the event would do
203+ require.True(t, fdOpen(other), "an unrelated descriptor that reuses the number stays open")
204+ _ = unix.Close(other)
205+}
206+
207+func TestLockRules(t *testing.T) {
208+ c := &Conn{q: newQueue(false), surfaces: map[SurfaceID]*Surface{}}
209+ l := &Lock{c: c, gen: 1}
210+ c.curLock = l
211+ require.ErrorContains(t, l.Unlock(), "before the locked event")
212+ c.q.post(&event{kind: evLocked, id: 2}) // another generation: ignored
213+ require.NoError(t, c.Dispatch(nil))
214+ require.False(t, l.Locked())
215+ c.q.post(&event{kind: evLocked, id: 1})
216+ require.NoError(t, c.Dispatch(nil))
217+ require.True(t, l.Locked())
218+ require.ErrorContains(t, l.Close(), "use Unlock")
219+ c.q.post(&event{kind: evLockFinished, id: 1})
220+ require.NoError(t, c.Dispatch(nil))
221+ require.False(t, l.Locked())
222+ require.ErrorContains(t, l.Unlock(), "before the locked event")
223+}
224+
225+// GO-004: the buffer scale returns to 1 when the output scale drops, with no
226+// viewport to carry the size.
227+func TestPresentResetsBufferScale(t *testing.T) {
228+ m := newMocksurfaceRequests(t)
229+ s := presentable(m, 2)
230+ s.viewport = nil
231+ s.buffers[1] = importedBuffer{w: 200, h: 100}
232+ m.EXPECT().setBufferScale(int32(2)).Return(nil).Once()
233+ m.EXPECT().setBufferScale(int32(1)).Return(nil).Once()
234+ m.EXPECT().setAcquire(mock.Anything, mock.Anything).Return(nil).Twice()
235+ m.EXPECT().setRelease(mock.Anything, mock.Anything).Return(nil).Twice()
236+ m.EXPECT().attach(uint64(1)).Return(nil).Twice()
237+ m.EXPECT().damageBuffer(mock.Anything).Return(nil).Twice()
238+ m.EXPECT().frame().Return(nil).Twice()
239+ m.EXPECT().commit().Return(nil).Twice()
240+ p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2}
241+ require.NoError(t, s.Present(p))
242+ s.scale, s.frameReady = 1, true
243+ s.buffers[1] = importedBuffer{w: 100, h: 50}
244+ require.NoError(t, s.Present(p))
245+}
246+
247+// GO-005: size mismatches, timeline point order and a nil request are refused
248+// before anything is sent (the mock is strict).
249+func TestPresentRefusesInconsistentRequests(t *testing.T) {
250+ m := newMocksurfaceRequests(t)
251+ good := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2}
252+ for name, tc := range map[string]struct {
253+ mutate func(*Surface)
254+ p *Present
255+ }{
256+ "nil": {func(*Surface) {}, nil},
257+ "mismatch no viewport": {func(s *Surface) { s.viewport = nil; s.buffers[1] = importedBuffer{w: 99, h: 50} }, good},
258+ "mismatch lock role": {func(s *Surface) { s.role = roleLock; s.buffers[1] = importedBuffer{w: 100, h: 49} }, good},
259+ "same timeline acquire not before release": {func(*Surface) {},
260+ &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 1, AcquirePoint: 5, ReleasePoint: 5}},
261+ } {
262+ s := presentable(m, 1)
263+ tc.mutate(s)
264+ require.Error(t, s.Present(tc.p), name)
265+ }
266+ // With a viewport a different buffer size is legal (scaled by the compositor).
267+ s := presentable(m, 1)
268+ s.buffers[1] = importedBuffer{w: 50, h: 25}
269+ m.EXPECT().setDestination(int32(100), int32(50)).Return(nil).Once()
270+ m.EXPECT().setAcquire(mock.Anything, mock.Anything).Return(nil).Once()
271+ m.EXPECT().setRelease(mock.Anything, mock.Anything).Return(nil).Once()
272+ m.EXPECT().attach(uint64(1)).Return(nil).Once()
273+ m.EXPECT().damageBuffer(mock.Anything).Return(nil).Once()
274+ m.EXPECT().frame().Return(nil).Once()
275+ m.EXPECT().commit().Return(nil).Once()
276+ require.NoError(t, s.Present(good))
277+}
278+
279+// GO-008: lock and layer configure sizes share the bounds check.
280+func TestConfigureEventBounds(t *testing.T) {
281+ for _, kind := range []eventKind{evLayerConfigure, evLockConfigure} {
282+ ev := configureEvent(kind, 3, 9, maxLayerDimension, 1)
283+ require.Equal(t, event{kind: kind, id: 3, serial: 9, a: maxLayerDimension, b: 1}, ev)
284+ require.Equal(t, evBadEvent, configureEvent(kind, 3, 9, maxLayerDimension+1, 1).kind)
285+ require.Equal(t, evBadEvent, configureEvent(kind, 3, 9, 1, 1<<31).kind, "no wrap to a negative int32")
286+ }
287+ c, _ := newOwnerConn()
288+ c.q.post(&event{kind: evBadEvent, id: 1, a: badConfigure})
289+ h := NewMockHandler(t)
290+ h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "configure size") })).Return().Once()
291+ require.NoError(t, c.Dispatch(h))
292+}
A surface_mock_test.go +520 -0
@@ -0,0 +1,520 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package neferclient
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMocksurfaceRequests creates a new instance of mocksurfaceRequests. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMocksurfaceRequests(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mocksurfaceRequests {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &mocksurfaceRequests{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// mocksurfaceRequests is an autogenerated mock type for the surfaceRequests type
35+type mocksurfaceRequests struct {
36+ mock.Mock
37+}
38+
39+type mocksurfaceRequests_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *mocksurfaceRequests) EXPECT() *mocksurfaceRequests_Expecter {
44+ return &mocksurfaceRequests_Expecter{mock: &_m.Mock}
45+}
46+
47+// attach provides a mock function for the type mocksurfaceRequests
48+func (_mock *mocksurfaceRequests) attach(buffer uint64) error {
49+ ret := _mock.Called(buffer)
50+
51+ if len(ret) == 0 {
52+ panic("no return value specified for attach")
53+ }
54+
55+ var r0 error
56+ if returnFunc, ok := ret.Get(0).(func(uint64) error); ok {
57+ r0 = returnFunc(buffer)
58+ } else {
59+ r0 = ret.Error(0)
60+ }
61+ return r0
62+}
63+
64+// mocksurfaceRequests_attach_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'attach'
65+type mocksurfaceRequests_attach_Call struct {
66+ *mock.Call
67+}
68+
69+// attach is a helper method to define mock.On call
70+// - buffer uint64
71+func (_e *mocksurfaceRequests_Expecter) attach(buffer any) *mocksurfaceRequests_attach_Call {
72+ return &mocksurfaceRequests_attach_Call{Call: _e.mock.On("attach", buffer)}
73+}
74+
75+func (_c *mocksurfaceRequests_attach_Call) Run(run func(buffer uint64)) *mocksurfaceRequests_attach_Call {
76+ _c.Call.Run(func(args mock.Arguments) {
77+ var arg0 uint64
78+ if args[0] != nil {
79+ arg0 = args[0].(uint64)
80+ }
81+ run(
82+ arg0,
83+ )
84+ })
85+ return _c
86+}
87+
88+func (_c *mocksurfaceRequests_attach_Call) Return(err error) *mocksurfaceRequests_attach_Call {
89+ _c.Call.Return(err)
90+ return _c
91+}
92+
93+func (_c *mocksurfaceRequests_attach_Call) RunAndReturn(run func(buffer uint64) error) *mocksurfaceRequests_attach_Call {
94+ _c.Call.Return(run)
95+ return _c
96+}
97+
98+// commit provides a mock function for the type mocksurfaceRequests
99+func (_mock *mocksurfaceRequests) commit() error {
100+ ret := _mock.Called()
101+
102+ if len(ret) == 0 {
103+ panic("no return value specified for commit")
104+ }
105+
106+ var r0 error
107+ if returnFunc, ok := ret.Get(0).(func() error); ok {
108+ r0 = returnFunc()
109+ } else {
110+ r0 = ret.Error(0)
111+ }
112+ return r0
113+}
114+
115+// mocksurfaceRequests_commit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'commit'
116+type mocksurfaceRequests_commit_Call struct {
117+ *mock.Call
118+}
119+
120+// commit is a helper method to define mock.On call
121+func (_e *mocksurfaceRequests_Expecter) commit() *mocksurfaceRequests_commit_Call {
122+ return &mocksurfaceRequests_commit_Call{Call: _e.mock.On("commit")}
123+}
124+
125+func (_c *mocksurfaceRequests_commit_Call) Run(run func()) *mocksurfaceRequests_commit_Call {
126+ _c.Call.Run(func(args mock.Arguments) {
127+ run()
128+ })
129+ return _c
130+}
131+
132+func (_c *mocksurfaceRequests_commit_Call) Return(err error) *mocksurfaceRequests_commit_Call {
133+ _c.Call.Return(err)
134+ return _c
135+}
136+
137+func (_c *mocksurfaceRequests_commit_Call) RunAndReturn(run func() error) *mocksurfaceRequests_commit_Call {
138+ _c.Call.Return(run)
139+ return _c
140+}
141+
142+// damageBuffer provides a mock function for the type mocksurfaceRequests
143+func (_mock *mocksurfaceRequests) damageBuffer(r Rect) error {
144+ ret := _mock.Called(r)
145+
146+ if len(ret) == 0 {
147+ panic("no return value specified for damageBuffer")
148+ }
149+
150+ var r0 error
151+ if returnFunc, ok := ret.Get(0).(func(Rect) error); ok {
152+ r0 = returnFunc(r)
153+ } else {
154+ r0 = ret.Error(0)
155+ }
156+ return r0
157+}
158+
159+// mocksurfaceRequests_damageBuffer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'damageBuffer'
160+type mocksurfaceRequests_damageBuffer_Call struct {
161+ *mock.Call
162+}
163+
164+// damageBuffer is a helper method to define mock.On call
165+// - r Rect
166+func (_e *mocksurfaceRequests_Expecter) damageBuffer(r any) *mocksurfaceRequests_damageBuffer_Call {
167+ return &mocksurfaceRequests_damageBuffer_Call{Call: _e.mock.On("damageBuffer", r)}
168+}
169+
170+func (_c *mocksurfaceRequests_damageBuffer_Call) Run(run func(r Rect)) *mocksurfaceRequests_damageBuffer_Call {
171+ _c.Call.Run(func(args mock.Arguments) {
172+ var arg0 Rect
173+ if args[0] != nil {
174+ arg0 = args[0].(Rect)
175+ }
176+ run(
177+ arg0,
178+ )
179+ })
180+ return _c
181+}
182+
183+func (_c *mocksurfaceRequests_damageBuffer_Call) Return(err error) *mocksurfaceRequests_damageBuffer_Call {
184+ _c.Call.Return(err)
185+ return _c
186+}
187+
188+func (_c *mocksurfaceRequests_damageBuffer_Call) RunAndReturn(run func(r Rect) error) *mocksurfaceRequests_damageBuffer_Call {
189+ _c.Call.Return(run)
190+ return _c
191+}
192+
193+// frame provides a mock function for the type mocksurfaceRequests
194+func (_mock *mocksurfaceRequests) frame() error {
195+ ret := _mock.Called()
196+
197+ if len(ret) == 0 {
198+ panic("no return value specified for frame")
199+ }
200+
201+ var r0 error
202+ if returnFunc, ok := ret.Get(0).(func() error); ok {
203+ r0 = returnFunc()
204+ } else {
205+ r0 = ret.Error(0)
206+ }
207+ return r0
208+}
209+
210+// mocksurfaceRequests_frame_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'frame'
211+type mocksurfaceRequests_frame_Call struct {
212+ *mock.Call
213+}
214+
215+// frame is a helper method to define mock.On call
216+func (_e *mocksurfaceRequests_Expecter) frame() *mocksurfaceRequests_frame_Call {
217+ return &mocksurfaceRequests_frame_Call{Call: _e.mock.On("frame")}
218+}
219+
220+func (_c *mocksurfaceRequests_frame_Call) Run(run func()) *mocksurfaceRequests_frame_Call {
221+ _c.Call.Run(func(args mock.Arguments) {
222+ run()
223+ })
224+ return _c
225+}
226+
227+func (_c *mocksurfaceRequests_frame_Call) Return(err error) *mocksurfaceRequests_frame_Call {
228+ _c.Call.Return(err)
229+ return _c
230+}
231+
232+func (_c *mocksurfaceRequests_frame_Call) RunAndReturn(run func() error) *mocksurfaceRequests_frame_Call {
233+ _c.Call.Return(run)
234+ return _c
235+}
236+
237+// setAcquire provides a mock function for the type mocksurfaceRequests
238+func (_mock *mocksurfaceRequests) setAcquire(timeline uint64, point uint64) error {
239+ ret := _mock.Called(timeline, point)
240+
241+ if len(ret) == 0 {
242+ panic("no return value specified for setAcquire")
243+ }
244+
245+ var r0 error
246+ if returnFunc, ok := ret.Get(0).(func(uint64, uint64) error); ok {
247+ r0 = returnFunc(timeline, point)
248+ } else {
249+ r0 = ret.Error(0)
250+ }
251+ return r0
252+}
253+
254+// mocksurfaceRequests_setAcquire_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'setAcquire'
255+type mocksurfaceRequests_setAcquire_Call struct {
256+ *mock.Call
257+}
258+
259+// setAcquire is a helper method to define mock.On call
260+// - timeline uint64
261+// - point uint64
262+func (_e *mocksurfaceRequests_Expecter) setAcquire(timeline any, point any) *mocksurfaceRequests_setAcquire_Call {
263+ return &mocksurfaceRequests_setAcquire_Call{Call: _e.mock.On("setAcquire", timeline, point)}
264+}
265+
266+func (_c *mocksurfaceRequests_setAcquire_Call) Run(run func(timeline uint64, point uint64)) *mocksurfaceRequests_setAcquire_Call {
267+ _c.Call.Run(func(args mock.Arguments) {
268+ var arg0 uint64
269+ if args[0] != nil {
270+ arg0 = args[0].(uint64)
271+ }
272+ var arg1 uint64
273+ if args[1] != nil {
274+ arg1 = args[1].(uint64)
275+ }
276+ run(
277+ arg0,
278+ arg1,
279+ )
280+ })
281+ return _c
282+}
283+
284+func (_c *mocksurfaceRequests_setAcquire_Call) Return(err error) *mocksurfaceRequests_setAcquire_Call {
285+ _c.Call.Return(err)
286+ return _c
287+}
288+
289+func (_c *mocksurfaceRequests_setAcquire_Call) RunAndReturn(run func(timeline uint64, point uint64) error) *mocksurfaceRequests_setAcquire_Call {
290+ _c.Call.Return(run)
291+ return _c
292+}
293+
294+// setBufferScale provides a mock function for the type mocksurfaceRequests
295+func (_mock *mocksurfaceRequests) setBufferScale(n int32) error {
296+ ret := _mock.Called(n)
297+
298+ if len(ret) == 0 {
299+ panic("no return value specified for setBufferScale")
300+ }
301+
302+ var r0 error
303+ if returnFunc, ok := ret.Get(0).(func(int32) error); ok {
304+ r0 = returnFunc(n)
305+ } else {
306+ r0 = ret.Error(0)
307+ }
308+ return r0
309+}
310+
311+// mocksurfaceRequests_setBufferScale_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'setBufferScale'
312+type mocksurfaceRequests_setBufferScale_Call struct {
313+ *mock.Call
314+}
315+
316+// setBufferScale is a helper method to define mock.On call
317+// - n int32
318+func (_e *mocksurfaceRequests_Expecter) setBufferScale(n any) *mocksurfaceRequests_setBufferScale_Call {
319+ return &mocksurfaceRequests_setBufferScale_Call{Call: _e.mock.On("setBufferScale", n)}
320+}
321+
322+func (_c *mocksurfaceRequests_setBufferScale_Call) Run(run func(n int32)) *mocksurfaceRequests_setBufferScale_Call {
323+ _c.Call.Run(func(args mock.Arguments) {
324+ var arg0 int32
325+ if args[0] != nil {
326+ arg0 = args[0].(int32)
327+ }
328+ run(
329+ arg0,
330+ )
331+ })
332+ return _c
333+}
334+
335+func (_c *mocksurfaceRequests_setBufferScale_Call) Return(err error) *mocksurfaceRequests_setBufferScale_Call {
336+ _c.Call.Return(err)
337+ return _c
338+}
339+
340+func (_c *mocksurfaceRequests_setBufferScale_Call) RunAndReturn(run func(n int32) error) *mocksurfaceRequests_setBufferScale_Call {
341+ _c.Call.Return(run)
342+ return _c
343+}
344+
345+// setDestination provides a mock function for the type mocksurfaceRequests
346+func (_mock *mocksurfaceRequests) setDestination(w int32, h int32) error {
347+ ret := _mock.Called(w, h)
348+
349+ if len(ret) == 0 {
350+ panic("no return value specified for setDestination")
351+ }
352+
353+ var r0 error
354+ if returnFunc, ok := ret.Get(0).(func(int32, int32) error); ok {
355+ r0 = returnFunc(w, h)
356+ } else {
357+ r0 = ret.Error(0)
358+ }
359+ return r0
360+}
361+
362+// mocksurfaceRequests_setDestination_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'setDestination'
363+type mocksurfaceRequests_setDestination_Call struct {
364+ *mock.Call
365+}
366+
367+// setDestination is a helper method to define mock.On call
368+// - w int32
369+// - h int32
370+func (_e *mocksurfaceRequests_Expecter) setDestination(w any, h any) *mocksurfaceRequests_setDestination_Call {
371+ return &mocksurfaceRequests_setDestination_Call{Call: _e.mock.On("setDestination", w, h)}
372+}
373+
374+func (_c *mocksurfaceRequests_setDestination_Call) Run(run func(w int32, h int32)) *mocksurfaceRequests_setDestination_Call {
375+ _c.Call.Run(func(args mock.Arguments) {
376+ var arg0 int32
377+ if args[0] != nil {
378+ arg0 = args[0].(int32)
379+ }
380+ var arg1 int32
381+ if args[1] != nil {
382+ arg1 = args[1].(int32)
383+ }
384+ run(
385+ arg0,
386+ arg1,
387+ )
388+ })
389+ return _c
390+}
391+
392+func (_c *mocksurfaceRequests_setDestination_Call) Return(err error) *mocksurfaceRequests_setDestination_Call {
393+ _c.Call.Return(err)
394+ return _c
395+}
396+
397+func (_c *mocksurfaceRequests_setDestination_Call) RunAndReturn(run func(w int32, h int32) error) *mocksurfaceRequests_setDestination_Call {
398+ _c.Call.Return(run)
399+ return _c
400+}
401+
402+// setOpaqueRegion provides a mock function for the type mocksurfaceRequests
403+func (_mock *mocksurfaceRequests) setOpaqueRegion(w int32, h int32, opaque bool) error {
404+ ret := _mock.Called(w, h, opaque)
405+
406+ if len(ret) == 0 {
407+ panic("no return value specified for setOpaqueRegion")
408+ }
409+
410+ var r0 error
411+ if returnFunc, ok := ret.Get(0).(func(int32, int32, bool) error); ok {
412+ r0 = returnFunc(w, h, opaque)
413+ } else {
414+ r0 = ret.Error(0)
415+ }
416+ return r0
417+}
418+
419+// mocksurfaceRequests_setOpaqueRegion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'setOpaqueRegion'
420+type mocksurfaceRequests_setOpaqueRegion_Call struct {
421+ *mock.Call
422+}
423+
424+// setOpaqueRegion is a helper method to define mock.On call
425+// - w int32
426+// - h int32
427+// - opaque bool
428+func (_e *mocksurfaceRequests_Expecter) setOpaqueRegion(w any, h any, opaque any) *mocksurfaceRequests_setOpaqueRegion_Call {
429+ return &mocksurfaceRequests_setOpaqueRegion_Call{Call: _e.mock.On("setOpaqueRegion", w, h, opaque)}
430+}
431+
432+func (_c *mocksurfaceRequests_setOpaqueRegion_Call) Run(run func(w int32, h int32, opaque bool)) *mocksurfaceRequests_setOpaqueRegion_Call {
433+ _c.Call.Run(func(args mock.Arguments) {
434+ var arg0 int32
435+ if args[0] != nil {
436+ arg0 = args[0].(int32)
437+ }
438+ var arg1 int32
439+ if args[1] != nil {
440+ arg1 = args[1].(int32)
441+ }
442+ var arg2 bool
443+ if args[2] != nil {
444+ arg2 = args[2].(bool)
445+ }
446+ run(
447+ arg0,
448+ arg1,
449+ arg2,
450+ )
451+ })
452+ return _c
453+}
454+
455+func (_c *mocksurfaceRequests_setOpaqueRegion_Call) Return(err error) *mocksurfaceRequests_setOpaqueRegion_Call {
456+ _c.Call.Return(err)
457+ return _c
458+}
459+
460+func (_c *mocksurfaceRequests_setOpaqueRegion_Call) RunAndReturn(run func(w int32, h int32, opaque bool) error) *mocksurfaceRequests_setOpaqueRegion_Call {
461+ _c.Call.Return(run)
462+ return _c
463+}
464+
465+// setRelease provides a mock function for the type mocksurfaceRequests
466+func (_mock *mocksurfaceRequests) setRelease(timeline uint64, point uint64) error {
467+ ret := _mock.Called(timeline, point)
468+
469+ if len(ret) == 0 {
470+ panic("no return value specified for setRelease")
471+ }
472+
473+ var r0 error
474+ if returnFunc, ok := ret.Get(0).(func(uint64, uint64) error); ok {
475+ r0 = returnFunc(timeline, point)
476+ } else {
477+ r0 = ret.Error(0)
478+ }
479+ return r0
480+}
481+
482+// mocksurfaceRequests_setRelease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'setRelease'
483+type mocksurfaceRequests_setRelease_Call struct {
484+ *mock.Call
485+}
486+
487+// setRelease is a helper method to define mock.On call
488+// - timeline uint64
489+// - point uint64
490+func (_e *mocksurfaceRequests_Expecter) setRelease(timeline any, point any) *mocksurfaceRequests_setRelease_Call {
491+ return &mocksurfaceRequests_setRelease_Call{Call: _e.mock.On("setRelease", timeline, point)}
492+}
493+
494+func (_c *mocksurfaceRequests_setRelease_Call) Run(run func(timeline uint64, point uint64)) *mocksurfaceRequests_setRelease_Call {
495+ _c.Call.Run(func(args mock.Arguments) {
496+ var arg0 uint64
497+ if args[0] != nil {
498+ arg0 = args[0].(uint64)
499+ }
500+ var arg1 uint64
501+ if args[1] != nil {
502+ arg1 = args[1].(uint64)
503+ }
504+ run(
505+ arg0,
506+ arg1,
507+ )
508+ })
509+ return _c
510+}
511+
512+func (_c *mocksurfaceRequests_setRelease_Call) Return(err error) *mocksurfaceRequests_setRelease_Call {
513+ _c.Call.Return(err)
514+ return _c
515+}
516+
517+func (_c *mocksurfaceRequests_setRelease_Call) RunAndReturn(run func(timeline uint64, point uint64) error) *mocksurfaceRequests_setRelease_Call {
518+ _c.Call.Return(run)
519+ return _c
520+}
@@ -0,0 +1,520 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package neferclient
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMocksurfaceRequests creates a new instance of mocksurfaceRequests. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMocksurfaceRequests(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mocksurfaceRequests {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &mocksurfaceRequests{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// mocksurfaceRequests is an autogenerated mock type for the surfaceRequests type
35+type mocksurfaceRequests struct {
36+ mock.Mock
37+}
38+
39+type mocksurfaceRequests_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *mocksurfaceRequests) EXPECT() *mocksurfaceRequests_Expecter {
44+ return &mocksurfaceRequests_Expecter{mock: &_m.Mock}
45+}
46+
47+// attach provides a mock function for the type mocksurfaceRequests
48+func (_mock *mocksurfaceRequests) attach(buffer uint64) error {
49+ ret := _mock.Called(buffer)
50+
51+ if len(ret) == 0 {
52+ panic("no return value specified for attach")
53+ }
54+
55+ var r0 error
56+ if returnFunc, ok := ret.Get(0).(func(uint64) error); ok {
57+ r0 = returnFunc(buffer)
58+ } else {
59+ r0 = ret.Error(0)
60+ }
61+ return r0
62+}
63+
64+// mocksurfaceRequests_attach_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'attach'
65+type mocksurfaceRequests_attach_Call struct {
66+ *mock.Call
67+}
68+
69+// attach is a helper method to define mock.On call
70+// - buffer uint64
71+func (_e *mocksurfaceRequests_Expecter) attach(buffer any) *mocksurfaceRequests_attach_Call {
72+ return &mocksurfaceRequests_attach_Call{Call: _e.mock.On("attach", buffer)}
73+}
74+
75+func (_c *mocksurfaceRequests_attach_Call) Run(run func(buffer uint64)) *mocksurfaceRequests_attach_Call {
76+ _c.Call.Run(func(args mock.Arguments) {
77+ var arg0 uint64
78+ if args[0] != nil {
79+ arg0 = args[0].(uint64)
80+ }
81+ run(
82+ arg0,
83+ )
84+ })
85+ return _c
86+}
87+
88+func (_c *mocksurfaceRequests_attach_Call) Return(err error) *mocksurfaceRequests_attach_Call {
89+ _c.Call.Return(err)
90+ return _c
91+}
92+
93+func (_c *mocksurfaceRequests_attach_Call) RunAndReturn(run func(buffer uint64) error) *mocksurfaceRequests_attach_Call {
94+ _c.Call.Return(run)
95+ return _c
96+}
97+
98+// commit provides a mock function for the type mocksurfaceRequests
99+func (_mock *mocksurfaceRequests) commit() error {
100+ ret := _mock.Called()
101+
102+ if len(ret) == 0 {
103+ panic("no return value specified for commit")
104+ }
105+
106+ var r0 error
107+ if returnFunc, ok := ret.Get(0).(func() error); ok {
108+ r0 = returnFunc()
109+ } else {
110+ r0 = ret.Error(0)
111+ }
112+ return r0
113+}
114+
115+// mocksurfaceRequests_commit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'commit'
116+type mocksurfaceRequests_commit_Call struct {
117+ *mock.Call
118+}
119+
120+// commit is a helper method to define mock.On call
121+func (_e *mocksurfaceRequests_Expecter) commit() *mocksurfaceRequests_commit_Call {
122+ return &mocksurfaceRequests_commit_Call{Call: _e.mock.On("commit")}
123+}
124+
125+func (_c *mocksurfaceRequests_commit_Call) Run(run func()) *mocksurfaceRequests_commit_Call {
126+ _c.Call.Run(func(args mock.Arguments) {
127+ run()
128+ })
129+ return _c
130+}
131+
132+func (_c *mocksurfaceRequests_commit_Call) Return(err error) *mocksurfaceRequests_commit_Call {
133+ _c.Call.Return(err)
134+ return _c
135+}
136+
137+func (_c *mocksurfaceRequests_commit_Call) RunAndReturn(run func() error) *mocksurfaceRequests_commit_Call {
138+ _c.Call.Return(run)
139+ return _c
140+}
141+
142+// damageBuffer provides a mock function for the type mocksurfaceRequests
143+func (_mock *mocksurfaceRequests) damageBuffer(r Rect) error {
144+ ret := _mock.Called(r)
145+
146+ if len(ret) == 0 {
147+ panic("no return value specified for damageBuffer")
148+ }
149+
150+ var r0 error
151+ if returnFunc, ok := ret.Get(0).(func(Rect) error); ok {
152+ r0 = returnFunc(r)
153+ } else {
154+ r0 = ret.Error(0)
155+ }
156+ return r0
157+}
158+
159+// mocksurfaceRequests_damageBuffer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'damageBuffer'
160+type mocksurfaceRequests_damageBuffer_Call struct {
161+ *mock.Call
162+}
163+
164+// damageBuffer is a helper method to define mock.On call
165+// - r Rect
166+func (_e *mocksurfaceRequests_Expecter) damageBuffer(r any) *mocksurfaceRequests_damageBuffer_Call {
167+ return &mocksurfaceRequests_damageBuffer_Call{Call: _e.mock.On("damageBuffer", r)}
168+}
169+
170+func (_c *mocksurfaceRequests_damageBuffer_Call) Run(run func(r Rect)) *mocksurfaceRequests_damageBuffer_Call {
171+ _c.Call.Run(func(args mock.Arguments) {
172+ var arg0 Rect
173+ if args[0] != nil {
174+ arg0 = args[0].(Rect)
175+ }
176+ run(
177+ arg0,
178+ )
179+ })
180+ return _c
181+}
182+
183+func (_c *mocksurfaceRequests_damageBuffer_Call) Return(err error) *mocksurfaceRequests_damageBuffer_Call {
184+ _c.Call.Return(err)
185+ return _c
186+}
187+
188+func (_c *mocksurfaceRequests_damageBuffer_Call) RunAndReturn(run func(r Rect) error) *mocksurfaceRequests_damageBuffer_Call {
189+ _c.Call.Return(run)
190+ return _c
191+}
192+
193+// frame provides a mock function for the type mocksurfaceRequests
194+func (_mock *mocksurfaceRequests) frame() error {
195+ ret := _mock.Called()
196+
197+ if len(ret) == 0 {
198+ panic("no return value specified for frame")
199+ }
200+
201+ var r0 error
202+ if returnFunc, ok := ret.Get(0).(func() error); ok {
203+ r0 = returnFunc()
204+ } else {
205+ r0 = ret.Error(0)
206+ }
207+ return r0
208+}
209+
210+// mocksurfaceRequests_frame_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'frame'
211+type mocksurfaceRequests_frame_Call struct {
212+ *mock.Call
213+}
214+
215+// frame is a helper method to define mock.On call
216+func (_e *mocksurfaceRequests_Expecter) frame() *mocksurfaceRequests_frame_Call {
217+ return &mocksurfaceRequests_frame_Call{Call: _e.mock.On("frame")}
218+}
219+
220+func (_c *mocksurfaceRequests_frame_Call) Run(run func()) *mocksurfaceRequests_frame_Call {
221+ _c.Call.Run(func(args mock.Arguments) {
222+ run()
223+ })
224+ return _c
225+}
226+
227+func (_c *mocksurfaceRequests_frame_Call) Return(err error) *mocksurfaceRequests_frame_Call {
228+ _c.Call.Return(err)
229+ return _c
230+}
231+
232+func (_c *mocksurfaceRequests_frame_Call) RunAndReturn(run func() error) *mocksurfaceRequests_frame_Call {
233+ _c.Call.Return(run)
234+ return _c
235+}
236+
237+// setAcquire provides a mock function for the type mocksurfaceRequests
238+func (_mock *mocksurfaceRequests) setAcquire(timeline uint64, point uint64) error {
239+ ret := _mock.Called(timeline, point)
240+
241+ if len(ret) == 0 {
242+ panic("no return value specified for setAcquire")
243+ }
244+
245+ var r0 error
246+ if returnFunc, ok := ret.Get(0).(func(uint64, uint64) error); ok {
247+ r0 = returnFunc(timeline, point)
248+ } else {
249+ r0 = ret.Error(0)
250+ }
251+ return r0
252+}
253+
254+// mocksurfaceRequests_setAcquire_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'setAcquire'
255+type mocksurfaceRequests_setAcquire_Call struct {
256+ *mock.Call
257+}
258+
259+// setAcquire is a helper method to define mock.On call
260+// - timeline uint64
261+// - point uint64
262+func (_e *mocksurfaceRequests_Expecter) setAcquire(timeline any, point any) *mocksurfaceRequests_setAcquire_Call {
263+ return &mocksurfaceRequests_setAcquire_Call{Call: _e.mock.On("setAcquire", timeline, point)}
264+}
265+
266+func (_c *mocksurfaceRequests_setAcquire_Call) Run(run func(timeline uint64, point uint64)) *mocksurfaceRequests_setAcquire_Call {
267+ _c.Call.Run(func(args mock.Arguments) {
268+ var arg0 uint64
269+ if args[0] != nil {
270+ arg0 = args[0].(uint64)
271+ }
272+ var arg1 uint64
273+ if args[1] != nil {
274+ arg1 = args[1].(uint64)
275+ }
276+ run(
277+ arg0,
278+ arg1,
279+ )
280+ })
281+ return _c
282+}
283+
284+func (_c *mocksurfaceRequests_setAcquire_Call) Return(err error) *mocksurfaceRequests_setAcquire_Call {
285+ _c.Call.Return(err)
286+ return _c
287+}
288+
289+func (_c *mocksurfaceRequests_setAcquire_Call) RunAndReturn(run func(timeline uint64, point uint64) error) *mocksurfaceRequests_setAcquire_Call {
290+ _c.Call.Return(run)
291+ return _c
292+}
293+
294+// setBufferScale provides a mock function for the type mocksurfaceRequests
295+func (_mock *mocksurfaceRequests) setBufferScale(n int32) error {
296+ ret := _mock.Called(n)
297+
298+ if len(ret) == 0 {
299+ panic("no return value specified for setBufferScale")
300+ }
301+
302+ var r0 error
303+ if returnFunc, ok := ret.Get(0).(func(int32) error); ok {
304+ r0 = returnFunc(n)
305+ } else {
306+ r0 = ret.Error(0)
307+ }
308+ return r0
309+}
310+
311+// mocksurfaceRequests_setBufferScale_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'setBufferScale'
312+type mocksurfaceRequests_setBufferScale_Call struct {
313+ *mock.Call
314+}
315+
316+// setBufferScale is a helper method to define mock.On call
317+// - n int32
318+func (_e *mocksurfaceRequests_Expecter) setBufferScale(n any) *mocksurfaceRequests_setBufferScale_Call {
319+ return &mocksurfaceRequests_setBufferScale_Call{Call: _e.mock.On("setBufferScale", n)}
320+}
321+
322+func (_c *mocksurfaceRequests_setBufferScale_Call) Run(run func(n int32)) *mocksurfaceRequests_setBufferScale_Call {
323+ _c.Call.Run(func(args mock.Arguments) {
324+ var arg0 int32
325+ if args[0] != nil {
326+ arg0 = args[0].(int32)
327+ }
328+ run(
329+ arg0,
330+ )
331+ })
332+ return _c
333+}
334+
335+func (_c *mocksurfaceRequests_setBufferScale_Call) Return(err error) *mocksurfaceRequests_setBufferScale_Call {
336+ _c.Call.Return(err)
337+ return _c
338+}
339+
340+func (_c *mocksurfaceRequests_setBufferScale_Call) RunAndReturn(run func(n int32) error) *mocksurfaceRequests_setBufferScale_Call {
341+ _c.Call.Return(run)
342+ return _c
343+}
344+
345+// setDestination provides a mock function for the type mocksurfaceRequests
346+func (_mock *mocksurfaceRequests) setDestination(w int32, h int32) error {
347+ ret := _mock.Called(w, h)
348+
349+ if len(ret) == 0 {
350+ panic("no return value specified for setDestination")
351+ }
352+
353+ var r0 error
354+ if returnFunc, ok := ret.Get(0).(func(int32, int32) error); ok {
355+ r0 = returnFunc(w, h)
356+ } else {
357+ r0 = ret.Error(0)
358+ }
359+ return r0
360+}
361+
362+// mocksurfaceRequests_setDestination_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'setDestination'
363+type mocksurfaceRequests_setDestination_Call struct {
364+ *mock.Call
365+}
366+
367+// setDestination is a helper method to define mock.On call
368+// - w int32
369+// - h int32
370+func (_e *mocksurfaceRequests_Expecter) setDestination(w any, h any) *mocksurfaceRequests_setDestination_Call {
371+ return &mocksurfaceRequests_setDestination_Call{Call: _e.mock.On("setDestination", w, h)}
372+}
373+
374+func (_c *mocksurfaceRequests_setDestination_Call) Run(run func(w int32, h int32)) *mocksurfaceRequests_setDestination_Call {
375+ _c.Call.Run(func(args mock.Arguments) {
376+ var arg0 int32
377+ if args[0] != nil {
378+ arg0 = args[0].(int32)
379+ }
380+ var arg1 int32
381+ if args[1] != nil {
382+ arg1 = args[1].(int32)
383+ }
384+ run(
385+ arg0,
386+ arg1,
387+ )
388+ })
389+ return _c
390+}
391+
392+func (_c *mocksurfaceRequests_setDestination_Call) Return(err error) *mocksurfaceRequests_setDestination_Call {
393+ _c.Call.Return(err)
394+ return _c
395+}
396+
397+func (_c *mocksurfaceRequests_setDestination_Call) RunAndReturn(run func(w int32, h int32) error) *mocksurfaceRequests_setDestination_Call {
398+ _c.Call.Return(run)
399+ return _c
400+}
401+
402+// setOpaqueRegion provides a mock function for the type mocksurfaceRequests
403+func (_mock *mocksurfaceRequests) setOpaqueRegion(w int32, h int32, opaque bool) error {
404+ ret := _mock.Called(w, h, opaque)
405+
406+ if len(ret) == 0 {
407+ panic("no return value specified for setOpaqueRegion")
408+ }
409+
410+ var r0 error
411+ if returnFunc, ok := ret.Get(0).(func(int32, int32, bool) error); ok {
412+ r0 = returnFunc(w, h, opaque)
413+ } else {
414+ r0 = ret.Error(0)
415+ }
416+ return r0
417+}
418+
419+// mocksurfaceRequests_setOpaqueRegion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'setOpaqueRegion'
420+type mocksurfaceRequests_setOpaqueRegion_Call struct {
421+ *mock.Call
422+}
423+
424+// setOpaqueRegion is a helper method to define mock.On call
425+// - w int32
426+// - h int32
427+// - opaque bool
428+func (_e *mocksurfaceRequests_Expecter) setOpaqueRegion(w any, h any, opaque any) *mocksurfaceRequests_setOpaqueRegion_Call {
429+ return &mocksurfaceRequests_setOpaqueRegion_Call{Call: _e.mock.On("setOpaqueRegion", w, h, opaque)}
430+}
431+
432+func (_c *mocksurfaceRequests_setOpaqueRegion_Call) Run(run func(w int32, h int32, opaque bool)) *mocksurfaceRequests_setOpaqueRegion_Call {
433+ _c.Call.Run(func(args mock.Arguments) {
434+ var arg0 int32
435+ if args[0] != nil {
436+ arg0 = args[0].(int32)
437+ }
438+ var arg1 int32
439+ if args[1] != nil {
440+ arg1 = args[1].(int32)
441+ }
442+ var arg2 bool
443+ if args[2] != nil {
444+ arg2 = args[2].(bool)
445+ }
446+ run(
447+ arg0,
448+ arg1,
449+ arg2,
450+ )
451+ })
452+ return _c
453+}
454+
455+func (_c *mocksurfaceRequests_setOpaqueRegion_Call) Return(err error) *mocksurfaceRequests_setOpaqueRegion_Call {
456+ _c.Call.Return(err)
457+ return _c
458+}
459+
460+func (_c *mocksurfaceRequests_setOpaqueRegion_Call) RunAndReturn(run func(w int32, h int32, opaque bool) error) *mocksurfaceRequests_setOpaqueRegion_Call {
461+ _c.Call.Return(run)
462+ return _c
463+}
464+
465+// setRelease provides a mock function for the type mocksurfaceRequests
466+func (_mock *mocksurfaceRequests) setRelease(timeline uint64, point uint64) error {
467+ ret := _mock.Called(timeline, point)
468+
469+ if len(ret) == 0 {
470+ panic("no return value specified for setRelease")
471+ }
472+
473+ var r0 error
474+ if returnFunc, ok := ret.Get(0).(func(uint64, uint64) error); ok {
475+ r0 = returnFunc(timeline, point)
476+ } else {
477+ r0 = ret.Error(0)
478+ }
479+ return r0
480+}
481+
482+// mocksurfaceRequests_setRelease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'setRelease'
483+type mocksurfaceRequests_setRelease_Call struct {
484+ *mock.Call
485+}
486+
487+// setRelease is a helper method to define mock.On call
488+// - timeline uint64
489+// - point uint64
490+func (_e *mocksurfaceRequests_Expecter) setRelease(timeline any, point any) *mocksurfaceRequests_setRelease_Call {
491+ return &mocksurfaceRequests_setRelease_Call{Call: _e.mock.On("setRelease", timeline, point)}
492+}
493+
494+func (_c *mocksurfaceRequests_setRelease_Call) Run(run func(timeline uint64, point uint64)) *mocksurfaceRequests_setRelease_Call {
495+ _c.Call.Run(func(args mock.Arguments) {
496+ var arg0 uint64
497+ if args[0] != nil {
498+ arg0 = args[0].(uint64)
499+ }
500+ var arg1 uint64
501+ if args[1] != nil {
502+ arg1 = args[1].(uint64)
503+ }
504+ run(
505+ arg0,
506+ arg1,
507+ )
508+ })
509+ return _c
510+}
511+
512+func (_c *mocksurfaceRequests_setRelease_Call) Return(err error) *mocksurfaceRequests_setRelease_Call {
513+ _c.Call.Return(err)
514+ return _c
515+}
516+
517+func (_c *mocksurfaceRequests_setRelease_Call) RunAndReturn(run func(timeline uint64, point uint64) error) *mocksurfaceRequests_setRelease_Call {
518+ _c.Call.Return(run)
519+ return _c
520+}
A surface_types.go +21 -0
@@ -0,0 +1,21 @@
1+package neferclient
2+
3+// SurfaceID identifies a [Surface] in Handler callbacks. IDs are never
4+// reused within a connection.
5+type SurfaceID uint32
6+
7+// Format is one DRM format and modifier pair from linux-dmabuf feedback.
8+type Format struct {
9+ FourCC uint32
10+ Modifier uint64
11+}
12+
13+// Feedback is the latest complete linux-dmabuf feedback of a surface.
14+type Feedback struct {
15+ MainDevice uint64 // dev_t of the compositor's main device
16+ Formats []Format // every tranche's formats, flattened in tranche order
17+}
18+
19+// Rect is an integer rectangle: surface-local logical pixels for input
20+// regions, buffer pixels for damage.
21+type Rect struct{ X, Y, Width, Height int32 }
@@ -0,0 +1,21 @@
1+package neferclient
2+
3+// SurfaceID identifies a [Surface] in Handler callbacks. IDs are never
4+// reused within a connection.
5+type SurfaceID uint32
6+
7+// Format is one DRM format and modifier pair from linux-dmabuf feedback.
8+type Format struct {
9+ FourCC uint32
10+ Modifier uint64
11+}
12+
13+// Feedback is the latest complete linux-dmabuf feedback of a surface.
14+type Feedback struct {
15+ MainDevice uint64 // dev_t of the compositor's main device
16+ Formats []Format // every tranche's formats, flattened in tranche order
17+}
18+
19+// Rect is an integer rectangle: surface-local logical pixels for input
20+// regions, buffer pixels for damage.
21+type Rect struct{ X, Y, Width, Height int32 }
A surface_wire_test.go +200 -0
@@ -0,0 +1,200 @@
1+package neferclient_test
2+
3+import (
4+ "encoding/binary"
5+ "os"
6+ "sync/atomic"
7+ "testing"
8+
9+ "github.com/stretchr/testify/require"
10+ "golang.org/x/sys/unix"
11+
12+ "github.com/bnema/neferclient"
13+)
14+
15+var surfaceGlobals = []wireGlobal{{1, "wl_compositor", 6}, {2, "zwp_linux_dmabuf_v1", 4},
16+ {3, "wp_linux_drm_syncobj_manager_v1", 1}, {4, "wp_viewporter", 1}, {5, "xdg_wm_base", 6},
17+ {6, "ext_session_lock_manager_v1", 1}, {7, outputIface, 4}}
18+
19+var surfaceOutputs = map[uint32]outputSpec{7: {"DP-1", 1, 640, 480}}
20+
21+func openFDs(t *testing.T) int {
22+ t.Helper()
23+ ents, err := os.ReadDir("/proc/self/fd")
24+ require.NoError(t, err)
25+ return len(ents)
26+}
27+
28+// GO-009 and GO-012: failure paths close each duplicated descriptor exactly
29+// once, and descriptor 0 is never touched.
30+func TestImportErrorPathsCloseDupsOnce(t *testing.T) {
31+ c, _ := connectWire(t, surfaceGlobals, surfaceOutputs)
32+ s, err := c.NewToplevel("fd", 64, 48)
33+ require.NoError(t, err)
34+ memfd, err := unix.MemfdCreate("fd", unix.MFD_CLOEXEC)
35+ require.NoError(t, err)
36+ defer unix.Close(memfd)
37+ _, err = unix.FcntlInt(0, unix.F_GETFD, 0)
38+ require.NoError(t, err, "stdin is open for the test")
39+
40+ before := openFDs(t)
41+ // Plane 0 duplicates, plane 1 cannot: the first duplicate must be closed.
42+ err = s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 2,
43+ Planes: [4]neferclient.Plane{{FD: memfd}, {FD: 9999}}})
44+ require.Error(t, err)
45+ require.Equal(t, before, openFDs(t), "no descriptor leaked")
46+ _, err = unix.FcntlInt(0, unix.F_GETFD, 0)
47+ require.NoError(t, err, "descriptor 0 untouched")
48+ require.Error(t, s.ImportTimeline(1, 9999))
49+ require.Equal(t, before, openFDs(t))
50+
51+ // Success paths: the request consumes the duplicates.
52+ require.NoError(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 1,
53+ Planes: [4]neferclient.Plane{{FD: memfd, Stride: 256}}}))
54+ require.NoError(t, s.ImportTimeline(1, memfd))
55+ require.Equal(t, before, openFDs(t), "sent descriptors are closed, the caller's stays open")
56+ require.Error(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 1,
57+ Planes: [4]neferclient.Plane{{FD: memfd}}}), "duplicate id")
58+ require.Equal(t, before, openFDs(t))
59+}
60+
61+// requestLog counts the commits the wire peer sees.
62+type requestLog struct{ commits atomic.Int32 }
63+
64+// GO-003: before the first Present the region is staged without a commit; a
65+// lock surface refuses it.
66+func TestSetInputRegionCommitsOnlyOnceMapped(t *testing.T) {
67+ c, srv := connectWire(t, surfaceGlobals, surfaceOutputs)
68+ s, err := c.NewToplevel("input", 64, 48)
69+ require.NoError(t, err)
70+ require.NoError(t, c.Roundtrip()) // the peer consumed the initial commit
71+ var log requestLog
72+ surfaceID := s.SurfaceObjectID()
73+ srv.mu.Lock()
74+ srv.hook = func(obj uint32, op uint16, _ []byte) {
75+ if obj == surfaceID && op == 6 {
76+ log.commits.Add(1)
77+ }
78+ }
79+ srv.mu.Unlock()
80+ s.MarkPresentable(64, 48)
81+
82+ require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 0, Y: 0, Width: 4, Height: 4}}))
83+ require.NoError(t, c.Roundtrip())
84+ require.Zero(t, log.commits.Load(), "unmapped surface: staged, not committed")
85+
86+ s.MarkPresented()
87+ require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 1, Y: 1, Width: 4, Height: 4}}))
88+ require.NoError(t, c.Roundtrip())
89+ require.Equal(t, int32(1), log.commits.Load(), "mapped surface: committed")
90+ require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 1, Y: 1, Width: 4, Height: 4}}))
91+ require.NoError(t, c.Roundtrip())
92+ require.Equal(t, int32(1), log.commits.Load(), "unchanged region sends nothing")
93+
94+ l, err := c.Lock()
95+ require.NoError(t, err)
96+ ls, err := l.NewSurface(7)
97+ require.NoError(t, err)
98+ require.Error(t, ls.SetInputRegion(nil), "lock surfaces take all input")
99+}
100+
101+// GO-007: one lock surface per output.
102+func TestLockRefusesSecondSurfacePerOutput(t *testing.T) {
103+ c, _ := connectWire(t, surfaceGlobals, surfaceOutputs)
104+ l, err := c.Lock()
105+ require.NoError(t, err)
106+ first, err := l.NewSurface(7)
107+ require.NoError(t, err)
108+ _, err = l.NewSurface(7)
109+ require.ErrorContains(t, err, "already has a lock surface")
110+ require.NoError(t, first.Close())
111+ again, err := l.NewSurface(7)
112+ require.NoError(t, err, "after closing the first one the output is free again")
113+ require.NoError(t, again.Close())
114+}
115+
116+// watchLock records the ext_session_lock_v1 id the peer saw requested and
117+// counts its destroy requests.
118+func watchLock(t *testing.T, srv *wireServer) (id func() uint32, destroys *atomic.Int32) {
119+ t.Helper()
120+ var lockID atomic.Uint32
121+ destroys = new(atomic.Int32)
122+ srv.mu.Lock()
123+ srv.hook = func(obj uint32, op uint16, body []byte) {
124+ srv.mu.Lock()
125+ manager := srv.bound[6].id // the lock manager is bound by the first Lock call
126+ srv.mu.Unlock()
127+ switch {
128+ case obj == manager && op == 1 && len(body) == 4: // lock(new_id)
129+ lockID.Store(binary.LittleEndian.Uint32(body))
130+ case obj == lockID.Load() && obj != 0 && op == 0: // destroy
131+ destroys.Add(1)
132+ }
133+ }
134+ srv.mu.Unlock()
135+ return lockID.Load, destroys
136+}
137+
138+// GO-006: a locked event already on the wire when Close runs wins: destroying
139+// a locked lock is a protocol error, so Close reports it instead.
140+func TestLockCloseSeesLockedInFlight(t *testing.T) {
141+ c, srv := connectWire(t, surfaceGlobals, surfaceOutputs)
142+ lockID, destroys := watchLock(t, srv)
143+
144+ l, err := c.Lock()
145+ require.NoError(t, err)
146+ require.NoError(t, c.Roundtrip()) // the peer saw the lock request
147+ srv.write(frame(lockID(), 0, nil)) // ext_session_lock_v1.locked, not yet dispatched
148+
149+ require.ErrorContains(t, l.Close(), "use Unlock")
150+ require.Zero(t, destroys.Load(), "no destroy request was sent")
151+ require.True(t, l.Locked(), "treated as locked")
152+ require.NoError(t, c.Roundtrip())
153+ require.Zero(t, destroys.Load())
154+}
155+
156+func TestLockCloseDestroysUnlockedLock(t *testing.T) {
157+ c, srv := connectWire(t, surfaceGlobals, surfaceOutputs)
158+ _, destroys := watchLock(t, srv)
159+ l, err := c.Lock()
160+ require.NoError(t, err)
161+ require.NoError(t, l.Close())
162+ require.NoError(t, c.Roundtrip())
163+ require.Equal(t, int32(1), destroys.Load())
164+ require.NoError(t, l.Close(), "idempotent")
165+}
166+
167+// GO-002 reproduction, enabled once wlturbo handles delete_id for objects the
168+// server creates: closing a surface with a pending frame callback must not turn
169+// the compositor's later callback done and delete_id into a transport error.
170+func TestCloseWithPendingFrame(t *testing.T) {
171+ t.Skip("needs wlturbo server-side delete_id handling")
172+ c, srv := connectWire(t, surfaceGlobals, surfaceOutputs)
173+ s, err := c.NewToplevel("close", 64, 48)
174+ require.NoError(t, err)
175+ s.MarkPresentable(64, 48)
176+ surfaceID := s.SurfaceObjectID()
177+ var callback atomic.Uint32
178+ srv.mu.Lock()
179+ srv.hook = func(obj uint32, op uint16, body []byte) {
180+ if obj == surfaceID && op == 3 && len(body) == 4 {
181+ callback.Store(binary.LittleEndian.Uint32(body))
182+ }
183+ }
184+ srv.mu.Unlock()
185+ memfd, err := unix.MemfdCreate("close", unix.MFD_CLOEXEC)
186+ require.NoError(t, err)
187+ defer unix.Close(memfd)
188+ require.NoError(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 1,
189+ Planes: [4]neferclient.Plane{{FD: memfd, Stride: 256}}}))
190+ require.NoError(t, s.ImportTimeline(1, memfd))
191+ require.NoError(t, s.ImportTimeline(2, memfd))
192+ require.NoError(t, s.Present(&neferclient.Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2, AcquirePoint: 1, ReleasePoint: 2}))
193+ require.NoError(t, c.Roundtrip())
194+ require.NoError(t, s.Close())
195+
196+ srv.write(frame(callback.Load(), 0, appendU32(nil, 1)))
197+ srv.write(frame(1, 1, appendU32(nil, callback.Load())))
198+ require.NoError(t, c.Roundtrip())
199+ require.NoError(t, c.Dispatch(nil))
200+}
@@ -0,0 +1,200 @@
1+package neferclient_test
2+
3+import (
4+ "encoding/binary"
5+ "os"
6+ "sync/atomic"
7+ "testing"
8+
9+ "github.com/stretchr/testify/require"
10+ "golang.org/x/sys/unix"
11+
12+ "github.com/bnema/neferclient"
13+)
14+
15+var surfaceGlobals = []wireGlobal{{1, "wl_compositor", 6}, {2, "zwp_linux_dmabuf_v1", 4},
16+ {3, "wp_linux_drm_syncobj_manager_v1", 1}, {4, "wp_viewporter", 1}, {5, "xdg_wm_base", 6},
17+ {6, "ext_session_lock_manager_v1", 1}, {7, outputIface, 4}}
18+
19+var surfaceOutputs = map[uint32]outputSpec{7: {"DP-1", 1, 640, 480}}
20+
21+func openFDs(t *testing.T) int {
22+ t.Helper()
23+ ents, err := os.ReadDir("/proc/self/fd")
24+ require.NoError(t, err)
25+ return len(ents)
26+}
27+
28+// GO-009 and GO-012: failure paths close each duplicated descriptor exactly
29+// once, and descriptor 0 is never touched.
30+func TestImportErrorPathsCloseDupsOnce(t *testing.T) {
31+ c, _ := connectWire(t, surfaceGlobals, surfaceOutputs)
32+ s, err := c.NewToplevel("fd", 64, 48)
33+ require.NoError(t, err)
34+ memfd, err := unix.MemfdCreate("fd", unix.MFD_CLOEXEC)
35+ require.NoError(t, err)
36+ defer unix.Close(memfd)
37+ _, err = unix.FcntlInt(0, unix.F_GETFD, 0)
38+ require.NoError(t, err, "stdin is open for the test")
39+
40+ before := openFDs(t)
41+ // Plane 0 duplicates, plane 1 cannot: the first duplicate must be closed.
42+ err = s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 2,
43+ Planes: [4]neferclient.Plane{{FD: memfd}, {FD: 9999}}})
44+ require.Error(t, err)
45+ require.Equal(t, before, openFDs(t), "no descriptor leaked")
46+ _, err = unix.FcntlInt(0, unix.F_GETFD, 0)
47+ require.NoError(t, err, "descriptor 0 untouched")
48+ require.Error(t, s.ImportTimeline(1, 9999))
49+ require.Equal(t, before, openFDs(t))
50+
51+ // Success paths: the request consumes the duplicates.
52+ require.NoError(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 1,
53+ Planes: [4]neferclient.Plane{{FD: memfd, Stride: 256}}}))
54+ require.NoError(t, s.ImportTimeline(1, memfd))
55+ require.Equal(t, before, openFDs(t), "sent descriptors are closed, the caller's stays open")
56+ require.Error(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 1,
57+ Planes: [4]neferclient.Plane{{FD: memfd}}}), "duplicate id")
58+ require.Equal(t, before, openFDs(t))
59+}
60+
61+// requestLog counts the commits the wire peer sees.
62+type requestLog struct{ commits atomic.Int32 }
63+
64+// GO-003: before the first Present the region is staged without a commit; a
65+// lock surface refuses it.
66+func TestSetInputRegionCommitsOnlyOnceMapped(t *testing.T) {
67+ c, srv := connectWire(t, surfaceGlobals, surfaceOutputs)
68+ s, err := c.NewToplevel("input", 64, 48)
69+ require.NoError(t, err)
70+ require.NoError(t, c.Roundtrip()) // the peer consumed the initial commit
71+ var log requestLog
72+ surfaceID := s.SurfaceObjectID()
73+ srv.mu.Lock()
74+ srv.hook = func(obj uint32, op uint16, _ []byte) {
75+ if obj == surfaceID && op == 6 {
76+ log.commits.Add(1)
77+ }
78+ }
79+ srv.mu.Unlock()
80+ s.MarkPresentable(64, 48)
81+
82+ require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 0, Y: 0, Width: 4, Height: 4}}))
83+ require.NoError(t, c.Roundtrip())
84+ require.Zero(t, log.commits.Load(), "unmapped surface: staged, not committed")
85+
86+ s.MarkPresented()
87+ require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 1, Y: 1, Width: 4, Height: 4}}))
88+ require.NoError(t, c.Roundtrip())
89+ require.Equal(t, int32(1), log.commits.Load(), "mapped surface: committed")
90+ require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 1, Y: 1, Width: 4, Height: 4}}))
91+ require.NoError(t, c.Roundtrip())
92+ require.Equal(t, int32(1), log.commits.Load(), "unchanged region sends nothing")
93+
94+ l, err := c.Lock()
95+ require.NoError(t, err)
96+ ls, err := l.NewSurface(7)
97+ require.NoError(t, err)
98+ require.Error(t, ls.SetInputRegion(nil), "lock surfaces take all input")
99+}
100+
101+// GO-007: one lock surface per output.
102+func TestLockRefusesSecondSurfacePerOutput(t *testing.T) {
103+ c, _ := connectWire(t, surfaceGlobals, surfaceOutputs)
104+ l, err := c.Lock()
105+ require.NoError(t, err)
106+ first, err := l.NewSurface(7)
107+ require.NoError(t, err)
108+ _, err = l.NewSurface(7)
109+ require.ErrorContains(t, err, "already has a lock surface")
110+ require.NoError(t, first.Close())
111+ again, err := l.NewSurface(7)
112+ require.NoError(t, err, "after closing the first one the output is free again")
113+ require.NoError(t, again.Close())
114+}
115+
116+// watchLock records the ext_session_lock_v1 id the peer saw requested and
117+// counts its destroy requests.
118+func watchLock(t *testing.T, srv *wireServer) (id func() uint32, destroys *atomic.Int32) {
119+ t.Helper()
120+ var lockID atomic.Uint32
121+ destroys = new(atomic.Int32)
122+ srv.mu.Lock()
123+ srv.hook = func(obj uint32, op uint16, body []byte) {
124+ srv.mu.Lock()
125+ manager := srv.bound[6].id // the lock manager is bound by the first Lock call
126+ srv.mu.Unlock()
127+ switch {
128+ case obj == manager && op == 1 && len(body) == 4: // lock(new_id)
129+ lockID.Store(binary.LittleEndian.Uint32(body))
130+ case obj == lockID.Load() && obj != 0 && op == 0: // destroy
131+ destroys.Add(1)
132+ }
133+ }
134+ srv.mu.Unlock()
135+ return lockID.Load, destroys
136+}
137+
138+// GO-006: a locked event already on the wire when Close runs wins: destroying
139+// a locked lock is a protocol error, so Close reports it instead.
140+func TestLockCloseSeesLockedInFlight(t *testing.T) {
141+ c, srv := connectWire(t, surfaceGlobals, surfaceOutputs)
142+ lockID, destroys := watchLock(t, srv)
143+
144+ l, err := c.Lock()
145+ require.NoError(t, err)
146+ require.NoError(t, c.Roundtrip()) // the peer saw the lock request
147+ srv.write(frame(lockID(), 0, nil)) // ext_session_lock_v1.locked, not yet dispatched
148+
149+ require.ErrorContains(t, l.Close(), "use Unlock")
150+ require.Zero(t, destroys.Load(), "no destroy request was sent")
151+ require.True(t, l.Locked(), "treated as locked")
152+ require.NoError(t, c.Roundtrip())
153+ require.Zero(t, destroys.Load())
154+}
155+
156+func TestLockCloseDestroysUnlockedLock(t *testing.T) {
157+ c, srv := connectWire(t, surfaceGlobals, surfaceOutputs)
158+ _, destroys := watchLock(t, srv)
159+ l, err := c.Lock()
160+ require.NoError(t, err)
161+ require.NoError(t, l.Close())
162+ require.NoError(t, c.Roundtrip())
163+ require.Equal(t, int32(1), destroys.Load())
164+ require.NoError(t, l.Close(), "idempotent")
165+}
166+
167+// GO-002 reproduction, enabled once wlturbo handles delete_id for objects the
168+// server creates: closing a surface with a pending frame callback must not turn
169+// the compositor's later callback done and delete_id into a transport error.
170+func TestCloseWithPendingFrame(t *testing.T) {
171+ t.Skip("needs wlturbo server-side delete_id handling")
172+ c, srv := connectWire(t, surfaceGlobals, surfaceOutputs)
173+ s, err := c.NewToplevel("close", 64, 48)
174+ require.NoError(t, err)
175+ s.MarkPresentable(64, 48)
176+ surfaceID := s.SurfaceObjectID()
177+ var callback atomic.Uint32
178+ srv.mu.Lock()
179+ srv.hook = func(obj uint32, op uint16, body []byte) {
180+ if obj == surfaceID && op == 3 && len(body) == 4 {
181+ callback.Store(binary.LittleEndian.Uint32(body))
182+ }
183+ }
184+ srv.mu.Unlock()
185+ memfd, err := unix.MemfdCreate("close", unix.MFD_CLOEXEC)
186+ require.NoError(t, err)
187+ defer unix.Close(memfd)
188+ require.NoError(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 1,
189+ Planes: [4]neferclient.Plane{{FD: memfd, Stride: 256}}}))
190+ require.NoError(t, s.ImportTimeline(1, memfd))
191+ require.NoError(t, s.ImportTimeline(2, memfd))
192+ require.NoError(t, s.Present(&neferclient.Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2, AcquirePoint: 1, ReleasePoint: 2}))
193+ require.NoError(t, c.Roundtrip())
194+ require.NoError(t, s.Close())
195+
196+ srv.write(frame(callback.Load(), 0, appendU32(nil, 1)))
197+ srv.write(frame(1, 1, appendU32(nil, callback.Load())))
198+ require.NoError(t, c.Roundtrip())
199+ require.NoError(t, c.Dispatch(nil))
200+}
M wire_test.go +11 -2
@@ -30,6 +30,7 @@ type wireServer struct {
3030 globals []wireGlobal
3131 bound map[uint32]boundObject // global name -> bound object
3232 outputs map[uint32]outputSpec
33+ hook func(obj uint32, op uint16, body []byte) // other requests, if set
3334 }
3435
3536 type wireGlobal struct {
@@ -115,6 +116,7 @@ func isClosed(err error) bool {
115116
116117 func (s *wireServer) serve() {
117118 var hdr [8]byte
119+ var buf []byte // reused: the peer must not allocate while draining
118120 for {
119121 if _, err := io.ReadFull(s.conn, hdr[:]); err != nil {
120122 return
@@ -122,7 +124,10 @@ func (s *wireServer) serve() {
122124 obj := binary.LittleEndian.Uint32(hdr[0:4])
123125 w := binary.LittleEndian.Uint32(hdr[4:8])
124126 size, op := int(w>>16), uint16(w)
125- body := make([]byte, size-8)
127+ if cap(buf) < size-8 {
128+ buf = make([]byte, size-8)
129+ }
130+ body := buf[:size-8]
126131 if _, err := io.ReadFull(s.conn, body); err != nil {
127132 return
128133 }
@@ -133,7 +138,11 @@ func (s *wireServer) serve() {
133138 func (s *wireServer) handle(obj uint32, op uint16, body []byte) {
134139 s.mu.Lock()
135140 reg := s.registry
141+ hook := s.hook
136142 s.mu.Unlock()
143+ if hook != nil && obj != 1 && obj != reg {
144+ hook(obj, op, body) // every request on a client-created or bound object
145+ }
137146 switch {
138147 case obj == 1 && op == 1: // get_registry
139148 id := binary.LittleEndian.Uint32(body)
@@ -165,7 +174,7 @@ func (s *wireServer) handle(obj uint32, op uint16, body []byte) {
165174 if isOutput {
166175 s.describe(id, version, spec)
167176 }
168- case op == 0 && body == nil: // wl_output.release (destructor, no args)
177+ case op == 0 && len(body) == 0: // wl_output.release (destructor, no args)
169178 s.write(frame(1, 1, appendU32(nil, obj)))
170179 }
171180 }
@@ -30,6 +30,7 @@ type wireServer struct {
30 globals []wireGlobal30 globals []wireGlobal
31 bound map[uint32]boundObject // global name -> bound object31 bound map[uint32]boundObject // global name -> bound object
32 outputs map[uint32]outputSpec32 outputs map[uint32]outputSpec
33+ hook func(obj uint32, op uint16, body []byte) // other requests, if set
33 }34 }
34 35
35 type wireGlobal struct {36 type wireGlobal struct {
@@ -115,6 +116,7 @@ func isClosed(err error) bool {
115 116
116 func (s *wireServer) serve() {117 func (s *wireServer) serve() {
117 var hdr [8]byte118 var hdr [8]byte
119+ var buf []byte // reused: the peer must not allocate while draining
118 for {120 for {
119 if _, err := io.ReadFull(s.conn, hdr[:]); err != nil {121 if _, err := io.ReadFull(s.conn, hdr[:]); err != nil {
120 return122 return
@@ -122,7 +124,10 @@ func (s *wireServer) serve() {
122 obj := binary.LittleEndian.Uint32(hdr[0:4])124 obj := binary.LittleEndian.Uint32(hdr[0:4])
123 w := binary.LittleEndian.Uint32(hdr[4:8])125 w := binary.LittleEndian.Uint32(hdr[4:8])
124 size, op := int(w>>16), uint16(w)126 size, op := int(w>>16), uint16(w)
125- body := make([]byte, size-8)127+ if cap(buf) < size-8 {
128+ buf = make([]byte, size-8)
129+ }
130+ body := buf[:size-8]
126 if _, err := io.ReadFull(s.conn, body); err != nil {131 if _, err := io.ReadFull(s.conn, body); err != nil {
127 return132 return
128 }133 }
@@ -133,7 +138,11 @@ func (s *wireServer) serve() {
133 func (s *wireServer) handle(obj uint32, op uint16, body []byte) {138 func (s *wireServer) handle(obj uint32, op uint16, body []byte) {
134 s.mu.Lock()139 s.mu.Lock()
135 reg := s.registry140 reg := s.registry
141+ hook := s.hook
136 s.mu.Unlock()142 s.mu.Unlock()
143+ if hook != nil && obj != 1 && obj != reg {
144+ hook(obj, op, body) // every request on a client-created or bound object
145+ }
137 switch {146 switch {
138 case obj == 1 && op == 1: // get_registry147 case obj == 1 && op == 1: // get_registry
139 id := binary.LittleEndian.Uint32(body)148 id := binary.LittleEndian.Uint32(body)
@@ -165,7 +174,7 @@ func (s *wireServer) handle(obj uint32, op uint16, body []byte) {
165 if isOutput {174 if isOutput {
166 s.describe(id, version, spec)175 s.describe(id, version, spec)
167 }176 }
168- case op == 0 && body == nil: // wl_output.release (destructor, no args)177+ case op == 0 && len(body) == 0: // wl_output.release (destructor, no args)
169 s.write(frame(1, 1, appendU32(nil, obj)))178 s.write(frame(1, 1, appendU32(nil, obj)))
170 }179 }
171 }180 }

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 1

Tags 4

↑↓ moveenter openesc close