detached ded8960

feat(wayland): Wine and Proton protocols (#60)

* feat(wayland): support wp_pointer_warp_v1 for client pointer warps

* feat(wayland): support wl_fixes v2

* feat(wayland): support wp_alpha_modifier_v1 with fade in the compositor shaders

* feat(wayland): support color-management v3 windows_bt2100 descriptions

* fix(drm): exclude non-desktop connectors from outputs

* build(deps): pin purego-libwayland with bare resource lifetime fix

* feat(drm): lease non-desktop connectors with tracked KMS resources

* feat(app): route DRM lease inventory and requests over channels

* feat(wayland): expose DRM lease protocol to VR clients

* docs: describe VR headset DRM leasing

* fix(drm): reserve leased primary planes against desktop outputs

* fix(wayland): retain lease device descriptor across backend shutdown

* fix(drmlease): scope lessees per card and serialize lease requests

* fix(wayland): validate pointer warps against the named pointer's enter serial

* fix(drmlease): hand each inventory its own fd and keep devices until release

* fix(drmlease): close lease fds of messages dropped at shutdown

* build(deps): use purego-libwayland v0.6.0
commit
ded89609249cc186c534d01cf0454bd9f7dc2fb3
author
bnema · · 2026-09-29 06:21 UTC
committer
GitHub · · 2026-09-29 06:21 UTC
parent
92ab9ad
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
42 files · +2382 -85
M README.md +1 -0
@@ -63,6 +63,7 @@ These protocols and kernel features cut copies, waits and latency between the ap
6363 - **Only what changed.** NeferWL redraws only damaged regions, skips surfaces hidden behind opaque ones, and leaves idle outputs alone. Frame, cursor, overlay and VRR go to the kernel in one atomic commit.
6464 - **Input.** Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. Input at 1 kHz and more never waits on rendering, and the cursor has its own hardware plane. With CAP_SYS_NICE, input and output threads request real-time scheduling.
6565 - **X11 games.** Steam and X11-only games run through xwayland-satellite, started on the first X11 connection. Wine with its Wayland driver does not need it.
66+- **VR.** Non-desktop headset connectors are leased to SteamVR and Monado via `wp_drm_lease_v1`.
6667
6768 ## Hardware support
6869
@@ -63,6 +63,7 @@ These protocols and kernel features cut copies, waits and latency between the ap
63 - **Only what changed.** NeferWL redraws only damaged regions, skips surfaces hidden behind opaque ones, and leaves idle outputs alone. Frame, cursor, overlay and VRR go to the kernel in one atomic commit.63 - **Only what changed.** NeferWL redraws only damaged regions, skips surfaces hidden behind opaque ones, and leaves idle outputs alone. Frame, cursor, overlay and VRR go to the kernel in one atomic commit.
64 - **Input.** Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. Input at 1 kHz and more never waits on rendering, and the cursor has its own hardware plane. With CAP_SYS_NICE, input and output threads request real-time scheduling.64 - **Input.** Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. Input at 1 kHz and more never waits on rendering, and the cursor has its own hardware plane. With CAP_SYS_NICE, input and output threads request real-time scheduling.
65 - **X11 games.** Steam and X11-only games run through xwayland-satellite, started on the first X11 connection. Wine with its Wayland driver does not need it.65 - **X11 games.** Steam and X11-only games run through xwayland-satellite, started on the first X11 connection. Wine with its Wayland driver does not need it.
66+- **VR.** Non-desktop headset connectors are leased to SteamVR and Monado via `wp_drm_lease_v1`.
66 67
67 ## Hardware support68 ## Hardware support
68 69
M go.mod +1 -1
@@ -4,7 +4,7 @@ go 1.27
44
55 require (
66 github.com/bnema/purego v0.13.0-bnema.1
7- github.com/bnema/purego-libwayland v0.5.0
7+ github.com/bnema/purego-libwayland v0.6.0
88 github.com/bnema/purego-vulkan v0.6.0
99 github.com/bnema/wlturbo v0.2.0
1010 github.com/bnema/zerowrap v1.4.1
@@ -4,7 +4,7 @@ go 1.27
4 4
5 require (5 require (
6 github.com/bnema/purego v0.13.0-bnema.16 github.com/bnema/purego v0.13.0-bnema.1
7- github.com/bnema/purego-libwayland v0.5.07+ github.com/bnema/purego-libwayland v0.6.0
8 github.com/bnema/purego-vulkan v0.6.08 github.com/bnema/purego-vulkan v0.6.0
9 github.com/bnema/wlturbo v0.2.09 github.com/bnema/wlturbo v0.2.0
10 github.com/bnema/zerowrap v1.4.110 github.com/bnema/zerowrap v1.4.1
M go.sum +2 -2 generated file: collapsed
M internal/adapters/drm/atomic.go +6 -0
@@ -3,6 +3,7 @@ package drm
33 import (
44 "encoding/binary"
55 "errors"
6+ "os"
67 "runtime"
78 "slices"
89 "sync"
@@ -85,6 +86,11 @@ type kms interface {
8586 // addFB imports a single-plane dmabuf as a framebuffer of format.
8687 addFB(b *ports.DMABuf, format uint32) (uint32, error)
8788 rmFB(id uint32) error
89+ createLease(objects []uint32) (*os.File, uint32, error)
90+ revokeLease(id uint32) error
91+ resources() ([]uint32, []uint32, error)
92+ connector(id uint32) (connector, error)
93+ pickCrtc(c connector, crtcs []uint32, used map[uint32]bool) (uint32, error)
8894 }
8995
9096 // planeRes is one plane of the card: possible is a bitmask of CRTC indices.
@@ -3,6 +3,7 @@ package drm
3 import (3 import (
4 "encoding/binary"4 "encoding/binary"
5 "errors"5 "errors"
6+ "os"
6 "runtime"7 "runtime"
7 "slices"8 "slices"
8 "sync"9 "sync"
@@ -85,6 +86,11 @@ type kms interface {
85 // addFB imports a single-plane dmabuf as a framebuffer of format.86 // addFB imports a single-plane dmabuf as a framebuffer of format.
86 addFB(b *ports.DMABuf, format uint32) (uint32, error)87 addFB(b *ports.DMABuf, format uint32) (uint32, error)
87 rmFB(id uint32) error88 rmFB(id uint32) error
89+ createLease(objects []uint32) (*os.File, uint32, error)
90+ revokeLease(id uint32) error
91+ resources() ([]uint32, []uint32, error)
92+ connector(id uint32) (connector, error)
93+ pickCrtc(c connector, crtcs []uint32, used map[uint32]bool) (uint32, error)
88 }94 }
89 95
90 // planeRes is one plane of the card: possible is a bitmask of CRTC indices.96 // planeRes is one plane of the card: possible is a bitmask of CRTC indices.
M internal/adapters/drm/card.go +36 -10
@@ -24,9 +24,12 @@ type Card struct {
2424 log zerowrap.Logger
2525 crtcs []uint32
2626 // outputs by connector name; owned by the goroutine that calls Scan.
27- outputs map[string]*Output
28- flips map[uint32]chan flipEvent // by CRTC; set before the reader starts
29- k kmsDevice
27+ outputs map[string]*Output
28+ leasable map[string]connector
29+ leases map[uint32]leaseRecord
30+ finished []uint32
31+ flips map[uint32]chan flipEvent // by CRTC; set before the reader starts
32+ k kms
3033 // taken are planes driven by an output, by plane ID.
3134 taken map[uint32]bool
3235 // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP.
@@ -53,7 +56,7 @@ func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error
5356 flips[c] = make(chan flipEvent, 4)
5457 }
5558 k := kmsDevice{fd: fd, gemMu: &sync.Mutex{}, modifiers: hasCap(fd, capAddFB2Modifiers)}
56- return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips, k: k, taken: map[uint32]bool{}, async: hasCap(fd, capAtomicAsync)}, nil
59+ return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, leasable: map[string]connector{}, leases: map[uint32]leaseRecord{}, flips: flips, k: k, taken: map[uint32]bool{}, async: hasCap(fd, capAtomicAsync)}, nil
5760 }
5861
5962 // Path is the device path, e.g. /dev/dri/card1.
@@ -84,17 +87,22 @@ func (c *Card) SetWant(w Want) {
8487 // names to stop because they need a new mode. A stopped output must be
8588 // closed by its goroutine before its CRTC is reused: call Release.
8689 func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) {
87- _, ids, err := resources(c.fd)
90+ _, ids, err := c.k.resources()
8891 if err != nil {
8992 return nil, nil, nil, err
9093 }
9194 seen := map[string]bool{}
95+ available := map[string]connector{}
9296 for _, id := range ids {
93- conn, err := readConnector(c.fd, id)
97+ conn, err := c.k.connector(id)
9498 if err != nil {
9599 c.log.Warn().Err(err).Uint32("connector", id).Msg("read connector")
96100 continue
97101 }
102+ if conn.nonDesktop && conn.connected {
103+ available[conn.name] = conn
104+ continue
105+ }
98106 if !c.want.usable(conn) {
99107 continue
100108 }
@@ -115,6 +123,19 @@ func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) {
115123 c.outputs[conn.name] = o
116124 added = append(added, o)
117125 }
126+ for id, lease := range c.leases {
127+ for _, name := range lease.names {
128+ if _, ok := available[name]; !ok {
129+ if err := c.Revoke(id); err != nil {
130+ c.log.Warn().Err(err).Uint32("lease", id).Msg("revoke disconnected lease")
131+ } else {
132+ c.finished = append(c.finished, id)
133+ }
134+ break
135+ }
136+ }
137+ }
138+ c.leasable = available
118139 for name := range c.outputs {
119140 if !seen[name] {
120141 removed = append(removed, name)
@@ -128,18 +149,18 @@ func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) {
128149 // ConnectedHeads inventories physical connectors independently of the desired
129150 // enabled state. The caller owns Scan and must invoke this on the same goroutine.
130151 func (c *Card) ConnectedHeads() ([]ports.OutputHead, error) {
131- _, ids, err := resources(c.fd)
152+ _, ids, err := c.k.resources()
132153 if err != nil {
133154 return nil, err
134155 }
135156 var heads []ports.OutputHead
136157 for _, id := range ids {
137- conn, err := readConnector(c.fd, id)
158+ conn, err := c.k.connector(id)
138159 if err != nil {
139160 c.log.Warn().Err(err).Uint32("connector", id).Msg("read connector for inventory")
140161 continue
141162 }
142- if !conn.connected || len(conn.modes) == 0 {
163+ if !conn.connected || conn.nonDesktop || len(conn.modes) == 0 {
143164 continue
144165 }
145166 monitor := readMonitor(c.path, conn.name)
@@ -195,7 +216,12 @@ func (c *Card) open(conn connector, mode modeInfo) (*Output, error) {
195216 for _, o := range c.outputs {
196217 used[o.crtc] = true
197218 }
198- crtc, err := pickCrtc(c.fd, conn, c.crtcs, used)
219+ for _, lease := range c.leases {
220+ for _, crtc := range lease.crtcs {
221+ used[crtc] = true
222+ }
223+ }
224+ crtc, err := c.k.pickCrtc(conn, c.crtcs, used)
199225 if err != nil {
200226 return nil, err
201227 }
@@ -24,9 +24,12 @@ type Card struct {
24 log zerowrap.Logger24 log zerowrap.Logger
25 crtcs []uint3225 crtcs []uint32
26 // outputs by connector name; owned by the goroutine that calls Scan.26 // outputs by connector name; owned by the goroutine that calls Scan.
27- outputs map[string]*Output27+ outputs map[string]*Output
28- flips map[uint32]chan flipEvent // by CRTC; set before the reader starts28+ leasable map[string]connector
29- k kmsDevice29+ leases map[uint32]leaseRecord
30+ finished []uint32
31+ flips map[uint32]chan flipEvent // by CRTC; set before the reader starts
32+ k kms
30 // taken are planes driven by an output, by plane ID.33 // taken are planes driven by an output, by plane ID.
31 taken map[uint32]bool34 taken map[uint32]bool
32 // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP.35 // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP.
@@ -53,7 +56,7 @@ func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error
53 flips[c] = make(chan flipEvent, 4)56 flips[c] = make(chan flipEvent, 4)
54 }57 }
55 k := kmsDevice{fd: fd, gemMu: &sync.Mutex{}, modifiers: hasCap(fd, capAddFB2Modifiers)}58 k := kmsDevice{fd: fd, gemMu: &sync.Mutex{}, modifiers: hasCap(fd, capAddFB2Modifiers)}
56- return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips, k: k, taken: map[uint32]bool{}, async: hasCap(fd, capAtomicAsync)}, nil59+ return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, leasable: map[string]connector{}, leases: map[uint32]leaseRecord{}, flips: flips, k: k, taken: map[uint32]bool{}, async: hasCap(fd, capAtomicAsync)}, nil
57 }60 }
58 61
59 // Path is the device path, e.g. /dev/dri/card1.62 // Path is the device path, e.g. /dev/dri/card1.
@@ -84,17 +87,22 @@ func (c *Card) SetWant(w Want) {
84 // names to stop because they need a new mode. A stopped output must be87 // names to stop because they need a new mode. A stopped output must be
85 // closed by its goroutine before its CRTC is reused: call Release.88 // closed by its goroutine before its CRTC is reused: call Release.
86 func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) {89 func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) {
87- _, ids, err := resources(c.fd)90+ _, ids, err := c.k.resources()
88 if err != nil {91 if err != nil {
89 return nil, nil, nil, err92 return nil, nil, nil, err
90 }93 }
91 seen := map[string]bool{}94 seen := map[string]bool{}
95+ available := map[string]connector{}
92 for _, id := range ids {96 for _, id := range ids {
93- conn, err := readConnector(c.fd, id)97+ conn, err := c.k.connector(id)
94 if err != nil {98 if err != nil {
95 c.log.Warn().Err(err).Uint32("connector", id).Msg("read connector")99 c.log.Warn().Err(err).Uint32("connector", id).Msg("read connector")
96 continue100 continue
97 }101 }
102+ if conn.nonDesktop && conn.connected {
103+ available[conn.name] = conn
104+ continue
105+ }
98 if !c.want.usable(conn) {106 if !c.want.usable(conn) {
99 continue107 continue
100 }108 }
@@ -115,6 +123,19 @@ func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) {
115 c.outputs[conn.name] = o123 c.outputs[conn.name] = o
116 added = append(added, o)124 added = append(added, o)
117 }125 }
126+ for id, lease := range c.leases {
127+ for _, name := range lease.names {
128+ if _, ok := available[name]; !ok {
129+ if err := c.Revoke(id); err != nil {
130+ c.log.Warn().Err(err).Uint32("lease", id).Msg("revoke disconnected lease")
131+ } else {
132+ c.finished = append(c.finished, id)
133+ }
134+ break
135+ }
136+ }
137+ }
138+ c.leasable = available
118 for name := range c.outputs {139 for name := range c.outputs {
119 if !seen[name] {140 if !seen[name] {
120 removed = append(removed, name)141 removed = append(removed, name)
@@ -128,18 +149,18 @@ func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) {
128 // ConnectedHeads inventories physical connectors independently of the desired149 // ConnectedHeads inventories physical connectors independently of the desired
129 // enabled state. The caller owns Scan and must invoke this on the same goroutine.150 // enabled state. The caller owns Scan and must invoke this on the same goroutine.
130 func (c *Card) ConnectedHeads() ([]ports.OutputHead, error) {151 func (c *Card) ConnectedHeads() ([]ports.OutputHead, error) {
131- _, ids, err := resources(c.fd)152+ _, ids, err := c.k.resources()
132 if err != nil {153 if err != nil {
133 return nil, err154 return nil, err
134 }155 }
135 var heads []ports.OutputHead156 var heads []ports.OutputHead
136 for _, id := range ids {157 for _, id := range ids {
137- conn, err := readConnector(c.fd, id)158+ conn, err := c.k.connector(id)
138 if err != nil {159 if err != nil {
139 c.log.Warn().Err(err).Uint32("connector", id).Msg("read connector for inventory")160 c.log.Warn().Err(err).Uint32("connector", id).Msg("read connector for inventory")
140 continue161 continue
141 }162 }
142- if !conn.connected || len(conn.modes) == 0 {163+ if !conn.connected || conn.nonDesktop || len(conn.modes) == 0 {
143 continue164 continue
144 }165 }
145 monitor := readMonitor(c.path, conn.name)166 monitor := readMonitor(c.path, conn.name)
@@ -195,7 +216,12 @@ func (c *Card) open(conn connector, mode modeInfo) (*Output, error) {
195 for _, o := range c.outputs {216 for _, o := range c.outputs {
196 used[o.crtc] = true217 used[o.crtc] = true
197 }218 }
198- crtc, err := pickCrtc(c.fd, conn, c.crtcs, used)219+ for _, lease := range c.leases {
220+ for _, crtc := range lease.crtcs {
221+ used[crtc] = true
222+ }
223+ }
224+ crtc, err := c.k.pickCrtc(conn, c.crtcs, used)
199 if err != nil {225 if err != nil {
200 return nil, err226 return nil, err
201 }227 }
M internal/adapters/drm/kms.go +8 -2
@@ -82,6 +82,7 @@ type connector struct {
8282 id, encoderID uint32
8383 name string
8484 connected bool
85+ nonDesktop bool
8586 mmW, mmH int
8687 modes []modeInfo
8788 encoders []uint32
@@ -117,7 +118,7 @@ type HDRSettings struct {
117118
118119 // usable reports whether a connector should be driven.
119120 func (w Want) usable(c connector) bool {
120- return c.connected && len(c.modes) > 0 && !w.Disabled[c.name]
121+ return c.connected && !c.nonDesktop && len(c.modes) > 0 && !w.Disabled[c.name]
121122 }
122123
123124 // refreshMilli is the exact refresh in mHz, computed from the timings like wlroots.
@@ -216,9 +217,14 @@ func readConnector(fd int, id uint32) (connector, error) {
216217 if name == "" {
217218 name = "Unknown"
218219 }
220+ props, err := objProps(fd, id, objConnector)
221+ if err != nil {
222+ return connector{}, fmt.Errorf("connector %d properties: %w", id, err)
223+ }
219224 return connector{
220225 id: id, encoderID: g2.encoderID, name: fmt.Sprintf("%s-%d", name, g2.typeID),
221- connected: g2.connection == connected, mmW: int(g2.mmW), mmH: int(g2.mmH),
226+ connected: g2.connection == connected, nonDesktop: props["non-desktop"][1] == 1,
227+ mmW: int(g2.mmW), mmH: int(g2.mmH),
222228 modes: modes[:min(int(g2.countModes), len(modes))],
223229 encoders: encs[:min(int(g2.countEncoders), len(encs))],
224230 }, nil
@@ -82,6 +82,7 @@ type connector struct {
82 id, encoderID uint3282 id, encoderID uint32
83 name string83 name string
84 connected bool84 connected bool
85+ nonDesktop bool
85 mmW, mmH int86 mmW, mmH int
86 modes []modeInfo87 modes []modeInfo
87 encoders []uint3288 encoders []uint32
@@ -117,7 +118,7 @@ type HDRSettings struct {
117 118
118 // usable reports whether a connector should be driven.119 // usable reports whether a connector should be driven.
119 func (w Want) usable(c connector) bool {120 func (w Want) usable(c connector) bool {
120- return c.connected && len(c.modes) > 0 && !w.Disabled[c.name]121+ return c.connected && !c.nonDesktop && len(c.modes) > 0 && !w.Disabled[c.name]
121 }122 }
122 123
123 // refreshMilli is the exact refresh in mHz, computed from the timings like wlroots.124 // refreshMilli is the exact refresh in mHz, computed from the timings like wlroots.
@@ -216,9 +217,14 @@ func readConnector(fd int, id uint32) (connector, error) {
216 if name == "" {217 if name == "" {
217 name = "Unknown"218 name = "Unknown"
218 }219 }
220+ props, err := objProps(fd, id, objConnector)
221+ if err != nil {
222+ return connector{}, fmt.Errorf("connector %d properties: %w", id, err)
223+ }
219 return connector{224 return connector{
220 id: id, encoderID: g2.encoderID, name: fmt.Sprintf("%s-%d", name, g2.typeID),225 id: id, encoderID: g2.encoderID, name: fmt.Sprintf("%s-%d", name, g2.typeID),
221- connected: g2.connection == connected, mmW: int(g2.mmW), mmH: int(g2.mmH),226+ connected: g2.connection == connected, nonDesktop: props["non-desktop"][1] == 1,
227+ mmW: int(g2.mmW), mmH: int(g2.mmH),
222 modes: modes[:min(int(g2.countModes), len(modes))],228 modes: modes[:min(int(g2.countModes), len(modes))],
223 encoders: encs[:min(int(g2.countEncoders), len(encs))],229 encoders: encs[:min(int(g2.countEncoders), len(encs))],
224 }, nil230 }, nil
M internal/adapters/drm/kms_mock_test.go +316 -0
@@ -5,6 +5,8 @@
55 package drm
66
77 import (
8+ "os"
9+
810 "github.com/bnema/neferwl/internal/ports"
911 mock "github.com/stretchr/testify/mock"
1012 )
@@ -174,6 +176,66 @@ func (_c *mockkms_commit_Call) RunAndReturn(run func(req *atomicReq, flags uint3
174176 return _c
175177 }
176178
179+// connector provides a mock function for the type mockkms
180+func (_mock *mockkms) connector(id uint32) (connector, error) {
181+ ret := _mock.Called(id)
182+
183+ if len(ret) == 0 {
184+ panic("no return value specified for connector")
185+ }
186+
187+ var r0 connector
188+ var r1 error
189+ if returnFunc, ok := ret.Get(0).(func(uint32) (connector, error)); ok {
190+ return returnFunc(id)
191+ }
192+ if returnFunc, ok := ret.Get(0).(func(uint32) connector); ok {
193+ r0 = returnFunc(id)
194+ } else {
195+ r0 = ret.Get(0).(connector)
196+ }
197+ if returnFunc, ok := ret.Get(1).(func(uint32) error); ok {
198+ r1 = returnFunc(id)
199+ } else {
200+ r1 = ret.Error(1)
201+ }
202+ return r0, r1
203+}
204+
205+// mockkms_connector_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'connector'
206+type mockkms_connector_Call struct {
207+ *mock.Call
208+}
209+
210+// connector is a helper method to define mock.On call
211+// - id uint32
212+func (_e *mockkms_Expecter) connector(id any) *mockkms_connector_Call {
213+ return &mockkms_connector_Call{Call: _e.mock.On("connector", id)}
214+}
215+
216+func (_c *mockkms_connector_Call) Run(run func(id uint32)) *mockkms_connector_Call {
217+ _c.Call.Run(func(args mock.Arguments) {
218+ var arg0 uint32
219+ if args[0] != nil {
220+ arg0 = args[0].(uint32)
221+ }
222+ run(
223+ arg0,
224+ )
225+ })
226+ return _c
227+}
228+
229+func (_c *mockkms_connector_Call) Return(connectorMoqParam connector, err error) *mockkms_connector_Call {
230+ _c.Call.Return(connectorMoqParam, err)
231+ return _c
232+}
233+
234+func (_c *mockkms_connector_Call) RunAndReturn(run func(id uint32) (connector, error)) *mockkms_connector_Call {
235+ _c.Call.Return(run)
236+ return _c
237+}
238+
177239 // createBlob provides a mock function for the type mockkms
178240 func (_mock *mockkms) createBlob(data []byte) (uint32, error) {
179241 ret := _mock.Called(data)
@@ -234,6 +296,74 @@ func (_c *mockkms_createBlob_Call) RunAndReturn(run func(data []byte) (uint32, e
234296 return _c
235297 }
236298
299+// createLease provides a mock function for the type mockkms
300+func (_mock *mockkms) createLease(objects []uint32) (*os.File, uint32, error) {
301+ ret := _mock.Called(objects)
302+
303+ if len(ret) == 0 {
304+ panic("no return value specified for createLease")
305+ }
306+
307+ var r0 *os.File
308+ var r1 uint32
309+ var r2 error
310+ if returnFunc, ok := ret.Get(0).(func([]uint32) (*os.File, uint32, error)); ok {
311+ return returnFunc(objects)
312+ }
313+ if returnFunc, ok := ret.Get(0).(func([]uint32) *os.File); ok {
314+ r0 = returnFunc(objects)
315+ } else {
316+ if ret.Get(0) != nil {
317+ r0 = ret.Get(0).(*os.File)
318+ }
319+ }
320+ if returnFunc, ok := ret.Get(1).(func([]uint32) uint32); ok {
321+ r1 = returnFunc(objects)
322+ } else {
323+ r1 = ret.Get(1).(uint32)
324+ }
325+ if returnFunc, ok := ret.Get(2).(func([]uint32) error); ok {
326+ r2 = returnFunc(objects)
327+ } else {
328+ r2 = ret.Error(2)
329+ }
330+ return r0, r1, r2
331+}
332+
333+// mockkms_createLease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'createLease'
334+type mockkms_createLease_Call struct {
335+ *mock.Call
336+}
337+
338+// createLease is a helper method to define mock.On call
339+// - objects []uint32
340+func (_e *mockkms_Expecter) createLease(objects any) *mockkms_createLease_Call {
341+ return &mockkms_createLease_Call{Call: _e.mock.On("createLease", objects)}
342+}
343+
344+func (_c *mockkms_createLease_Call) Run(run func(objects []uint32)) *mockkms_createLease_Call {
345+ _c.Call.Run(func(args mock.Arguments) {
346+ var arg0 []uint32
347+ if args[0] != nil {
348+ arg0 = args[0].([]uint32)
349+ }
350+ run(
351+ arg0,
352+ )
353+ })
354+ return _c
355+}
356+
357+func (_c *mockkms_createLease_Call) Return(file *os.File, v uint32, err error) *mockkms_createLease_Call {
358+ _c.Call.Return(file, v, err)
359+ return _c
360+}
361+
362+func (_c *mockkms_createLease_Call) RunAndReturn(run func(objects []uint32) (*os.File, uint32, error)) *mockkms_createLease_Call {
363+ _c.Call.Return(run)
364+ return _c
365+}
366+
237367 // destroyBlob provides a mock function for the type mockkms
238368 func (_mock *mockkms) destroyBlob(id uint32) error {
239369 ret := _mock.Called(id)
@@ -415,6 +545,78 @@ func (_c *mockkms_objProps_Call) RunAndReturn(run func(obj uint32, typ uint32) (
415545 return _c
416546 }
417547
548+// pickCrtc provides a mock function for the type mockkms
549+func (_mock *mockkms) pickCrtc(c connector, crtcs []uint32, used map[uint32]bool) (uint32, error) {
550+ ret := _mock.Called(c, crtcs, used)
551+
552+ if len(ret) == 0 {
553+ panic("no return value specified for pickCrtc")
554+ }
555+
556+ var r0 uint32
557+ var r1 error
558+ if returnFunc, ok := ret.Get(0).(func(connector, []uint32, map[uint32]bool) (uint32, error)); ok {
559+ return returnFunc(c, crtcs, used)
560+ }
561+ if returnFunc, ok := ret.Get(0).(func(connector, []uint32, map[uint32]bool) uint32); ok {
562+ r0 = returnFunc(c, crtcs, used)
563+ } else {
564+ r0 = ret.Get(0).(uint32)
565+ }
566+ if returnFunc, ok := ret.Get(1).(func(connector, []uint32, map[uint32]bool) error); ok {
567+ r1 = returnFunc(c, crtcs, used)
568+ } else {
569+ r1 = ret.Error(1)
570+ }
571+ return r0, r1
572+}
573+
574+// mockkms_pickCrtc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'pickCrtc'
575+type mockkms_pickCrtc_Call struct {
576+ *mock.Call
577+}
578+
579+// pickCrtc is a helper method to define mock.On call
580+// - c connector
581+// - crtcs []uint32
582+// - used map[uint32]bool
583+func (_e *mockkms_Expecter) pickCrtc(c any, crtcs any, used any) *mockkms_pickCrtc_Call {
584+ return &mockkms_pickCrtc_Call{Call: _e.mock.On("pickCrtc", c, crtcs, used)}
585+}
586+
587+func (_c *mockkms_pickCrtc_Call) Run(run func(c connector, crtcs []uint32, used map[uint32]bool)) *mockkms_pickCrtc_Call {
588+ _c.Call.Run(func(args mock.Arguments) {
589+ var arg0 connector
590+ if args[0] != nil {
591+ arg0 = args[0].(connector)
592+ }
593+ var arg1 []uint32
594+ if args[1] != nil {
595+ arg1 = args[1].([]uint32)
596+ }
597+ var arg2 map[uint32]bool
598+ if args[2] != nil {
599+ arg2 = args[2].(map[uint32]bool)
600+ }
601+ run(
602+ arg0,
603+ arg1,
604+ arg2,
605+ )
606+ })
607+ return _c
608+}
609+
610+func (_c *mockkms_pickCrtc_Call) Return(v uint32, err error) *mockkms_pickCrtc_Call {
611+ _c.Call.Return(v, err)
612+ return _c
613+}
614+
615+func (_c *mockkms_pickCrtc_Call) RunAndReturn(run func(c connector, crtcs []uint32, used map[uint32]bool) (uint32, error)) *mockkms_pickCrtc_Call {
616+ _c.Call.Return(run)
617+ return _c
618+}
619+
418620 // planes provides a mock function for the type mockkms
419621 func (_mock *mockkms) planes() ([]planeRes, error) {
420622 ret := _mock.Called()
@@ -470,6 +672,120 @@ func (_c *mockkms_planes_Call) RunAndReturn(run func() ([]planeRes, error)) *moc
470672 return _c
471673 }
472674
675+// resources provides a mock function for the type mockkms
676+func (_mock *mockkms) resources() ([]uint32, []uint32, error) {
677+ ret := _mock.Called()
678+
679+ if len(ret) == 0 {
680+ panic("no return value specified for resources")
681+ }
682+
683+ var r0 []uint32
684+ var r1 []uint32
685+ var r2 error
686+ if returnFunc, ok := ret.Get(0).(func() ([]uint32, []uint32, error)); ok {
687+ return returnFunc()
688+ }
689+ if returnFunc, ok := ret.Get(0).(func() []uint32); ok {
690+ r0 = returnFunc()
691+ } else {
692+ if ret.Get(0) != nil {
693+ r0 = ret.Get(0).([]uint32)
694+ }
695+ }
696+ if returnFunc, ok := ret.Get(1).(func() []uint32); ok {
697+ r1 = returnFunc()
698+ } else {
699+ if ret.Get(1) != nil {
700+ r1 = ret.Get(1).([]uint32)
701+ }
702+ }
703+ if returnFunc, ok := ret.Get(2).(func() error); ok {
704+ r2 = returnFunc()
705+ } else {
706+ r2 = ret.Error(2)
707+ }
708+ return r0, r1, r2
709+}
710+
711+// mockkms_resources_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'resources'
712+type mockkms_resources_Call struct {
713+ *mock.Call
714+}
715+
716+// resources is a helper method to define mock.On call
717+func (_e *mockkms_Expecter) resources() *mockkms_resources_Call {
718+ return &mockkms_resources_Call{Call: _e.mock.On("resources")}
719+}
720+
721+func (_c *mockkms_resources_Call) Run(run func()) *mockkms_resources_Call {
722+ _c.Call.Run(func(args mock.Arguments) {
723+ run()
724+ })
725+ return _c
726+}
727+
728+func (_c *mockkms_resources_Call) Return(uint32s []uint32, uint32s1 []uint32, err error) *mockkms_resources_Call {
729+ _c.Call.Return(uint32s, uint32s1, err)
730+ return _c
731+}
732+
733+func (_c *mockkms_resources_Call) RunAndReturn(run func() ([]uint32, []uint32, error)) *mockkms_resources_Call {
734+ _c.Call.Return(run)
735+ return _c
736+}
737+
738+// revokeLease provides a mock function for the type mockkms
739+func (_mock *mockkms) revokeLease(id uint32) error {
740+ ret := _mock.Called(id)
741+
742+ if len(ret) == 0 {
743+ panic("no return value specified for revokeLease")
744+ }
745+
746+ var r0 error
747+ if returnFunc, ok := ret.Get(0).(func(uint32) error); ok {
748+ r0 = returnFunc(id)
749+ } else {
750+ r0 = ret.Error(0)
751+ }
752+ return r0
753+}
754+
755+// mockkms_revokeLease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'revokeLease'
756+type mockkms_revokeLease_Call struct {
757+ *mock.Call
758+}
759+
760+// revokeLease is a helper method to define mock.On call
761+// - id uint32
762+func (_e *mockkms_Expecter) revokeLease(id any) *mockkms_revokeLease_Call {
763+ return &mockkms_revokeLease_Call{Call: _e.mock.On("revokeLease", id)}
764+}
765+
766+func (_c *mockkms_revokeLease_Call) Run(run func(id uint32)) *mockkms_revokeLease_Call {
767+ _c.Call.Run(func(args mock.Arguments) {
768+ var arg0 uint32
769+ if args[0] != nil {
770+ arg0 = args[0].(uint32)
771+ }
772+ run(
773+ arg0,
774+ )
775+ })
776+ return _c
777+}
778+
779+func (_c *mockkms_revokeLease_Call) Return(err error) *mockkms_revokeLease_Call {
780+ _c.Call.Return(err)
781+ return _c
782+}
783+
784+func (_c *mockkms_revokeLease_Call) RunAndReturn(run func(id uint32) error) *mockkms_revokeLease_Call {
785+ _c.Call.Return(run)
786+ return _c
787+}
788+
473789 // rmFB provides a mock function for the type mockkms
474790 func (_mock *mockkms) rmFB(id uint32) error {
475791 ret := _mock.Called(id)
@@ -5,6 +5,8 @@
5 package drm5 package drm
6 6
7 import (7 import (
8+ "os"
9+
8 "github.com/bnema/neferwl/internal/ports"10 "github.com/bnema/neferwl/internal/ports"
9 mock "github.com/stretchr/testify/mock"11 mock "github.com/stretchr/testify/mock"
10 )12 )
@@ -174,6 +176,66 @@ func (_c *mockkms_commit_Call) RunAndReturn(run func(req *atomicReq, flags uint3
174 return _c176 return _c
175 }177 }
176 178
179+// connector provides a mock function for the type mockkms
180+func (_mock *mockkms) connector(id uint32) (connector, error) {
181+ ret := _mock.Called(id)
182+
183+ if len(ret) == 0 {
184+ panic("no return value specified for connector")
185+ }
186+
187+ var r0 connector
188+ var r1 error
189+ if returnFunc, ok := ret.Get(0).(func(uint32) (connector, error)); ok {
190+ return returnFunc(id)
191+ }
192+ if returnFunc, ok := ret.Get(0).(func(uint32) connector); ok {
193+ r0 = returnFunc(id)
194+ } else {
195+ r0 = ret.Get(0).(connector)
196+ }
197+ if returnFunc, ok := ret.Get(1).(func(uint32) error); ok {
198+ r1 = returnFunc(id)
199+ } else {
200+ r1 = ret.Error(1)
201+ }
202+ return r0, r1
203+}
204+
205+// mockkms_connector_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'connector'
206+type mockkms_connector_Call struct {
207+ *mock.Call
208+}
209+
210+// connector is a helper method to define mock.On call
211+// - id uint32
212+func (_e *mockkms_Expecter) connector(id any) *mockkms_connector_Call {
213+ return &mockkms_connector_Call{Call: _e.mock.On("connector", id)}
214+}
215+
216+func (_c *mockkms_connector_Call) Run(run func(id uint32)) *mockkms_connector_Call {
217+ _c.Call.Run(func(args mock.Arguments) {
218+ var arg0 uint32
219+ if args[0] != nil {
220+ arg0 = args[0].(uint32)
221+ }
222+ run(
223+ arg0,
224+ )
225+ })
226+ return _c
227+}
228+
229+func (_c *mockkms_connector_Call) Return(connectorMoqParam connector, err error) *mockkms_connector_Call {
230+ _c.Call.Return(connectorMoqParam, err)
231+ return _c
232+}
233+
234+func (_c *mockkms_connector_Call) RunAndReturn(run func(id uint32) (connector, error)) *mockkms_connector_Call {
235+ _c.Call.Return(run)
236+ return _c
237+}
238+
177 // createBlob provides a mock function for the type mockkms239 // createBlob provides a mock function for the type mockkms
178 func (_mock *mockkms) createBlob(data []byte) (uint32, error) {240 func (_mock *mockkms) createBlob(data []byte) (uint32, error) {
179 ret := _mock.Called(data)241 ret := _mock.Called(data)
@@ -234,6 +296,74 @@ func (_c *mockkms_createBlob_Call) RunAndReturn(run func(data []byte) (uint32, e
234 return _c296 return _c
235 }297 }
236 298
299+// createLease provides a mock function for the type mockkms
300+func (_mock *mockkms) createLease(objects []uint32) (*os.File, uint32, error) {
301+ ret := _mock.Called(objects)
302+
303+ if len(ret) == 0 {
304+ panic("no return value specified for createLease")
305+ }
306+
307+ var r0 *os.File
308+ var r1 uint32
309+ var r2 error
310+ if returnFunc, ok := ret.Get(0).(func([]uint32) (*os.File, uint32, error)); ok {
311+ return returnFunc(objects)
312+ }
313+ if returnFunc, ok := ret.Get(0).(func([]uint32) *os.File); ok {
314+ r0 = returnFunc(objects)
315+ } else {
316+ if ret.Get(0) != nil {
317+ r0 = ret.Get(0).(*os.File)
318+ }
319+ }
320+ if returnFunc, ok := ret.Get(1).(func([]uint32) uint32); ok {
321+ r1 = returnFunc(objects)
322+ } else {
323+ r1 = ret.Get(1).(uint32)
324+ }
325+ if returnFunc, ok := ret.Get(2).(func([]uint32) error); ok {
326+ r2 = returnFunc(objects)
327+ } else {
328+ r2 = ret.Error(2)
329+ }
330+ return r0, r1, r2
331+}
332+
333+// mockkms_createLease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'createLease'
334+type mockkms_createLease_Call struct {
335+ *mock.Call
336+}
337+
338+// createLease is a helper method to define mock.On call
339+// - objects []uint32
340+func (_e *mockkms_Expecter) createLease(objects any) *mockkms_createLease_Call {
341+ return &mockkms_createLease_Call{Call: _e.mock.On("createLease", objects)}
342+}
343+
344+func (_c *mockkms_createLease_Call) Run(run func(objects []uint32)) *mockkms_createLease_Call {
345+ _c.Call.Run(func(args mock.Arguments) {
346+ var arg0 []uint32
347+ if args[0] != nil {
348+ arg0 = args[0].([]uint32)
349+ }
350+ run(
351+ arg0,
352+ )
353+ })
354+ return _c
355+}
356+
357+func (_c *mockkms_createLease_Call) Return(file *os.File, v uint32, err error) *mockkms_createLease_Call {
358+ _c.Call.Return(file, v, err)
359+ return _c
360+}
361+
362+func (_c *mockkms_createLease_Call) RunAndReturn(run func(objects []uint32) (*os.File, uint32, error)) *mockkms_createLease_Call {
363+ _c.Call.Return(run)
364+ return _c
365+}
366+
237 // destroyBlob provides a mock function for the type mockkms367 // destroyBlob provides a mock function for the type mockkms
238 func (_mock *mockkms) destroyBlob(id uint32) error {368 func (_mock *mockkms) destroyBlob(id uint32) error {
239 ret := _mock.Called(id)369 ret := _mock.Called(id)
@@ -415,6 +545,78 @@ func (_c *mockkms_objProps_Call) RunAndReturn(run func(obj uint32, typ uint32) (
415 return _c545 return _c
416 }546 }
417 547
548+// pickCrtc provides a mock function for the type mockkms
549+func (_mock *mockkms) pickCrtc(c connector, crtcs []uint32, used map[uint32]bool) (uint32, error) {
550+ ret := _mock.Called(c, crtcs, used)
551+
552+ if len(ret) == 0 {
553+ panic("no return value specified for pickCrtc")
554+ }
555+
556+ var r0 uint32
557+ var r1 error
558+ if returnFunc, ok := ret.Get(0).(func(connector, []uint32, map[uint32]bool) (uint32, error)); ok {
559+ return returnFunc(c, crtcs, used)
560+ }
561+ if returnFunc, ok := ret.Get(0).(func(connector, []uint32, map[uint32]bool) uint32); ok {
562+ r0 = returnFunc(c, crtcs, used)
563+ } else {
564+ r0 = ret.Get(0).(uint32)
565+ }
566+ if returnFunc, ok := ret.Get(1).(func(connector, []uint32, map[uint32]bool) error); ok {
567+ r1 = returnFunc(c, crtcs, used)
568+ } else {
569+ r1 = ret.Error(1)
570+ }
571+ return r0, r1
572+}
573+
574+// mockkms_pickCrtc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'pickCrtc'
575+type mockkms_pickCrtc_Call struct {
576+ *mock.Call
577+}
578+
579+// pickCrtc is a helper method to define mock.On call
580+// - c connector
581+// - crtcs []uint32
582+// - used map[uint32]bool
583+func (_e *mockkms_Expecter) pickCrtc(c any, crtcs any, used any) *mockkms_pickCrtc_Call {
584+ return &mockkms_pickCrtc_Call{Call: _e.mock.On("pickCrtc", c, crtcs, used)}
585+}
586+
587+func (_c *mockkms_pickCrtc_Call) Run(run func(c connector, crtcs []uint32, used map[uint32]bool)) *mockkms_pickCrtc_Call {
588+ _c.Call.Run(func(args mock.Arguments) {
589+ var arg0 connector
590+ if args[0] != nil {
591+ arg0 = args[0].(connector)
592+ }
593+ var arg1 []uint32
594+ if args[1] != nil {
595+ arg1 = args[1].([]uint32)
596+ }
597+ var arg2 map[uint32]bool
598+ if args[2] != nil {
599+ arg2 = args[2].(map[uint32]bool)
600+ }
601+ run(
602+ arg0,
603+ arg1,
604+ arg2,
605+ )
606+ })
607+ return _c
608+}
609+
610+func (_c *mockkms_pickCrtc_Call) Return(v uint32, err error) *mockkms_pickCrtc_Call {
611+ _c.Call.Return(v, err)
612+ return _c
613+}
614+
615+func (_c *mockkms_pickCrtc_Call) RunAndReturn(run func(c connector, crtcs []uint32, used map[uint32]bool) (uint32, error)) *mockkms_pickCrtc_Call {
616+ _c.Call.Return(run)
617+ return _c
618+}
619+
418 // planes provides a mock function for the type mockkms620 // planes provides a mock function for the type mockkms
419 func (_mock *mockkms) planes() ([]planeRes, error) {621 func (_mock *mockkms) planes() ([]planeRes, error) {
420 ret := _mock.Called()622 ret := _mock.Called()
@@ -470,6 +672,120 @@ func (_c *mockkms_planes_Call) RunAndReturn(run func() ([]planeRes, error)) *moc
470 return _c672 return _c
471 }673 }
472 674
675+// resources provides a mock function for the type mockkms
676+func (_mock *mockkms) resources() ([]uint32, []uint32, error) {
677+ ret := _mock.Called()
678+
679+ if len(ret) == 0 {
680+ panic("no return value specified for resources")
681+ }
682+
683+ var r0 []uint32
684+ var r1 []uint32
685+ var r2 error
686+ if returnFunc, ok := ret.Get(0).(func() ([]uint32, []uint32, error)); ok {
687+ return returnFunc()
688+ }
689+ if returnFunc, ok := ret.Get(0).(func() []uint32); ok {
690+ r0 = returnFunc()
691+ } else {
692+ if ret.Get(0) != nil {
693+ r0 = ret.Get(0).([]uint32)
694+ }
695+ }
696+ if returnFunc, ok := ret.Get(1).(func() []uint32); ok {
697+ r1 = returnFunc()
698+ } else {
699+ if ret.Get(1) != nil {
700+ r1 = ret.Get(1).([]uint32)
701+ }
702+ }
703+ if returnFunc, ok := ret.Get(2).(func() error); ok {
704+ r2 = returnFunc()
705+ } else {
706+ r2 = ret.Error(2)
707+ }
708+ return r0, r1, r2
709+}
710+
711+// mockkms_resources_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'resources'
712+type mockkms_resources_Call struct {
713+ *mock.Call
714+}
715+
716+// resources is a helper method to define mock.On call
717+func (_e *mockkms_Expecter) resources() *mockkms_resources_Call {
718+ return &mockkms_resources_Call{Call: _e.mock.On("resources")}
719+}
720+
721+func (_c *mockkms_resources_Call) Run(run func()) *mockkms_resources_Call {
722+ _c.Call.Run(func(args mock.Arguments) {
723+ run()
724+ })
725+ return _c
726+}
727+
728+func (_c *mockkms_resources_Call) Return(uint32s []uint32, uint32s1 []uint32, err error) *mockkms_resources_Call {
729+ _c.Call.Return(uint32s, uint32s1, err)
730+ return _c
731+}
732+
733+func (_c *mockkms_resources_Call) RunAndReturn(run func() ([]uint32, []uint32, error)) *mockkms_resources_Call {
734+ _c.Call.Return(run)
735+ return _c
736+}
737+
738+// revokeLease provides a mock function for the type mockkms
739+func (_mock *mockkms) revokeLease(id uint32) error {
740+ ret := _mock.Called(id)
741+
742+ if len(ret) == 0 {
743+ panic("no return value specified for revokeLease")
744+ }
745+
746+ var r0 error
747+ if returnFunc, ok := ret.Get(0).(func(uint32) error); ok {
748+ r0 = returnFunc(id)
749+ } else {
750+ r0 = ret.Error(0)
751+ }
752+ return r0
753+}
754+
755+// mockkms_revokeLease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'revokeLease'
756+type mockkms_revokeLease_Call struct {
757+ *mock.Call
758+}
759+
760+// revokeLease is a helper method to define mock.On call
761+// - id uint32
762+func (_e *mockkms_Expecter) revokeLease(id any) *mockkms_revokeLease_Call {
763+ return &mockkms_revokeLease_Call{Call: _e.mock.On("revokeLease", id)}
764+}
765+
766+func (_c *mockkms_revokeLease_Call) Run(run func(id uint32)) *mockkms_revokeLease_Call {
767+ _c.Call.Run(func(args mock.Arguments) {
768+ var arg0 uint32
769+ if args[0] != nil {
770+ arg0 = args[0].(uint32)
771+ }
772+ run(
773+ arg0,
774+ )
775+ })
776+ return _c
777+}
778+
779+func (_c *mockkms_revokeLease_Call) Return(err error) *mockkms_revokeLease_Call {
780+ _c.Call.Return(err)
781+ return _c
782+}
783+
784+func (_c *mockkms_revokeLease_Call) RunAndReturn(run func(id uint32) error) *mockkms_revokeLease_Call {
785+ _c.Call.Return(run)
786+ return _c
787+}
788+
473 // rmFB provides a mock function for the type mockkms789 // rmFB provides a mock function for the type mockkms
474 func (_mock *mockkms) rmFB(id uint32) error {790 func (_mock *mockkms) rmFB(id uint32) error {
475 ret := _mock.Called(id)791 ret := _mock.Called(id)
M internal/adapters/drm/kms_test.go +4 -2
@@ -68,8 +68,10 @@ func TestWantPicksMode(t *testing.T) {
6868 t.Errorf("%s: got %s", tc.name, m)
6969 }
7070 }
71- if (Want{}).usable(off) || !(Want{}).usable(lg) || (Want{Disabled: map[string]bool{"DP-2": true}}).usable(lg) {
72- t.Fatal("usable")
71+ vr := lg
72+ vr.nonDesktop = true
73+ if (Want{}).usable(off) || !(Want{}).usable(lg) || (Want{Disabled: map[string]bool{"DP-2": true}}).usable(lg) || (Want{}).usable(vr) {
74+ t.Fatal("usable: non-desktop connectors must not be modeset")
7375 }
7476 }
7577
@@ -68,8 +68,10 @@ func TestWantPicksMode(t *testing.T) {
68 t.Errorf("%s: got %s", tc.name, m)68 t.Errorf("%s: got %s", tc.name, m)
69 }69 }
70 }70 }
71- if (Want{}).usable(off) || !(Want{}).usable(lg) || (Want{Disabled: map[string]bool{"DP-2": true}}).usable(lg) {71+ vr := lg
72- t.Fatal("usable")72+ vr.nonDesktop = true
73+ if (Want{}).usable(off) || !(Want{}).usable(lg) || (Want{Disabled: map[string]bool{"DP-2": true}}).usable(lg) || (Want{}).usable(vr) {
74+ t.Fatal("usable: non-desktop connectors must not be modeset")
73 }75 }
74 }76 }
75 77
A internal/adapters/drm/lease.go +206 -0
@@ -0,0 +1,206 @@
1+package drm
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "os"
7+ "runtime"
8+ "slices"
9+ "unsafe"
10+
11+ "github.com/bnema/neferwl/internal/ports"
12+ "golang.org/x/sys/unix"
13+)
14+
15+const (
16+ ioctlCreateLease = 0xC01864C6
17+ ioctlRevokeLease = 0xC00464C9
18+ ioctlDropMaster = 0x641f
19+)
20+
21+type modeCreateLease struct {
22+ objects uint64
23+ count, flags, lessee, fd uint32
24+}
25+type modeRevokeLease struct{ lessee uint32 }
26+
27+func (k kmsDevice) createLease(objects []uint32) (*os.File, uint32, error) {
28+ if len(objects) == 0 {
29+ return nil, 0, errors.New("empty lease")
30+ }
31+ q := modeCreateLease{objects: uint64(uintptr(unsafe.Pointer(&objects[0]))), count: uint32(len(objects)), flags: unix.O_CLOEXEC}
32+ err := ioctl(k.fd, ioctlCreateLease, unsafe.Pointer(&q))
33+ runtime.KeepAlive(objects)
34+ if err != nil {
35+ return nil, 0, err
36+ }
37+ return os.NewFile(uintptr(q.fd), "drm lease"), q.lessee, nil
38+}
39+func (k kmsDevice) revokeLease(id uint32) error {
40+ return ioctl(k.fd, ioctlRevokeLease, unsafe.Pointer(&modeRevokeLease{lessee: id}))
41+}
42+func (k kmsDevice) resources() ([]uint32, []uint32, error) { return resources(k.fd) }
43+func (k kmsDevice) connector(id uint32) (connector, error) { return readConnector(k.fd, id) }
44+func (k kmsDevice) pickCrtc(c connector, crtcs []uint32, used map[uint32]bool) (uint32, error) {
45+ return pickCrtc(k.fd, c, crtcs, used)
46+}
47+
48+type leaseRecord struct {
49+ names []string
50+ crtcs, planes []uint32
51+}
52+
53+// Leasable returns connected non-desktop connectors not held by a lease.
54+// Scan and lease operations share one owner goroutine.
55+func (c *Card) Leasable() []ports.LeaseConnector {
56+ var result []ports.LeaseConnector
57+ for _, conn := range c.leasable {
58+ if !conn.connected || c.leasedName(conn.name) {
59+ continue
60+ }
61+ m := readMonitor(c.path, conn.name)
62+ result = append(result, ports.LeaseConnector{Card: c.path, Name: conn.name, Description: fmt.Sprintf("%s %s", m.Make, m.Model), ConnectorID: conn.id})
63+ }
64+ slices.SortFunc(result, func(a, b ports.LeaseConnector) int {
65+ if a.Name < b.Name {
66+ return -1
67+ }
68+ if a.Name > b.Name {
69+ return 1
70+ }
71+ return 0
72+ })
73+ return result
74+}
75+func (c *Card) leasedName(name string) bool {
76+ for _, l := range c.leases {
77+ if slices.Contains(l.names, name) {
78+ return true
79+ }
80+ }
81+ return false
82+}
83+func (c *Card) Lease(names []string) (*os.File, uint32, error) {
84+ if len(names) == 0 {
85+ return nil, 0, errors.New("empty lease")
86+ }
87+ used := map[uint32]bool{}
88+ planesUsed := map[uint32]bool{}
89+ for _, o := range c.outputs {
90+ used[o.crtc] = true
91+ for _, p := range o.owned() {
92+ planesUsed[p.id] = true
93+ }
94+ }
95+ for _, l := range c.leases {
96+ for _, id := range l.crtcs {
97+ used[id] = true
98+ }
99+ for _, id := range l.planes {
100+ planesUsed[id] = true
101+ }
102+ }
103+ objects := make([]uint32, 0, len(names)*3)
104+ record := leaseRecord{}
105+ seen := map[string]bool{}
106+ for _, name := range names {
107+ if seen[name] || c.leasedName(name) {
108+ return nil, 0, fmt.Errorf("connector %s unavailable", name)
109+ }
110+ seen[name] = true
111+ var conn connector
112+ found := false
113+ for _, v := range c.leasable {
114+ if v.name == name && v.connected {
115+ conn = v
116+ found = true
117+ break
118+ }
119+ }
120+ if !found {
121+ return nil, 0, fmt.Errorf("connector %s unavailable", name)
122+ }
123+ crtc, err := c.k.pickCrtc(conn, c.crtcs, used)
124+ if err != nil {
125+ return nil, 0, err
126+ }
127+ pipe := slices.Index(c.crtcs, crtc)
128+ if pipe < 0 {
129+ return nil, 0, fmt.Errorf("CRTC %d not on card", crtc)
130+ }
131+ ps, err := readPlanes(c.k, pipe)
132+ if err != nil {
133+ return nil, 0, err
134+ }
135+ var planeID uint32
136+ for _, p := range ps {
137+ if p.typ == planePrimary && !planesUsed[p.id] && !c.taken[p.id] {
138+ planeID = p.id
139+ break
140+ }
141+ }
142+ if planeID == 0 {
143+ return nil, 0, fmt.Errorf("no primary plane for %s", name)
144+ }
145+ used[crtc] = true
146+ planesUsed[planeID] = true
147+ record.names = append(record.names, name)
148+ record.crtcs = append(record.crtcs, crtc)
149+ record.planes = append(record.planes, planeID)
150+ objects = append(objects, conn.id, crtc, planeID)
151+ }
152+ fd, id, err := c.k.createLease(objects)
153+ if err != nil {
154+ return nil, 0, err
155+ }
156+ if c.leases == nil {
157+ c.leases = map[uint32]leaseRecord{}
158+ }
159+ c.leases[id] = record
160+ for _, plane := range record.planes {
161+ c.taken[plane] = true
162+ }
163+ return fd, id, nil
164+}
165+func (c *Card) Revoke(id uint32) error {
166+ record, ok := c.leases[id]
167+ if !ok {
168+ return nil
169+ }
170+ if err := c.k.revokeLease(id); err != nil {
171+ return err
172+ }
173+ for _, plane := range record.planes {
174+ delete(c.taken, plane)
175+ }
176+ delete(c.leases, id)
177+ return nil
178+}
179+func (c *Card) LeaseIDs() []uint32 {
180+ ids := make([]uint32, 0, len(c.leases))
181+ for id := range c.leases {
182+ ids = append(ids, id)
183+ }
184+ slices.Sort(ids)
185+ return ids
186+}
187+
188+// ClientFD opens a separate card fd and makes sure it is not a DRM master.
189+func (c *Card) ClientFD() (*os.File, error) {
190+ fd, err := unix.Open(c.path, unix.O_RDWR|unix.O_CLOEXEC, 0)
191+ if err != nil {
192+ return nil, err
193+ }
194+ if err = ioctl(fd, ioctlDropMaster, nil); err != nil && !errors.Is(err, unix.EINVAL) && !errors.Is(err, unix.EACCES) {
195+ unix.Close(fd)
196+ return nil, err
197+ }
198+ return os.NewFile(uintptr(fd), c.path), nil
199+}
200+
201+// FinishedLeases drains IDs revoked after a connector disappeared during Scan.
202+func (c *Card) FinishedLeases() []uint32 {
203+ ids := c.finished
204+ c.finished = nil
205+ return ids
206+}
@@ -0,0 +1,206 @@
1+package drm
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "os"
7+ "runtime"
8+ "slices"
9+ "unsafe"
10+
11+ "github.com/bnema/neferwl/internal/ports"
12+ "golang.org/x/sys/unix"
13+)
14+
15+const (
16+ ioctlCreateLease = 0xC01864C6
17+ ioctlRevokeLease = 0xC00464C9
18+ ioctlDropMaster = 0x641f
19+)
20+
21+type modeCreateLease struct {
22+ objects uint64
23+ count, flags, lessee, fd uint32
24+}
25+type modeRevokeLease struct{ lessee uint32 }
26+
27+func (k kmsDevice) createLease(objects []uint32) (*os.File, uint32, error) {
28+ if len(objects) == 0 {
29+ return nil, 0, errors.New("empty lease")
30+ }
31+ q := modeCreateLease{objects: uint64(uintptr(unsafe.Pointer(&objects[0]))), count: uint32(len(objects)), flags: unix.O_CLOEXEC}
32+ err := ioctl(k.fd, ioctlCreateLease, unsafe.Pointer(&q))
33+ runtime.KeepAlive(objects)
34+ if err != nil {
35+ return nil, 0, err
36+ }
37+ return os.NewFile(uintptr(q.fd), "drm lease"), q.lessee, nil
38+}
39+func (k kmsDevice) revokeLease(id uint32) error {
40+ return ioctl(k.fd, ioctlRevokeLease, unsafe.Pointer(&modeRevokeLease{lessee: id}))
41+}
42+func (k kmsDevice) resources() ([]uint32, []uint32, error) { return resources(k.fd) }
43+func (k kmsDevice) connector(id uint32) (connector, error) { return readConnector(k.fd, id) }
44+func (k kmsDevice) pickCrtc(c connector, crtcs []uint32, used map[uint32]bool) (uint32, error) {
45+ return pickCrtc(k.fd, c, crtcs, used)
46+}
47+
48+type leaseRecord struct {
49+ names []string
50+ crtcs, planes []uint32
51+}
52+
53+// Leasable returns connected non-desktop connectors not held by a lease.
54+// Scan and lease operations share one owner goroutine.
55+func (c *Card) Leasable() []ports.LeaseConnector {
56+ var result []ports.LeaseConnector
57+ for _, conn := range c.leasable {
58+ if !conn.connected || c.leasedName(conn.name) {
59+ continue
60+ }
61+ m := readMonitor(c.path, conn.name)
62+ result = append(result, ports.LeaseConnector{Card: c.path, Name: conn.name, Description: fmt.Sprintf("%s %s", m.Make, m.Model), ConnectorID: conn.id})
63+ }
64+ slices.SortFunc(result, func(a, b ports.LeaseConnector) int {
65+ if a.Name < b.Name {
66+ return -1
67+ }
68+ if a.Name > b.Name {
69+ return 1
70+ }
71+ return 0
72+ })
73+ return result
74+}
75+func (c *Card) leasedName(name string) bool {
76+ for _, l := range c.leases {
77+ if slices.Contains(l.names, name) {
78+ return true
79+ }
80+ }
81+ return false
82+}
83+func (c *Card) Lease(names []string) (*os.File, uint32, error) {
84+ if len(names) == 0 {
85+ return nil, 0, errors.New("empty lease")
86+ }
87+ used := map[uint32]bool{}
88+ planesUsed := map[uint32]bool{}
89+ for _, o := range c.outputs {
90+ used[o.crtc] = true
91+ for _, p := range o.owned() {
92+ planesUsed[p.id] = true
93+ }
94+ }
95+ for _, l := range c.leases {
96+ for _, id := range l.crtcs {
97+ used[id] = true
98+ }
99+ for _, id := range l.planes {
100+ planesUsed[id] = true
101+ }
102+ }
103+ objects := make([]uint32, 0, len(names)*3)
104+ record := leaseRecord{}
105+ seen := map[string]bool{}
106+ for _, name := range names {
107+ if seen[name] || c.leasedName(name) {
108+ return nil, 0, fmt.Errorf("connector %s unavailable", name)
109+ }
110+ seen[name] = true
111+ var conn connector
112+ found := false
113+ for _, v := range c.leasable {
114+ if v.name == name && v.connected {
115+ conn = v
116+ found = true
117+ break
118+ }
119+ }
120+ if !found {
121+ return nil, 0, fmt.Errorf("connector %s unavailable", name)
122+ }
123+ crtc, err := c.k.pickCrtc(conn, c.crtcs, used)
124+ if err != nil {
125+ return nil, 0, err
126+ }
127+ pipe := slices.Index(c.crtcs, crtc)
128+ if pipe < 0 {
129+ return nil, 0, fmt.Errorf("CRTC %d not on card", crtc)
130+ }
131+ ps, err := readPlanes(c.k, pipe)
132+ if err != nil {
133+ return nil, 0, err
134+ }
135+ var planeID uint32
136+ for _, p := range ps {
137+ if p.typ == planePrimary && !planesUsed[p.id] && !c.taken[p.id] {
138+ planeID = p.id
139+ break
140+ }
141+ }
142+ if planeID == 0 {
143+ return nil, 0, fmt.Errorf("no primary plane for %s", name)
144+ }
145+ used[crtc] = true
146+ planesUsed[planeID] = true
147+ record.names = append(record.names, name)
148+ record.crtcs = append(record.crtcs, crtc)
149+ record.planes = append(record.planes, planeID)
150+ objects = append(objects, conn.id, crtc, planeID)
151+ }
152+ fd, id, err := c.k.createLease(objects)
153+ if err != nil {
154+ return nil, 0, err
155+ }
156+ if c.leases == nil {
157+ c.leases = map[uint32]leaseRecord{}
158+ }
159+ c.leases[id] = record
160+ for _, plane := range record.planes {
161+ c.taken[plane] = true
162+ }
163+ return fd, id, nil
164+}
165+func (c *Card) Revoke(id uint32) error {
166+ record, ok := c.leases[id]
167+ if !ok {
168+ return nil
169+ }
170+ if err := c.k.revokeLease(id); err != nil {
171+ return err
172+ }
173+ for _, plane := range record.planes {
174+ delete(c.taken, plane)
175+ }
176+ delete(c.leases, id)
177+ return nil
178+}
179+func (c *Card) LeaseIDs() []uint32 {
180+ ids := make([]uint32, 0, len(c.leases))
181+ for id := range c.leases {
182+ ids = append(ids, id)
183+ }
184+ slices.Sort(ids)
185+ return ids
186+}
187+
188+// ClientFD opens a separate card fd and makes sure it is not a DRM master.
189+func (c *Card) ClientFD() (*os.File, error) {
190+ fd, err := unix.Open(c.path, unix.O_RDWR|unix.O_CLOEXEC, 0)
191+ if err != nil {
192+ return nil, err
193+ }
194+ if err = ioctl(fd, ioctlDropMaster, nil); err != nil && !errors.Is(err, unix.EINVAL) && !errors.Is(err, unix.EACCES) {
195+ unix.Close(fd)
196+ return nil, err
197+ }
198+ return os.NewFile(uintptr(fd), c.path), nil
199+}
200+
201+// FinishedLeases drains IDs revoked after a connector disappeared during Scan.
202+func (c *Card) FinishedLeases() []uint32 {
203+ ids := c.finished
204+ c.finished = nil
205+ return ids
206+}
A internal/adapters/drm/lease_test.go +84 -0
@@ -0,0 +1,84 @@
1+package drm
2+
3+import (
4+ "context"
5+ "os"
6+ "slices"
7+ "testing"
8+ "unsafe"
9+
10+ "github.com/bnema/neferwl/internal/logging"
11+ "github.com/stretchr/testify/mock"
12+)
13+
14+func TestLeaseResourceOwnership(t *testing.T) {
15+ k := newMockkms(t)
16+ conn := connector{id: 40, name: "DP-1", connected: true, nonDesktop: true, encoders: []uint32{3}}
17+ c := &Card{path: "/dev/dri/card-test", crtcs: []uint32{30}, outputs: map[string]*Output{}, leasable: map[string]connector{conn.name: conn}, leases: map[uint32]leaseRecord{}, taken: map[uint32]bool{}, k: k}
18+ k.EXPECT().pickCrtc(conn, []uint32{30}, mock.Anything).RunAndReturn(func(_ connector, _ []uint32, used map[uint32]bool) (uint32, error) {
19+ if used[30] {
20+ t.Fatal("CRTC already used")
21+ }
22+ return 30, nil
23+ })
24+ k.EXPECT().planes().Return([]planeRes{{id: 50, possible: 1}}, nil)
25+ k.EXPECT().objProps(uint32(50), uint32(objPlane)).Return(map[string][2]uint64{"type": {1, planePrimary}}, nil)
26+ r, w, err := os.Pipe()
27+ if err != nil {
28+ t.Fatal(err)
29+ }
30+ defer r.Close()
31+ defer w.Close()
32+ k.EXPECT().createLease(mock.Anything).RunAndReturn(func(ids []uint32) (*os.File, uint32, error) {
33+ if !slices.Equal(ids, []uint32{40, 30, 50}) {
34+ t.Errorf("objects %v", ids)
35+ }
36+ return w, 7, nil
37+ })
38+ f, id, err := c.Lease([]string{"DP-1"})
39+ if err != nil || f != w || id != 7 {
40+ t.Fatalf("lease fd %v id %d err %v", f, id, err)
41+ }
42+ if len(c.Leasable()) != 0 {
43+ t.Fatal("leased connector advertised")
44+ }
45+ if _, _, err = c.Lease([]string{"DP-1"}); err == nil {
46+ t.Fatal("duplicate lease allowed")
47+ }
48+ k.EXPECT().revokeLease(uint32(7)).Return(nil)
49+ if err = c.Revoke(7); err != nil {
50+ t.Fatal(err)
51+ }
52+ if len(c.Leasable()) != 1 {
53+ t.Fatal("connector not available after revoke")
54+ }
55+}
56+
57+func TestLeaseABI(t *testing.T) {
58+ if unsafe.Sizeof(modeCreateLease{}) != 24 || unsafe.Sizeof(modeRevokeLease{}) != 4 {
59+ t.Fatal("DRM lease ioctl structs")
60+ }
61+ if ioctlCreateLease>>16&0x3fff != uint64(unsafe.Sizeof(modeCreateLease{})) || ioctlRevokeLease>>16&0x3fff != uint64(unsafe.Sizeof(modeRevokeLease{})) {
62+ t.Fatal("DRM lease ioctl sizes")
63+ }
64+}
65+
66+func TestScanNonDesktopAndRevokeDisconnected(t *testing.T) {
67+ k := newMockkms(t)
68+ conn := connector{id: 40, name: "DP-1", connected: true, nonDesktop: true, modes: []modeInfo{{HDisplay: 1920}}, encoders: []uint32{3}}
69+ c := &Card{k: k, path: "/dev/dri/card-test", crtcs: []uint32{30}, outputs: map[string]*Output{}, leases: map[uint32]leaseRecord{7: {names: []string{"DP-1"}, crtcs: []uint32{30}, planes: []uint32{50}}}, want: Want{}, log: logging.For(context.Background(), "drm"), taken: map[uint32]bool{}}
70+ k.EXPECT().resources().Return([]uint32{30}, []uint32{40}, nil).Once()
71+ k.EXPECT().connector(uint32(40)).Return(conn, nil).Once()
72+ added, _, _, err := c.Scan()
73+ if err != nil || len(added) != 0 {
74+ t.Fatalf("non-desktop output: %v %v", added, err)
75+ }
76+ k.EXPECT().resources().Return([]uint32{30}, []uint32{40}, nil).Once()
77+ conn.connected = false
78+ k.EXPECT().connector(uint32(40)).Return(conn, nil).Once()
79+ k.EXPECT().revokeLease(uint32(7)).Return(nil)
80+ _, _, _, err = c.Scan()
81+ if err != nil || !slices.Equal(c.FinishedLeases(), []uint32{7}) {
82+ t.Fatalf("hotplug revoke: %v", err)
83+ }
84+}
@@ -0,0 +1,84 @@
1+package drm
2+
3+import (
4+ "context"
5+ "os"
6+ "slices"
7+ "testing"
8+ "unsafe"
9+
10+ "github.com/bnema/neferwl/internal/logging"
11+ "github.com/stretchr/testify/mock"
12+)
13+
14+func TestLeaseResourceOwnership(t *testing.T) {
15+ k := newMockkms(t)
16+ conn := connector{id: 40, name: "DP-1", connected: true, nonDesktop: true, encoders: []uint32{3}}
17+ c := &Card{path: "/dev/dri/card-test", crtcs: []uint32{30}, outputs: map[string]*Output{}, leasable: map[string]connector{conn.name: conn}, leases: map[uint32]leaseRecord{}, taken: map[uint32]bool{}, k: k}
18+ k.EXPECT().pickCrtc(conn, []uint32{30}, mock.Anything).RunAndReturn(func(_ connector, _ []uint32, used map[uint32]bool) (uint32, error) {
19+ if used[30] {
20+ t.Fatal("CRTC already used")
21+ }
22+ return 30, nil
23+ })
24+ k.EXPECT().planes().Return([]planeRes{{id: 50, possible: 1}}, nil)
25+ k.EXPECT().objProps(uint32(50), uint32(objPlane)).Return(map[string][2]uint64{"type": {1, planePrimary}}, nil)
26+ r, w, err := os.Pipe()
27+ if err != nil {
28+ t.Fatal(err)
29+ }
30+ defer r.Close()
31+ defer w.Close()
32+ k.EXPECT().createLease(mock.Anything).RunAndReturn(func(ids []uint32) (*os.File, uint32, error) {
33+ if !slices.Equal(ids, []uint32{40, 30, 50}) {
34+ t.Errorf("objects %v", ids)
35+ }
36+ return w, 7, nil
37+ })
38+ f, id, err := c.Lease([]string{"DP-1"})
39+ if err != nil || f != w || id != 7 {
40+ t.Fatalf("lease fd %v id %d err %v", f, id, err)
41+ }
42+ if len(c.Leasable()) != 0 {
43+ t.Fatal("leased connector advertised")
44+ }
45+ if _, _, err = c.Lease([]string{"DP-1"}); err == nil {
46+ t.Fatal("duplicate lease allowed")
47+ }
48+ k.EXPECT().revokeLease(uint32(7)).Return(nil)
49+ if err = c.Revoke(7); err != nil {
50+ t.Fatal(err)
51+ }
52+ if len(c.Leasable()) != 1 {
53+ t.Fatal("connector not available after revoke")
54+ }
55+}
56+
57+func TestLeaseABI(t *testing.T) {
58+ if unsafe.Sizeof(modeCreateLease{}) != 24 || unsafe.Sizeof(modeRevokeLease{}) != 4 {
59+ t.Fatal("DRM lease ioctl structs")
60+ }
61+ if ioctlCreateLease>>16&0x3fff != uint64(unsafe.Sizeof(modeCreateLease{})) || ioctlRevokeLease>>16&0x3fff != uint64(unsafe.Sizeof(modeRevokeLease{})) {
62+ t.Fatal("DRM lease ioctl sizes")
63+ }
64+}
65+
66+func TestScanNonDesktopAndRevokeDisconnected(t *testing.T) {
67+ k := newMockkms(t)
68+ conn := connector{id: 40, name: "DP-1", connected: true, nonDesktop: true, modes: []modeInfo{{HDisplay: 1920}}, encoders: []uint32{3}}
69+ c := &Card{k: k, path: "/dev/dri/card-test", crtcs: []uint32{30}, outputs: map[string]*Output{}, leases: map[uint32]leaseRecord{7: {names: []string{"DP-1"}, crtcs: []uint32{30}, planes: []uint32{50}}}, want: Want{}, log: logging.For(context.Background(), "drm"), taken: map[uint32]bool{}}
70+ k.EXPECT().resources().Return([]uint32{30}, []uint32{40}, nil).Once()
71+ k.EXPECT().connector(uint32(40)).Return(conn, nil).Once()
72+ added, _, _, err := c.Scan()
73+ if err != nil || len(added) != 0 {
74+ t.Fatalf("non-desktop output: %v %v", added, err)
75+ }
76+ k.EXPECT().resources().Return([]uint32{30}, []uint32{40}, nil).Once()
77+ conn.connected = false
78+ k.EXPECT().connector(uint32(40)).Return(conn, nil).Once()
79+ k.EXPECT().revokeLease(uint32(7)).Return(nil)
80+ _, _, _, err = c.Scan()
81+ if err != nil || !slices.Equal(c.FinishedLeases(), []uint32{7}) {
82+ t.Fatalf("hotplug revoke: %v", err)
83+ }
84+}
M internal/adapters/drm/scanout.go +3 -0
@@ -120,6 +120,9 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo
120120 switch {
121121 case c.DMABuf == nil:
122122 return c, "not_dmabuf"
123+ case c.Fade > 0:
124+ // The plane would show it unfaded (wp_alpha_modifier_v1).
125+ return c, "fade"
123126 case len(c.Children) > 0:
124127 return c, "subsurfaces"
125128 case c.Transform != 0:
@@ -120,6 +120,9 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo
120 switch {120 switch {
121 case c.DMABuf == nil:121 case c.DMABuf == nil:
122 return c, "not_dmabuf"122 return c, "not_dmabuf"
123+ case c.Fade > 0:
124+ // The plane would show it unfaded (wp_alpha_modifier_v1).
125+ return c, "fade"
123 case len(c.Children) > 0:126 case len(c.Children) > 0:
124 return c, "subsurfaces"127 return c, "subsurfaces"
125 case c.Transform != 0:128 case c.Transform != 0:
M internal/adapters/drm/scanout_test.go +1 -0
@@ -26,6 +26,7 @@ func TestScanoutCandidate(t *testing.T) {
2626 }, nil, ""},
2727 {"no window", func(s *ports.Scene) { s.Windows = nil }, nil, "no_fullscreen"},
2828 {"dimmed", func(s *ports.Scene) { s.Windows[0].Dim = 0.5 }, nil, "dim"},
29+ {"faded", nil, func(c *ports.SurfaceContent) { c.Fade = 0.5 }, "fade"},
2930 {"tiled", func(s *ports.Scene) { s.Windows[0].Fullscreen = false }, nil, "other_windows"},
3031 {"float above", func(s *ports.Scene) {
3132 s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Rect: ports.Rect{W: 10, H: 10}})
@@ -26,6 +26,7 @@ func TestScanoutCandidate(t *testing.T) {
26 }, nil, ""},26 }, nil, ""},
27 {"no window", func(s *ports.Scene) { s.Windows = nil }, nil, "no_fullscreen"},27 {"no window", func(s *ports.Scene) { s.Windows = nil }, nil, "no_fullscreen"},
28 {"dimmed", func(s *ports.Scene) { s.Windows[0].Dim = 0.5 }, nil, "dim"},28 {"dimmed", func(s *ports.Scene) { s.Windows[0].Dim = 0.5 }, nil, "dim"},
29+ {"faded", nil, func(c *ports.SurfaceContent) { c.Fade = 0.5 }, "fade"},
29 {"tiled", func(s *ports.Scene) { s.Windows[0].Fullscreen = false }, nil, "other_windows"},30 {"tiled", func(s *ports.Scene) { s.Windows[0].Fullscreen = false }, nil, "other_windows"},
30 {"float above", func(s *ports.Scene) {31 {"float above", func(s *ports.Scene) {
31 s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Rect: ports.Rect{W: 10, H: 10}})32 s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Rect: ports.Rect{W: 10, H: 10}})
M internal/adapters/libinput/libinput.go +2 -2
@@ -605,9 +605,9 @@ func (p *pointer) set(x, y float64) (float64, float64) {
605605
606606 // constrain applies a new constraint. Entering or leaving a lock resyncs
607607 // the pointer to core's cursor, which stood still meanwhile; a confined
608-// pointer moves inside.
608+// pointer moves inside. A warp moves it to core's cursor.
609609 func (p *pointer) constrain(c ports.PointerConstraint) {
610- if c.Mode == ports.ConstraintLock || p.constraint.Mode == ports.ConstraintLock {
610+ if c.Warp || c.Mode == ports.ConstraintLock || p.constraint.Mode == ports.ConstraintLock {
611611 p.x, p.y = p.layout.Clamp(c.X, c.Y, c.X, c.Y)
612612 }
613613 p.constraint = c
@@ -605,9 +605,9 @@ func (p *pointer) set(x, y float64) (float64, float64) {
605 605
606 // constrain applies a new constraint. Entering or leaving a lock resyncs606 // constrain applies a new constraint. Entering or leaving a lock resyncs
607 // the pointer to core's cursor, which stood still meanwhile; a confined607 // the pointer to core's cursor, which stood still meanwhile; a confined
608-// pointer moves inside.608+// pointer moves inside. A warp moves it to core's cursor.
609 func (p *pointer) constrain(c ports.PointerConstraint) {609 func (p *pointer) constrain(c ports.PointerConstraint) {
610- if c.Mode == ports.ConstraintLock || p.constraint.Mode == ports.ConstraintLock {610+ if c.Warp || c.Mode == ports.ConstraintLock || p.constraint.Mode == ports.ConstraintLock {
611 p.x, p.y = p.layout.Clamp(c.X, c.Y, c.X, c.Y)611 p.x, p.y = p.layout.Clamp(c.X, c.Y, c.X, c.Y)
612 }612 }
613 p.constraint = c613 p.constraint = c
M internal/adapters/libinput/libinput_test.go +5 -0
@@ -169,4 +169,9 @@ func TestPointerConstraints(t *testing.T) {
169169 if p.x != 21 || p.y != 31 {
170170 t.Fatal(p.x, p.y)
171171 }
172+ // A warp moves a free pointer to core's cursor.
173+ p.constrain(ports.PointerConstraint{X: 70, Y: 40, Warp: true})
174+ if p.x != 70 || p.y != 40 {
175+ t.Fatal(p.x, p.y)
176+ }
172177 }
@@ -169,4 +169,9 @@ func TestPointerConstraints(t *testing.T) {
169 if p.x != 21 || p.y != 31 {169 if p.x != 21 || p.y != 31 {
170 t.Fatal(p.x, p.y)170 t.Fatal(p.x, p.y)
171 }171 }
172+ // A warp moves a free pointer to core's cursor.
173+ p.constrain(ports.PointerConstraint{X: 70, Y: 40, Warp: true})
174+ if p.x != 70 || p.y != 40 {
175+ t.Fatal(p.x, p.y)
176+ }
172 }177 }
M internal/adapters/vulkan/compose.go +2 -0
@@ -360,6 +360,8 @@ func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, source [4]f
360360 flags |= flagSmooth
361361 }
362362 dr.pc.misc = [4]uint32{mode, flags, uint32(r.width), uint32(r.height)}
363+ // color.a is the opacity of a content draw (wp_alpha_modifier_v1).
364+ dr.pc.color[3] = 1
363365 return dr
364366 }
365367
@@ -360,6 +360,8 @@ func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, source [4]f
360 flags |= flagSmooth360 flags |= flagSmooth
361 }361 }
362 dr.pc.misc = [4]uint32{mode, flags, uint32(r.width), uint32(r.height)}362 dr.pc.misc = [4]uint32{mode, flags, uint32(r.width), uint32(r.height)}
363+ // color.a is the opacity of a content draw (wp_alpha_modifier_v1).
364+ dr.pc.color[3] = 1
363 return dr365 return dr
364 }366 }
365 367
M internal/adapters/vulkan/renderer_test.go +19 -0
@@ -92,6 +92,25 @@ func TestRendererRender(t *testing.T) {
9292 check(s, map[image.Point]color.RGBA{{5, 5}: bg})
9393 }
9494
95+// A faded window blends with the background (wp_alpha_modifier_v1).
96+func TestRendererFade(t *testing.T) {
97+ r, err := New(64, 48)
98+ if err != nil {
99+ t.Skipf("Vulkan unavailable: %v", err)
100+ }
101+ defer r.Close()
102+ c := solidContent(t, 64, 48, color.RGBA{200, 0, 0, 255})
103+ c.Fade = 0.5
104+ c.Opaque = false
105+ s := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}}
106+ if err := render(r, s, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
107+ t.Fatal(err)
108+ }
109+ if got := r.Pixels().At(10, 10).(color.RGBA); got.R < 99 || got.R > 101 || got.G != 0 || got.B != 0 {
110+ t.Fatalf("faded pixel %v", got)
111+ }
112+}
113+
95114 func TestRendererContents(t *testing.T) {
96115 r, err := New(64, 48)
97116 if err != nil {
@@ -92,6 +92,25 @@ func TestRendererRender(t *testing.T) {
92 check(s, map[image.Point]color.RGBA{{5, 5}: bg})92 check(s, map[image.Point]color.RGBA{{5, 5}: bg})
93 }93 }
94 94
95+// A faded window blends with the background (wp_alpha_modifier_v1).
96+func TestRendererFade(t *testing.T) {
97+ r, err := New(64, 48)
98+ if err != nil {
99+ t.Skipf("Vulkan unavailable: %v", err)
100+ }
101+ defer r.Close()
102+ c := solidContent(t, 64, 48, color.RGBA{200, 0, 0, 255})
103+ c.Fade = 0.5
104+ c.Opaque = false
105+ s := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}}
106+ if err := render(r, s, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
107+ t.Fatal(err)
108+ }
109+ if got := r.Pixels().At(10, 10).(color.RGBA); got.R < 99 || got.R > 101 || got.G != 0 || got.B != 0 {
110+ t.Fatalf("faded pixel %v", got)
111+ }
112+}
113+
95 func TestRendererContents(t *testing.T) {114 func TestRendererContents(t *testing.T) {
96 r, err := New(64, 48)115 r, err := New(64, 48)
97 if err != nil {116 if err != nil {
M internal/adapters/vulkan/scene.go +7 -4
@@ -288,7 +288,7 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon
288288 return
289289 }
290290 dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeImage, content.Opaque)
291- r.setContentColor(&dr, content.Color)
291+ r.setContentColor(&dr, content)
292292 if im.yuv {
293293 dr.pc.misc[1] |= flagYUV
294294 if content.DMABuf.Format == fourcc('P', '0', '1', '0') {
@@ -324,14 +324,17 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon
324324 return
325325 }
326326 dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeBuffer, content.Opaque)
327- r.setContentColor(&dr, content.Color)
327+ r.setContentColor(&dr, content)
328328 dr.set = c.set
329329 dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
330330 w.draws = append(w.draws, dr)
331331 }
332332
333-// setContentColor selects only color encodings the protocol accepts.
334-func (r *Renderer) setContentColor(dr *draw, c ports.SurfaceColor) {
333+// setContentColor selects only color encodings the protocol accepts, and
334+// the surface's fade.
335+func (r *Renderer) setContentColor(dr *draw, content *ports.SurfaceContent) {
336+ dr.pc.color[3] = 1 - max(0, min(1, content.Fade))
337+ c := content.Color
335338 if c.IsPQ2020() {
336339 dr.pc.misc[1] |= flagPQ
337340 } else if c.IsExtendedLinear() {
@@ -288,7 +288,7 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon
288 return288 return
289 }289 }
290 dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeImage, content.Opaque)290 dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeImage, content.Opaque)
291- r.setContentColor(&dr, content.Color)291+ r.setContentColor(&dr, content)
292 if im.yuv {292 if im.yuv {
293 dr.pc.misc[1] |= flagYUV293 dr.pc.misc[1] |= flagYUV
294 if content.DMABuf.Format == fourcc('P', '0', '1', '0') {294 if content.DMABuf.Format == fourcc('P', '0', '1', '0') {
@@ -324,14 +324,17 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon
324 return324 return
325 }325 }
326 dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeBuffer, content.Opaque)326 dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeBuffer, content.Opaque)
327- r.setContentColor(&dr, content.Color)327+ r.setContentColor(&dr, content)
328 dr.set = c.set328 dr.set = c.set
329 dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}329 dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
330 w.draws = append(w.draws, dr)330 w.draws = append(w.draws, dr)
331 }331 }
332 332
333-// setContentColor selects only color encodings the protocol accepts.333+// setContentColor selects only color encodings the protocol accepts, and
334-func (r *Renderer) setContentColor(dr *draw, c ports.SurfaceColor) {334+// the surface's fade.
335+func (r *Renderer) setContentColor(dr *draw, content *ports.SurfaceContent) {
336+ dr.pc.color[3] = 1 - max(0, min(1, content.Fade))
337+ c := content.Color
335 if c.IsPQ2020() {338 if c.IsPQ2020() {
336 dr.pc.misc[1] |= flagPQ339 dr.pc.misc[1] |= flagPQ
337 } else if c.IsExtendedLinear() {340 } else if c.IsExtendedLinear() {
M internal/adapters/vulkan/shaders/compose.frag +4 -0
@@ -132,5 +132,9 @@ void main() {
132132 c.rgb = mix(v * 12.92, 1.055 * pow(v, vec3(1.0 / 2.4)) - 0.055, greaterThan(v, vec3(0.0031308))) * c.a;
133133 }
134134 }
135+ if (d.misc.x != modeSolid) {
136+ // The surface's alpha modifier; premultiplied, it scales all four.
137+ c *= d.color.a;
138+ }
135139 color = c;
136140 }
@@ -132,5 +132,9 @@ void main() {
132 c.rgb = mix(v * 12.92, 1.055 * pow(v, vec3(1.0 / 2.4)) - 0.055, greaterThan(v, vec3(0.0031308))) * c.a;132 c.rgb = mix(v * 12.92, 1.055 * pow(v, vec3(1.0 / 2.4)) - 0.055, greaterThan(v, vec3(0.0031308))) * c.a;
133 }133 }
134 }134 }
135+ if (d.misc.x != modeSolid) {
136+ // The surface's alpha modifier; premultiplied, it scales all four.
137+ c *= d.color.a;
138+ }
135 color = c;139 color = c;
136 }140 }
M internal/adapters/vulkan/shaders/compose.frag.spv +0 -0 binary file: no text diff

binary file: no text diff

M internal/adapters/vulkan/shaders/compose_hdr.frag +4 -0
@@ -130,5 +130,9 @@ void main() {
130130 (d.misc.y & flagExtendedLinear) != 0u ? c.rgb / c.a * (80.0 / d.color.x) : decodeSRGB(c.rgb / c.a);
131131 c.rgb = linearRGB * c.a;
132132 }
133+ if (d.misc.x != modeSolid) {
134+ // The surface's alpha modifier; premultiplied, it scales all four.
135+ c *= d.color.a;
136+ }
133137 color = c;
134138 }
@@ -130,5 +130,9 @@ void main() {
130 (d.misc.y & flagExtendedLinear) != 0u ? c.rgb / c.a * (80.0 / d.color.x) : decodeSRGB(c.rgb / c.a);130 (d.misc.y & flagExtendedLinear) != 0u ? c.rgb / c.a * (80.0 / d.color.x) : decodeSRGB(c.rgb / c.a);
131 c.rgb = linearRGB * c.a;131 c.rgb = linearRGB * c.a;
132 }132 }
133+ if (d.misc.x != modeSolid) {
134+ // The surface's alpha modifier; premultiplied, it scales all four.
135+ c *= d.color.a;
136+ }
133 color = c;137 color = c;
134 }138 }
M internal/adapters/vulkan/shaders/compose_hdr.frag.spv +0 -0 binary file: no text diff

binary file: no text diff

A internal/adapters/wayland/alpha.go +61 -0
@@ -0,0 +1,61 @@
1+package wayland
2+
3+import (
4+ "math"
5+
6+ "github.com/bnema/purego-libwayland/protocol/alphamodifier"
7+ "github.com/bnema/purego-libwayland/protocol/wayland"
8+ "github.com/bnema/purego-libwayland/server"
9+)
10+
11+// wp_alpha_modifier_v1: a client fades a whole surface (Wine's layered
12+// windows) without redrawing it. The multiplier is double-buffered and
13+// travels with the content as Fade; the renderer blends it.
14+
15+func registerAlphaModifier(d *server.Display, s *Server) error {
16+ return alphamodifier.NewWpAlphaModifierV1Global(d, 1, func(c server.Client, v, id uint32) {
17+ _, _ = alphamodifier.NewWpAlphaModifierV1(c, int32(v), id, alphaManager{s})
18+ })
19+}
20+
21+type alphaManager struct{ server *Server }
22+
23+func (alphaManager) Destroy(*alphamodifier.WpAlphaModifierV1) {}
24+
25+func (m alphaManager) GetSurface(r *alphamodifier.WpAlphaModifierV1, id uint32, surf *wayland.Surface) {
26+ state := m.server.surfaceOf(surf)
27+ if state != nil && state.alpha != nil {
28+ r.PostError(uint32(alphamodifier.WpAlphaModifierV1ErrorAlreadyConstructed), "surface already has an alpha modifier")
29+ return
30+ }
31+ h := &alphaHandler{surface: state}
32+ res, err := alphamodifier.NewWpAlphaModifierSurfaceV1(r.Client(), r.Version(), id, h)
33+ if err != nil || state == nil {
34+ return
35+ }
36+ state.alpha = h
37+ res.OnDestroy = func() {
38+ // Destroying it sets the multiplier back to opaque, double-buffered.
39+ if state.alpha == h {
40+ state.alpha = nil
41+ state.next.fade = 0
42+ }
43+ }
44+}
45+
46+type alphaHandler struct{ surface *surface }
47+
48+func (*alphaHandler) Destroy(*alphamodifier.WpAlphaModifierSurfaceV1) {}
49+
50+func (h *alphaHandler) SetMultiplier(r *alphamodifier.WpAlphaModifierSurfaceV1, factor uint32) {
51+ if h.surface == nil || h.surface.destroyed || h.surface.alpha != h {
52+ r.PostError(uint32(alphamodifier.WpAlphaModifierSurfaceV1ErrorNoSurface), "wl_surface was destroyed")
53+ return
54+ }
55+ h.surface.next.fade = fade(factor)
56+}
57+
58+// fade turns a multiplier (0 transparent, UINT32_MAX opaque) into Fade.
59+func fade(factor uint32) float32 {
60+ return float32(1 - float64(factor)/math.MaxUint32)
61+}
@@ -0,0 +1,61 @@
1+package wayland
2+
3+import (
4+ "math"
5+
6+ "github.com/bnema/purego-libwayland/protocol/alphamodifier"
7+ "github.com/bnema/purego-libwayland/protocol/wayland"
8+ "github.com/bnema/purego-libwayland/server"
9+)
10+
11+// wp_alpha_modifier_v1: a client fades a whole surface (Wine's layered
12+// windows) without redrawing it. The multiplier is double-buffered and
13+// travels with the content as Fade; the renderer blends it.
14+
15+func registerAlphaModifier(d *server.Display, s *Server) error {
16+ return alphamodifier.NewWpAlphaModifierV1Global(d, 1, func(c server.Client, v, id uint32) {
17+ _, _ = alphamodifier.NewWpAlphaModifierV1(c, int32(v), id, alphaManager{s})
18+ })
19+}
20+
21+type alphaManager struct{ server *Server }
22+
23+func (alphaManager) Destroy(*alphamodifier.WpAlphaModifierV1) {}
24+
25+func (m alphaManager) GetSurface(r *alphamodifier.WpAlphaModifierV1, id uint32, surf *wayland.Surface) {
26+ state := m.server.surfaceOf(surf)
27+ if state != nil && state.alpha != nil {
28+ r.PostError(uint32(alphamodifier.WpAlphaModifierV1ErrorAlreadyConstructed), "surface already has an alpha modifier")
29+ return
30+ }
31+ h := &alphaHandler{surface: state}
32+ res, err := alphamodifier.NewWpAlphaModifierSurfaceV1(r.Client(), r.Version(), id, h)
33+ if err != nil || state == nil {
34+ return
35+ }
36+ state.alpha = h
37+ res.OnDestroy = func() {
38+ // Destroying it sets the multiplier back to opaque, double-buffered.
39+ if state.alpha == h {
40+ state.alpha = nil
41+ state.next.fade = 0
42+ }
43+ }
44+}
45+
46+type alphaHandler struct{ surface *surface }
47+
48+func (*alphaHandler) Destroy(*alphamodifier.WpAlphaModifierSurfaceV1) {}
49+
50+func (h *alphaHandler) SetMultiplier(r *alphamodifier.WpAlphaModifierSurfaceV1, factor uint32) {
51+ if h.surface == nil || h.surface.destroyed || h.surface.alpha != h {
52+ r.PostError(uint32(alphamodifier.WpAlphaModifierSurfaceV1ErrorNoSurface), "wl_surface was destroyed")
53+ return
54+ }
55+ h.surface.next.fade = fade(factor)
56+}
57+
58+// fade turns a multiplier (0 transparent, UINT32_MAX opaque) into Fade.
59+func fade(factor uint32) float32 {
60+ return float32(1 - float64(factor)/math.MaxUint32)
61+}
A internal/adapters/wayland/alpha_test.go +79 -0
@@ -0,0 +1,79 @@
1+package wayland
2+
3+import (
4+ "math"
5+ "testing"
6+ "time"
7+
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/purego-libwayland/protocol/alphamodifier"
10+ "github.com/bnema/purego-libwayland/protocol/wayland"
11+)
12+
13+// contentMatching waits for a content of the window that matches want.
14+func contentMatching(t *testing.T, contents <-chan ports.SurfaceContent, id ports.WindowID, want func(ports.SurfaceContent) bool) ports.SurfaceContent {
15+ t.Helper()
16+ deadline := time.After(2 * time.Second)
17+ for {
18+ select {
19+ case c := <-contents:
20+ if c.ID == id && want(c) {
21+ return c
22+ }
23+ case <-deadline:
24+ t.Fatal("no matching content")
25+ }
26+ }
27+}
28+
29+// The multiplier is double-buffered, fades the content and makes it
30+// translucent; destroying the modifier restores the surface on commit.
31+func TestAlphaModifier(t *testing.T) {
32+ s, events, _, contents, dir := contentServer(t)
33+ c := protocolClient(t, s, dir)
34+ w, surf, _ := surfaceMapper(t, c, events)()
35+ comp := bindProtocol(t, c, "wl_compositor")
36+ region := c.AllocateID()
37+ registerProtocol(t, c, region)
38+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region)
39+ requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1))
40+ requestProtocol(t, c, surf, wayland.SurfaceRequestSetOpaqueRegion, region)
41+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
42+ contentMatching(t, contents, w.ID, func(c ports.SurfaceContent) bool { return c.Opaque })
43+
44+ manager := bindProtocol(t, c, "wp_alpha_modifier_v1")
45+ mod := c.AllocateID()
46+ registerProtocol(t, c, mod)
47+ requestProtocol(t, c, manager, alphamodifier.WpAlphaModifierV1RequestGetSurface, mod, surf)
48+ requestProtocol(t, c, mod, alphamodifier.WpAlphaModifierSurfaceV1RequestSetMultiplier, uint32(math.MaxUint32/4))
49+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
50+ got := contentMatching(t, contents, w.ID, func(c ports.SurfaceContent) bool { return c.Fade > 0 })
51+ if got.Opaque || math.Abs(float64(got.Fade)-0.75) > 1e-6 {
52+ t.Fatalf("faded content %+v", got)
53+ }
54+
55+ requestProtocol(t, c, mod, alphamodifier.WpAlphaModifierSurfaceV1RequestDestroy)
56+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
57+ got = contentMatching(t, contents, w.ID, func(c ports.SurfaceContent) bool { return c.Fade == 0 })
58+ if !got.Opaque {
59+ t.Fatalf("restored content %+v", got)
60+ }
61+
62+ // A second modifier on the same surface is a protocol error.
63+ mod2, mod3 := c.AllocateID(), c.AllocateID()
64+ registerProtocol(t, c, mod2)
65+ registerProtocol(t, c, mod3)
66+ requestProtocol(t, c, manager, alphamodifier.WpAlphaModifierV1RequestGetSurface, mod2, surf)
67+ requestProtocol(t, c, manager, alphamodifier.WpAlphaModifierV1RequestGetSurface, mod3, surf)
68+ if err := c.Roundtrip(); err == nil {
69+ t.Fatal("second modifier accepted")
70+ }
71+}
72+
73+func TestFade(t *testing.T) {
74+ for factor, want := range map[uint32]float32{0: 1, math.MaxUint32: 0} {
75+ if got := fade(factor); got != want {
76+ t.Fatalf("fade(%d) = %v, want %v", factor, got, want)
77+ }
78+ }
79+}
@@ -0,0 +1,79 @@
1+package wayland
2+
3+import (
4+ "math"
5+ "testing"
6+ "time"
7+
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/purego-libwayland/protocol/alphamodifier"
10+ "github.com/bnema/purego-libwayland/protocol/wayland"
11+)
12+
13+// contentMatching waits for a content of the window that matches want.
14+func contentMatching(t *testing.T, contents <-chan ports.SurfaceContent, id ports.WindowID, want func(ports.SurfaceContent) bool) ports.SurfaceContent {
15+ t.Helper()
16+ deadline := time.After(2 * time.Second)
17+ for {
18+ select {
19+ case c := <-contents:
20+ if c.ID == id && want(c) {
21+ return c
22+ }
23+ case <-deadline:
24+ t.Fatal("no matching content")
25+ }
26+ }
27+}
28+
29+// The multiplier is double-buffered, fades the content and makes it
30+// translucent; destroying the modifier restores the surface on commit.
31+func TestAlphaModifier(t *testing.T) {
32+ s, events, _, contents, dir := contentServer(t)
33+ c := protocolClient(t, s, dir)
34+ w, surf, _ := surfaceMapper(t, c, events)()
35+ comp := bindProtocol(t, c, "wl_compositor")
36+ region := c.AllocateID()
37+ registerProtocol(t, c, region)
38+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region)
39+ requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1))
40+ requestProtocol(t, c, surf, wayland.SurfaceRequestSetOpaqueRegion, region)
41+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
42+ contentMatching(t, contents, w.ID, func(c ports.SurfaceContent) bool { return c.Opaque })
43+
44+ manager := bindProtocol(t, c, "wp_alpha_modifier_v1")
45+ mod := c.AllocateID()
46+ registerProtocol(t, c, mod)
47+ requestProtocol(t, c, manager, alphamodifier.WpAlphaModifierV1RequestGetSurface, mod, surf)
48+ requestProtocol(t, c, mod, alphamodifier.WpAlphaModifierSurfaceV1RequestSetMultiplier, uint32(math.MaxUint32/4))
49+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
50+ got := contentMatching(t, contents, w.ID, func(c ports.SurfaceContent) bool { return c.Fade > 0 })
51+ if got.Opaque || math.Abs(float64(got.Fade)-0.75) > 1e-6 {
52+ t.Fatalf("faded content %+v", got)
53+ }
54+
55+ requestProtocol(t, c, mod, alphamodifier.WpAlphaModifierSurfaceV1RequestDestroy)
56+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
57+ got = contentMatching(t, contents, w.ID, func(c ports.SurfaceContent) bool { return c.Fade == 0 })
58+ if !got.Opaque {
59+ t.Fatalf("restored content %+v", got)
60+ }
61+
62+ // A second modifier on the same surface is a protocol error.
63+ mod2, mod3 := c.AllocateID(), c.AllocateID()
64+ registerProtocol(t, c, mod2)
65+ registerProtocol(t, c, mod3)
66+ requestProtocol(t, c, manager, alphamodifier.WpAlphaModifierV1RequestGetSurface, mod2, surf)
67+ requestProtocol(t, c, manager, alphamodifier.WpAlphaModifierV1RequestGetSurface, mod3, surf)
68+ if err := c.Roundtrip(); err == nil {
69+ t.Fatal("second modifier accepted")
70+ }
71+}
72+
73+func TestFade(t *testing.T) {
74+ for factor, want := range map[uint32]float32{0: 1, math.MaxUint32: 0} {
75+ if got := fade(factor); got != want {
76+ t.Fatalf("fade(%d) = %v, want %v", factor, got, want)
77+ }
78+ }
79+}
M internal/adapters/wayland/colormanagement.go +13 -3
@@ -60,13 +60,16 @@ func (s *Server) outputColor(o *output) colorDescription {
6060 return d
6161 }
6262 func registerColorManagement(d *server.Display, s *Server) error {
63- return cm.NewWpColorManagerV1Global(d, 2, func(c server.Client, v, id uint32) {
63+ return cm.NewWpColorManagerV1Global(d, 3, func(c server.Client, v, id uint32) {
6464 r, err := cm.NewWpColorManagerV1(c, int32(v), id, colorManager{s})
6565 if err != nil {
6666 return
6767 }
6868 r.SendSupportedIntent(uint32(cm.WpColorManagerV1RenderIntentPerceptual))
6969 r.SendSupportedFeature(uint32(cm.WpColorManagerV1FeatureParametric))
70+ if v >= 3 {
71+ r.SendSupportedFeature(uint32(cm.WpColorManagerV1FeatureWindowsBt2100))
72+ }
7073 for _, tf := range []uint32{pq, extLinear, uint32(cm.WpColorManagerV1TransferFunctionSrgb)} {
7174 r.SendSupportedTfNamed(tf)
7275 }
@@ -138,8 +141,15 @@ func (m colorManager) CreateParametricCreator(r *cm.WpColorManagerV1, id uint32)
138141 func (colorManager) CreateWindowsScrgb(r *cm.WpColorManagerV1, _ uint32) {
139142 r.PostError(uint32(cm.WpColorManagerV1ErrorUnsupportedFeature), "scRGB unsupported")
140143 }
141-func (colorManager) CreateWindowsBt2100(r *cm.WpColorManagerV1, _ uint32) {
142- r.PostError(uint32(cm.WpColorManagerV1ErrorUnsupportedFeature), "BT2100 unsupported")
144+
145+// CreateWindowsBt2100 is BT.2020 primaries with the PQ transfer function,
146+// as Windows drives HDR screens (DXGI HDR10 swapchains through Wine or
147+// DXVK). Its reference white is BT.2408's 203 cd/m²; it has no
148+// get_information.
149+func (m colorManager) CreateWindowsBt2100(r *cm.WpColorManagerV1, id uint32) {
150+ d := colorDescription{SurfaceColor: SurfaceColor{TF: pq, Primaries: bt2020, Set: true}, min: 50, max: 10000, white: 203}
151+ d.targetMin, d.targetMax = d.min, d.max
152+ m.s.createColorDescription(r.Client(), r.Version(), id, d, 0, false)
143153 }
144154 func (colorManager) GetImageDescription(r *cm.WpColorManagerV1, _ uint32, _ *cm.WpImageDescriptionReferenceV1) {
145155 r.PostError(uint32(cm.WpColorManagerV1ErrorUnsupportedFeature), "references unsupported")
@@ -60,13 +60,16 @@ func (s *Server) outputColor(o *output) colorDescription {
60 return d60 return d
61 }61 }
62 func registerColorManagement(d *server.Display, s *Server) error {62 func registerColorManagement(d *server.Display, s *Server) error {
63- return cm.NewWpColorManagerV1Global(d, 2, func(c server.Client, v, id uint32) {63+ return cm.NewWpColorManagerV1Global(d, 3, func(c server.Client, v, id uint32) {
64 r, err := cm.NewWpColorManagerV1(c, int32(v), id, colorManager{s})64 r, err := cm.NewWpColorManagerV1(c, int32(v), id, colorManager{s})
65 if err != nil {65 if err != nil {
66 return66 return
67 }67 }
68 r.SendSupportedIntent(uint32(cm.WpColorManagerV1RenderIntentPerceptual))68 r.SendSupportedIntent(uint32(cm.WpColorManagerV1RenderIntentPerceptual))
69 r.SendSupportedFeature(uint32(cm.WpColorManagerV1FeatureParametric))69 r.SendSupportedFeature(uint32(cm.WpColorManagerV1FeatureParametric))
70+ if v >= 3 {
71+ r.SendSupportedFeature(uint32(cm.WpColorManagerV1FeatureWindowsBt2100))
72+ }
70 for _, tf := range []uint32{pq, extLinear, uint32(cm.WpColorManagerV1TransferFunctionSrgb)} {73 for _, tf := range []uint32{pq, extLinear, uint32(cm.WpColorManagerV1TransferFunctionSrgb)} {
71 r.SendSupportedTfNamed(tf)74 r.SendSupportedTfNamed(tf)
72 }75 }
@@ -138,8 +141,15 @@ func (m colorManager) CreateParametricCreator(r *cm.WpColorManagerV1, id uint32)
138 func (colorManager) CreateWindowsScrgb(r *cm.WpColorManagerV1, _ uint32) {141 func (colorManager) CreateWindowsScrgb(r *cm.WpColorManagerV1, _ uint32) {
139 r.PostError(uint32(cm.WpColorManagerV1ErrorUnsupportedFeature), "scRGB unsupported")142 r.PostError(uint32(cm.WpColorManagerV1ErrorUnsupportedFeature), "scRGB unsupported")
140 }143 }
141-func (colorManager) CreateWindowsBt2100(r *cm.WpColorManagerV1, _ uint32) {144+
142- r.PostError(uint32(cm.WpColorManagerV1ErrorUnsupportedFeature), "BT2100 unsupported")145+// CreateWindowsBt2100 is BT.2020 primaries with the PQ transfer function,
146+// as Windows drives HDR screens (DXGI HDR10 swapchains through Wine or
147+// DXVK). Its reference white is BT.2408's 203 cd/m²; it has no
148+// get_information.
149+func (m colorManager) CreateWindowsBt2100(r *cm.WpColorManagerV1, id uint32) {
150+ d := colorDescription{SurfaceColor: SurfaceColor{TF: pq, Primaries: bt2020, Set: true}, min: 50, max: 10000, white: 203}
151+ d.targetMin, d.targetMax = d.min, d.max
152+ m.s.createColorDescription(r.Client(), r.Version(), id, d, 0, false)
143 }153 }
144 func (colorManager) GetImageDescription(r *cm.WpColorManagerV1, _ uint32, _ *cm.WpImageDescriptionReferenceV1) {154 func (colorManager) GetImageDescription(r *cm.WpColorManagerV1, _ uint32, _ *cm.WpImageDescriptionReferenceV1) {
145 r.PostError(uint32(cm.WpColorManagerV1ErrorUnsupportedFeature), "references unsupported")155 r.PostError(uint32(cm.WpColorManagerV1ErrorUnsupportedFeature), "references unsupported")
M internal/adapters/wayland/colormanagement_test.go +52 -0
@@ -135,6 +135,58 @@ func TestColorMesaFlow(t *testing.T) {
135135 t.Fatal("unset not committed")
136136 }
137137 }
138+
139+// v3 advertises windows_bt2100; its description is PQ BT.2020, applies to
140+// a surface, and allows no get_information.
141+func TestColorWindowsBT2100(t *testing.T) {
142+ s, c, _ := colorServer(t)
143+ manager := bindVersion(t, c, "wp_color_manager_v1", 3)
144+ caps := watchColor(c, manager)
145+ if err := c.Roundtrip(); err != nil {
146+ t.Fatal(err)
147+ }
148+ var bt2100 bool
149+ for i, op := range caps.events {
150+ if uint32(op) == cm.WpColorManagerV1EventSupportedFeature && caps.values[i][0] == uint32(cm.WpColorManagerV1FeatureWindowsBt2100) {
151+ bt2100 = true
152+ }
153+ }
154+ if !bt2100 {
155+ t.Fatalf("windows_bt2100 not advertised: %v", caps.values)
156+ }
157+ comp := bindProtocol(t, c, "wl_compositor")
158+ wl := c.AllocateID()
159+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, wl)
160+ registerProtocol(t, c, wl)
161+ surfaceID := c.AllocateID()
162+ requestProtocol(t, c, manager, cm.WpColorManagerV1RequestGetSurface, surfaceID, wl)
163+ registerProtocol(t, c, surfaceID)
164+ image := c.AllocateID()
165+ requestProtocol(t, c, manager, cm.WpColorManagerV1RequestCreateWindowsBt2100, image)
166+ ready := watchColor(c, image)
167+ if err := c.Roundtrip(); err != nil {
168+ t.Fatal(err)
169+ }
170+ if len(ready.events) != 1 || uint32(ready.events[0]) != cm.WpImageDescriptionV1EventReady2 {
171+ t.Fatalf("ready %v", ready.events)
172+ }
173+ requestProtocol(t, c, surfaceID, cm.WpColorManagementSurfaceV1RequestSetImageDescription, image, uint32(cm.WpColorManagerV1RenderIntentPerceptual))
174+ requestProtocol(t, c, wl, wayland.SurfaceRequestCommit)
175+ if err := c.Roundtrip(); err != nil {
176+ t.Fatal(err)
177+ }
178+ var got SurfaceColor
179+ s.display.Do(func() { got = s.surfaceColor(wl) })
180+ if got != (SurfaceColor{Set: true, TF: pq, Primaries: bt2020}) {
181+ t.Fatalf("committed color %+v", got)
182+ }
183+ info := c.AllocateID()
184+ requestProtocol(t, c, image, cm.WpImageDescriptionV1RequestGetInformation, info)
185+ if err := c.Roundtrip(); err == nil {
186+ t.Fatal("get_information allowed")
187+ }
188+}
189+
138190 func (s *Server) surfaceColor(id uint32) SurfaceColor {
139191 for r, v := range s.surfaces {
140192 if r.ID() == id {
@@ -135,6 +135,58 @@ func TestColorMesaFlow(t *testing.T) {
135 t.Fatal("unset not committed")135 t.Fatal("unset not committed")
136 }136 }
137 }137 }
138+
139+// v3 advertises windows_bt2100; its description is PQ BT.2020, applies to
140+// a surface, and allows no get_information.
141+func TestColorWindowsBT2100(t *testing.T) {
142+ s, c, _ := colorServer(t)
143+ manager := bindVersion(t, c, "wp_color_manager_v1", 3)
144+ caps := watchColor(c, manager)
145+ if err := c.Roundtrip(); err != nil {
146+ t.Fatal(err)
147+ }
148+ var bt2100 bool
149+ for i, op := range caps.events {
150+ if uint32(op) == cm.WpColorManagerV1EventSupportedFeature && caps.values[i][0] == uint32(cm.WpColorManagerV1FeatureWindowsBt2100) {
151+ bt2100 = true
152+ }
153+ }
154+ if !bt2100 {
155+ t.Fatalf("windows_bt2100 not advertised: %v", caps.values)
156+ }
157+ comp := bindProtocol(t, c, "wl_compositor")
158+ wl := c.AllocateID()
159+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, wl)
160+ registerProtocol(t, c, wl)
161+ surfaceID := c.AllocateID()
162+ requestProtocol(t, c, manager, cm.WpColorManagerV1RequestGetSurface, surfaceID, wl)
163+ registerProtocol(t, c, surfaceID)
164+ image := c.AllocateID()
165+ requestProtocol(t, c, manager, cm.WpColorManagerV1RequestCreateWindowsBt2100, image)
166+ ready := watchColor(c, image)
167+ if err := c.Roundtrip(); err != nil {
168+ t.Fatal(err)
169+ }
170+ if len(ready.events) != 1 || uint32(ready.events[0]) != cm.WpImageDescriptionV1EventReady2 {
171+ t.Fatalf("ready %v", ready.events)
172+ }
173+ requestProtocol(t, c, surfaceID, cm.WpColorManagementSurfaceV1RequestSetImageDescription, image, uint32(cm.WpColorManagerV1RenderIntentPerceptual))
174+ requestProtocol(t, c, wl, wayland.SurfaceRequestCommit)
175+ if err := c.Roundtrip(); err != nil {
176+ t.Fatal(err)
177+ }
178+ var got SurfaceColor
179+ s.display.Do(func() { got = s.surfaceColor(wl) })
180+ if got != (SurfaceColor{Set: true, TF: pq, Primaries: bt2020}) {
181+ t.Fatalf("committed color %+v", got)
182+ }
183+ info := c.AllocateID()
184+ requestProtocol(t, c, image, cm.WpImageDescriptionV1RequestGetInformation, info)
185+ if err := c.Roundtrip(); err == nil {
186+ t.Fatal("get_information allowed")
187+ }
188+}
189+
138 func (s *Server) surfaceColor(id uint32) SurfaceColor {190 func (s *Server) surfaceColor(id uint32) SurfaceColor {
139 for r, v := range s.surfaces {191 for r, v := range s.surfaces {
140 if r.ID() == id {192 if r.ID() == id {
M internal/adapters/wayland/constraints.go +29 -0
@@ -3,6 +3,7 @@ package wayland
33 import (
44 "github.com/bnema/neferwl/internal/ports"
55 "github.com/bnema/purego-libwayland/protocol/pointerconstraints"
6+ "github.com/bnema/purego-libwayland/protocol/pointerwarp"
67 "github.com/bnema/purego-libwayland/protocol/relativepointer"
78 "github.com/bnema/purego-libwayland/protocol/wayland"
89 "github.com/bnema/purego-libwayland/server"
@@ -14,6 +15,11 @@ func registerPointerConstraints(d *server.Display, s *Server) error {
1415 }); err != nil {
1516 return err
1617 }
18+ if err := pointerwarp.NewWpPointerWarpV1Global(d, 1, func(c server.Client, v, id uint32) {
19+ _, _ = pointerwarp.NewWpPointerWarpV1(c, int32(v), id, pointerWarp{s})
20+ }); err != nil {
21+ return err
22+ }
1723 return pointerconstraints.NewZwpPointerConstraintsV1Global(d, 1, func(c server.Client, v, id uint32) {
1824 _, _ = pointerconstraints.NewZwpPointerConstraintsV1(c, int32(v), id, constraintManager{s})
1925 })
@@ -38,6 +44,29 @@ func (m relativeManager) GetRelativePointer(r *relativepointer.ZwpRelativePointe
3844 }
3945 }
4046
47+// pointerWarp is wp_pointer_warp_v1: a client with the pointer moves it on
48+// its window (Wine's SetCursorPos). Warps without the pointer, with a serial
49+// other than that pointer's current enter, or outside the window are ignored.
50+type pointerWarp struct{ server *Server }
51+
52+func (pointerWarp) Destroy(*pointerwarp.WpPointerWarpV1) {}
53+func (h pointerWarp) WarpPointer(r *pointerwarp.WpPointerWarpV1, surf *wayland.Surface, p *wayland.Pointer, x, y server.Fixed, serial uint32) {
54+ s := h.server
55+ if surf == nil || p == nil || p.Client() != r.Client() {
56+ return
57+ }
58+ if enter, ok := s.seat.enters[p.Resource]; !ok || enter != serial {
59+ return
60+ }
61+ w := s.windows[s.seat.pointerFocus]
62+ if w == nil || !w.mapped || w.xdg.resource.Client() != r.Client() || s.surfaces[surf.Resource] != w.xdg.surface {
63+ return
64+ }
65+ // Surface-local to window-local (core's, from the geometry origin).
66+ g := w.xdg.geometry
67+ s.emit(ports.PointerWarp{ID: w.id, X: x.Float() - float64(g.X), Y: y.Float() - float64(g.Y)})
68+}
69+
4170 type relativeHandler struct{}
4271
4372 func (relativeHandler) Destroy(*relativepointer.ZwpRelativePointerV1) {}
@@ -3,6 +3,7 @@ package wayland
3 import (3 import (
4 "github.com/bnema/neferwl/internal/ports"4 "github.com/bnema/neferwl/internal/ports"
5 "github.com/bnema/purego-libwayland/protocol/pointerconstraints"5 "github.com/bnema/purego-libwayland/protocol/pointerconstraints"
6+ "github.com/bnema/purego-libwayland/protocol/pointerwarp"
6 "github.com/bnema/purego-libwayland/protocol/relativepointer"7 "github.com/bnema/purego-libwayland/protocol/relativepointer"
7 "github.com/bnema/purego-libwayland/protocol/wayland"8 "github.com/bnema/purego-libwayland/protocol/wayland"
8 "github.com/bnema/purego-libwayland/server"9 "github.com/bnema/purego-libwayland/server"
@@ -14,6 +15,11 @@ func registerPointerConstraints(d *server.Display, s *Server) error {
14 }); err != nil {15 }); err != nil {
15 return err16 return err
16 }17 }
18+ if err := pointerwarp.NewWpPointerWarpV1Global(d, 1, func(c server.Client, v, id uint32) {
19+ _, _ = pointerwarp.NewWpPointerWarpV1(c, int32(v), id, pointerWarp{s})
20+ }); err != nil {
21+ return err
22+ }
17 return pointerconstraints.NewZwpPointerConstraintsV1Global(d, 1, func(c server.Client, v, id uint32) {23 return pointerconstraints.NewZwpPointerConstraintsV1Global(d, 1, func(c server.Client, v, id uint32) {
18 _, _ = pointerconstraints.NewZwpPointerConstraintsV1(c, int32(v), id, constraintManager{s})24 _, _ = pointerconstraints.NewZwpPointerConstraintsV1(c, int32(v), id, constraintManager{s})
19 })25 })
@@ -38,6 +44,29 @@ func (m relativeManager) GetRelativePointer(r *relativepointer.ZwpRelativePointe
38 }44 }
39 }45 }
40 46
47+// pointerWarp is wp_pointer_warp_v1: a client with the pointer moves it on
48+// its window (Wine's SetCursorPos). Warps without the pointer, with a serial
49+// other than that pointer's current enter, or outside the window are ignored.
50+type pointerWarp struct{ server *Server }
51+
52+func (pointerWarp) Destroy(*pointerwarp.WpPointerWarpV1) {}
53+func (h pointerWarp) WarpPointer(r *pointerwarp.WpPointerWarpV1, surf *wayland.Surface, p *wayland.Pointer, x, y server.Fixed, serial uint32) {
54+ s := h.server
55+ if surf == nil || p == nil || p.Client() != r.Client() {
56+ return
57+ }
58+ if enter, ok := s.seat.enters[p.Resource]; !ok || enter != serial {
59+ return
60+ }
61+ w := s.windows[s.seat.pointerFocus]
62+ if w == nil || !w.mapped || w.xdg.resource.Client() != r.Client() || s.surfaces[surf.Resource] != w.xdg.surface {
63+ return
64+ }
65+ // Surface-local to window-local (core's, from the geometry origin).
66+ g := w.xdg.geometry
67+ s.emit(ports.PointerWarp{ID: w.id, X: x.Float() - float64(g.X), Y: y.Float() - float64(g.Y)})
68+}
69+
41 type relativeHandler struct{}70 type relativeHandler struct{}
42 71
43 func (relativeHandler) Destroy(*relativepointer.ZwpRelativePointerV1) {}72 func (relativeHandler) Destroy(*relativepointer.ZwpRelativePointerV1) {}
M internal/adapters/wayland/constraints_test.go +57 -2
@@ -6,6 +6,7 @@ import (
66
77 "github.com/bnema/neferwl/internal/ports"
88 "github.com/bnema/purego-libwayland/protocol/pointerconstraints"
9+ "github.com/bnema/purego-libwayland/protocol/pointerwarp"
910 "github.com/bnema/purego-libwayland/protocol/relativepointer"
1011 "github.com/bnema/purego-libwayland/protocol/wayland"
1112 "github.com/bnema/purego-libwayland/protocol/xdgshell"
@@ -15,18 +16,22 @@ import (
1516 // eventLog records the opcodes (and first fixed args) a proxy receives.
1617 type eventLog struct {
1718 wlturbo.BaseProxy
18- events chan []float64 // opcode, then fixed args for relative motion
19+ events chan []float64 // opcode, then fixed args for relative motion
20+ pointer bool // a wl_pointer: enter events carry their serial
1921 }
2022
2123 func (p *eventLog) Dispatch(e *wlturbo.Event) {
2224 ev := []float64{float64(e.Opcode)}
23- if e.Opcode == uint16(relativepointer.ZwpRelativePointerV1EventRelativeMotion) && e.ProxyID == p.ID() {
25+ if e.Opcode == uint16(relativepointer.ZwpRelativePointerV1EventRelativeMotion) && e.ProxyID == p.ID() && !p.pointer {
2426 e.Uint32()
2527 e.Uint32()
2628 for range 4 {
2729 ev = append(ev, e.Fixed().Float64())
2830 }
2931 }
32+ if e.Opcode == uint16(wayland.PointerEventEnter) && p.pointer {
33+ ev = append(ev, float64(e.Uint32())) // the enter serial
34+ }
3035 p.events <- ev
3136 }
3237
@@ -65,6 +70,56 @@ func constrained(t *testing.T, events <-chan ports.ClientEvent) ports.PointerCon
6570 }
6671 }
6772
73+// A warp from the client with the pointer, with its enter serial and on
74+// its window, reaches core window-local; others are ignored.
75+func TestPointerWarp(t *testing.T) {
76+ s, events, commands, dir := lifecycleServer(t)
77+ c := protocolClient(t, s, dir)
78+ seat := bindProtocol(t, c, "wl_seat")
79+ registerProtocol(t, c, seat)
80+ pointer := c.AllocateID()
81+ p := &eventLog{events: make(chan []float64, 64), pointer: true}
82+ p.SetID(pointer)
83+ c.Context().Register(p)
84+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
85+ w, surf, xdgID := surfaceMapper(t, c, events)()
86+ requestProtocol(t, c, xdgID, xdgshell.SurfaceRequestSetWindowGeometry, int32(4), int32(6), int32(50), int32(50))
87+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
88+ warp := bindProtocol(t, c, "wp_pointer_warp_v1")
89+
90+ // Without the pointer, a warp is ignored.
91+ requestProtocol(t, c, warp, pointerwarp.WpPointerWarpV1RequestWarpPointer, surf, pointer, wlturbo.Fixed(10*256), wlturbo.Fixed(10*256), uint32(0))
92+ commands <- ports.PointerFocus{ID: w.ID}
93+ ev := next(t, c, p.events)
94+ for ev[0] != float64(wayland.PointerEventEnter) {
95+ ev = next(t, c, p.events)
96+ }
97+ serial := uint32(ev[1])
98+ // A second pointer gets its own enter serial: the first pointer's
99+ // serial does not validate it.
100+ pointer2 := c.AllocateID()
101+ p2 := &eventLog{events: make(chan []float64, 64), pointer: true}
102+ p2.SetID(pointer2)
103+ c.Context().Register(p2)
104+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer2)
105+ requestProtocol(t, c, warp, pointerwarp.WpPointerWarpV1RequestWarpPointer, surf, pointer2, wlturbo.Fixed(10*256), wlturbo.Fixed(10*256), serial)
106+ // A stale serial is ignored; the enter serial moves the pointer.
107+ requestProtocol(t, c, warp, pointerwarp.WpPointerWarpV1RequestWarpPointer, surf, pointer, wlturbo.Fixed(10*256), wlturbo.Fixed(10*256), serial+100)
108+ requestProtocol(t, c, warp, pointerwarp.WpPointerWarpV1RequestWarpPointer, surf, pointer, wlturbo.Fixed(14*256), wlturbo.Fixed(26*256), serial)
109+ if err := c.Roundtrip(); err != nil {
110+ t.Fatal(err)
111+ }
112+ for {
113+ if v, ok := waitEvent(t, events, 2*time.Second).(ports.PointerWarp); ok {
114+ // Surface-local minus the window geometry origin.
115+ if v != (ports.PointerWarp{ID: w.ID, X: 10, Y: 20}) {
116+ t.Fatalf("warp %+v", v)
117+ }
118+ break
119+ }
120+ }
121+}
122+
68123 func TestPointerLockAndRelativeMotion(t *testing.T) {
69124 s, events, commands, dir := lifecycleServer(t)
70125 c := protocolClient(t, s, dir)
@@ -6,6 +6,7 @@ import (
6 6
7 "github.com/bnema/neferwl/internal/ports"7 "github.com/bnema/neferwl/internal/ports"
8 "github.com/bnema/purego-libwayland/protocol/pointerconstraints"8 "github.com/bnema/purego-libwayland/protocol/pointerconstraints"
9+ "github.com/bnema/purego-libwayland/protocol/pointerwarp"
9 "github.com/bnema/purego-libwayland/protocol/relativepointer"10 "github.com/bnema/purego-libwayland/protocol/relativepointer"
10 "github.com/bnema/purego-libwayland/protocol/wayland"11 "github.com/bnema/purego-libwayland/protocol/wayland"
11 "github.com/bnema/purego-libwayland/protocol/xdgshell"12 "github.com/bnema/purego-libwayland/protocol/xdgshell"
@@ -15,18 +16,22 @@ import (
15 // eventLog records the opcodes (and first fixed args) a proxy receives.16 // eventLog records the opcodes (and first fixed args) a proxy receives.
16 type eventLog struct {17 type eventLog struct {
17 wlturbo.BaseProxy18 wlturbo.BaseProxy
18- events chan []float64 // opcode, then fixed args for relative motion19+ events chan []float64 // opcode, then fixed args for relative motion
20+ pointer bool // a wl_pointer: enter events carry their serial
19 }21 }
20 22
21 func (p *eventLog) Dispatch(e *wlturbo.Event) {23 func (p *eventLog) Dispatch(e *wlturbo.Event) {
22 ev := []float64{float64(e.Opcode)}24 ev := []float64{float64(e.Opcode)}
23- if e.Opcode == uint16(relativepointer.ZwpRelativePointerV1EventRelativeMotion) && e.ProxyID == p.ID() {25+ if e.Opcode == uint16(relativepointer.ZwpRelativePointerV1EventRelativeMotion) && e.ProxyID == p.ID() && !p.pointer {
24 e.Uint32()26 e.Uint32()
25 e.Uint32()27 e.Uint32()
26 for range 4 {28 for range 4 {
27 ev = append(ev, e.Fixed().Float64())29 ev = append(ev, e.Fixed().Float64())
28 }30 }
29 }31 }
32+ if e.Opcode == uint16(wayland.PointerEventEnter) && p.pointer {
33+ ev = append(ev, float64(e.Uint32())) // the enter serial
34+ }
30 p.events <- ev35 p.events <- ev
31 }36 }
32 37
@@ -65,6 +70,56 @@ func constrained(t *testing.T, events <-chan ports.ClientEvent) ports.PointerCon
65 }70 }
66 }71 }
67 72
73+// A warp from the client with the pointer, with its enter serial and on
74+// its window, reaches core window-local; others are ignored.
75+func TestPointerWarp(t *testing.T) {
76+ s, events, commands, dir := lifecycleServer(t)
77+ c := protocolClient(t, s, dir)
78+ seat := bindProtocol(t, c, "wl_seat")
79+ registerProtocol(t, c, seat)
80+ pointer := c.AllocateID()
81+ p := &eventLog{events: make(chan []float64, 64), pointer: true}
82+ p.SetID(pointer)
83+ c.Context().Register(p)
84+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
85+ w, surf, xdgID := surfaceMapper(t, c, events)()
86+ requestProtocol(t, c, xdgID, xdgshell.SurfaceRequestSetWindowGeometry, int32(4), int32(6), int32(50), int32(50))
87+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
88+ warp := bindProtocol(t, c, "wp_pointer_warp_v1")
89+
90+ // Without the pointer, a warp is ignored.
91+ requestProtocol(t, c, warp, pointerwarp.WpPointerWarpV1RequestWarpPointer, surf, pointer, wlturbo.Fixed(10*256), wlturbo.Fixed(10*256), uint32(0))
92+ commands <- ports.PointerFocus{ID: w.ID}
93+ ev := next(t, c, p.events)
94+ for ev[0] != float64(wayland.PointerEventEnter) {
95+ ev = next(t, c, p.events)
96+ }
97+ serial := uint32(ev[1])
98+ // A second pointer gets its own enter serial: the first pointer's
99+ // serial does not validate it.
100+ pointer2 := c.AllocateID()
101+ p2 := &eventLog{events: make(chan []float64, 64), pointer: true}
102+ p2.SetID(pointer2)
103+ c.Context().Register(p2)
104+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer2)
105+ requestProtocol(t, c, warp, pointerwarp.WpPointerWarpV1RequestWarpPointer, surf, pointer2, wlturbo.Fixed(10*256), wlturbo.Fixed(10*256), serial)
106+ // A stale serial is ignored; the enter serial moves the pointer.
107+ requestProtocol(t, c, warp, pointerwarp.WpPointerWarpV1RequestWarpPointer, surf, pointer, wlturbo.Fixed(10*256), wlturbo.Fixed(10*256), serial+100)
108+ requestProtocol(t, c, warp, pointerwarp.WpPointerWarpV1RequestWarpPointer, surf, pointer, wlturbo.Fixed(14*256), wlturbo.Fixed(26*256), serial)
109+ if err := c.Roundtrip(); err != nil {
110+ t.Fatal(err)
111+ }
112+ for {
113+ if v, ok := waitEvent(t, events, 2*time.Second).(ports.PointerWarp); ok {
114+ // Surface-local minus the window geometry origin.
115+ if v != (ports.PointerWarp{ID: w.ID, X: 10, Y: 20}) {
116+ t.Fatalf("warp %+v", v)
117+ }
118+ break
119+ }
120+ }
121+}
122+
68 func TestPointerLockAndRelativeMotion(t *testing.T) {123 func TestPointerLockAndRelativeMotion(t *testing.T) {
69 s, events, commands, dir := lifecycleServer(t)124 s, events, commands, dir := lifecycleServer(t)
70 c := protocolClient(t, s, dir)125 c := protocolClient(t, s, dir)
A internal/adapters/wayland/drmlease.go +359 -0
@@ -0,0 +1,359 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "os"
6+ "slices"
7+
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/purego-libwayland/protocol/drmlease"
10+ "github.com/bnema/purego-libwayland/server"
11+ "golang.org/x/sys/unix"
12+)
13+
14+type leaseDevice struct {
15+ server *Server
16+ card string
17+ global *server.Global
18+ fd *os.File // owned by the display after receiving a duplicated DRM fd
19+ connectors map[string]ports.LeaseConnector
20+ binds map[*drmlease.WpDrmLeaseDeviceV1]*leaseBind
21+}
22+type leaseBind struct {
23+ device *leaseDevice
24+ res *drmlease.WpDrmLeaseDeviceV1
25+ connectors map[*server.Resource]string
26+ known map[*server.Resource]string
27+}
28+type leaseRequestHandler struct {
29+ bind *leaseBind
30+ names []string
31+ withdrawn bool
32+}
33+
34+// DRM lessee IDs are allocated independently on each card.
35+type leaseKey struct {
36+ card string
37+ id uint32
38+}
39+
40+type leaseObject struct {
41+ server *Server
42+ res *drmlease.WpDrmLeaseV1
43+ card string
44+ names []string
45+ id uint32
46+ finished bool
47+}
48+type leaseConnectorHandler struct{}
49+
50+func (leaseConnectorHandler) Destroy(*drmlease.WpDrmLeaseConnectorV1) {}
51+
52+func (s *Server) forwardLeases(ctx context.Context) {
53+ // Messages own their fds: close those still queued when the display stops.
54+ defer func() {
55+ for {
56+ select {
57+ case event, ok := <-s.channels.LeaseEvents:
58+ if !ok {
59+ return
60+ }
61+ ports.CloseLeaseFiles(event)
62+ default:
63+ return
64+ }
65+ }
66+ }()
67+ for {
68+ select {
69+ case <-ctx.Done():
70+ return
71+ case <-s.display.Stopped():
72+ return
73+ case event, ok := <-s.channels.LeaseEvents:
74+ if !ok {
75+ return
76+ }
77+ if !s.display.Do(func() { s.applyLeaseEvent(event) }) {
78+ ports.CloseLeaseFiles(event)
79+ return
80+ }
81+ }
82+ }
83+}
84+func (s *Server) applyLeaseEvent(event ports.LeaseMessage) {
85+ switch v := event.(type) {
86+ case ports.LeaseConnectors:
87+ s.updateLeaseConnectors(v)
88+ case ports.LeaseReply:
89+ obj := s.pendingLeases[v.ID]
90+ card := s.pendingLeaseCards[v.ID]
91+ delete(s.pendingLeases, v.ID)
92+ delete(s.pendingLeaseCards, v.ID)
93+ if obj == nil || !obj.res.Alive() {
94+ if v.FD != nil {
95+ v.FD.Close()
96+ }
97+ if v.LeaseID != 0 && card != "" {
98+ s.sendLeaseRequest(ports.LeaseRevoke{Card: card, LeaseID: v.LeaseID})
99+ }
100+ return
101+ }
102+ if v.Err != nil || v.FD == nil {
103+ if v.FD != nil {
104+ v.FD.Close()
105+ }
106+ obj.finish()
107+ return
108+ }
109+ obj.id = v.LeaseID
110+ s.activeLeases[leaseKey{obj.card, v.LeaseID}] = obj
111+ obj.res.SendLeaseFd(int(v.FD.Fd()))
112+ v.FD.Close()
113+ case ports.LeaseFinished:
114+ if obj := s.activeLeases[leaseKey{v.Card, v.LeaseID}]; obj != nil {
115+ obj.finish()
116+ }
117+ }
118+}
119+
120+// sendLeaseRequest enqueues without blocking the display. One sender drains
121+// this queue in order even when the backend channel is full.
122+func (s *Server) sendLeaseRequest(event ports.LeaseMessage) {
123+ if s.channels.LeaseRequests == nil {
124+ return
125+ }
126+ s.leaseMu.Lock()
127+ s.leaseOut = append(s.leaseOut, event)
128+ s.leaseMu.Unlock()
129+ select {
130+ case s.leaseReady <- struct{}{}:
131+ default:
132+ }
133+}
134+func (s *Server) forwardLeaseRequests(ctx context.Context) {
135+ for {
136+ s.leaseMu.Lock()
137+ if len(s.leaseOut) == 0 {
138+ s.leaseMu.Unlock()
139+ select {
140+ case <-s.leaseReady:
141+ continue
142+ case <-ctx.Done():
143+ return
144+ case <-s.display.Stopped():
145+ return
146+ }
147+ }
148+ event := s.leaseOut[0]
149+ s.leaseMu.Unlock()
150+ select {
151+ case s.channels.LeaseRequests <- event:
152+ s.leaseMu.Lock()
153+ s.leaseOut[0] = nil
154+ s.leaseOut = s.leaseOut[1:]
155+ s.leaseMu.Unlock()
156+ case <-ctx.Done():
157+ return
158+ case <-s.display.Stopped():
159+ return
160+ }
161+ }
162+}
163+func (s *Server) updateLeaseConnectors(msg ports.LeaseConnectors) {
164+ // The display owns Device from here on.
165+ copyFD := msg.Device
166+ d := s.leaseDevices[msg.Card]
167+ if d == nil {
168+ if copyFD == nil || len(msg.Connectors) == 0 {
169+ if copyFD != nil {
170+ copyFD.Close()
171+ }
172+ return
173+ }
174+ d = &leaseDevice{server: s, card: msg.Card, fd: copyFD, connectors: map[string]ports.LeaseConnector{}, binds: map[*drmlease.WpDrmLeaseDeviceV1]*leaseBind{}}
175+ g, err := s.display.AddGlobal(drmlease.WpDrmLeaseDeviceV1Interface, 1, func(c server.Client, v, id uint32) { d.bind(c, v, id) })
176+ if err != nil {
177+ copyFD.Close()
178+ s.log.Warn().Err(err).Str("card", msg.Card).Msg("drm lease global")
179+ return
180+ }
181+ d.global = g
182+ s.leaseDevices[msg.Card] = d
183+ }
184+ if copyFD == nil || len(msg.Connectors) == 0 {
185+ if copyFD != nil {
186+ copyFD.Close()
187+ }
188+ d.global.Remove()
189+ if d.fd != nil {
190+ d.fd.Close()
191+ d.fd = nil
192+ }
193+ // Bound devices stay until the client sends release, as the
194+ // protocol asks after global_remove; they offer nothing meanwhile.
195+ for _, b := range d.binds {
196+ for r := range b.connectors {
197+ drmlease.WrapWpDrmLeaseConnectorV1(r).SendWithdrawn()
198+ delete(b.connectors, r)
199+ }
200+ b.res.SendDone()
201+ }
202+ delete(s.leaseDevices, msg.Card)
203+ return
204+ }
205+ if d.fd != copyFD {
206+ if d.fd != nil {
207+ d.fd.Close()
208+ }
209+ d.fd = copyFD
210+ }
211+ next := map[string]ports.LeaseConnector{}
212+ for _, c := range msg.Connectors {
213+ next[c.Name] = c
214+ }
215+ for _, b := range d.binds {
216+ changed := false
217+ for r, name := range b.connectors {
218+ if _, ok := next[name]; !ok && r.Alive() {
219+ drmlease.WrapWpDrmLeaseConnectorV1(r).SendWithdrawn()
220+ delete(b.connectors, r)
221+ changed = true
222+ }
223+ }
224+ for name, c := range next {
225+ found := false
226+ for _, n := range b.connectors {
227+ if n == name {
228+ found = true
229+ break
230+ }
231+ }
232+ if !found {
233+ b.add(c)
234+ changed = true
235+ }
236+ }
237+ if changed {
238+ b.res.SendDone()
239+ }
240+ }
241+ d.connectors = next
242+}
243+func (d *leaseDevice) bind(c server.Client, v, id uint32) {
244+ b := &leaseBind{device: d, connectors: map[*server.Resource]string{}, known: map[*server.Resource]string{}}
245+ r, err := drmlease.NewWpDrmLeaseDeviceV1(c, int32(v), id, b)
246+ if err != nil {
247+ return
248+ }
249+ b.res = r
250+ d.binds[r] = b
251+ r.OnDestroy = func() { delete(d.binds, r) }
252+ if d.fd != nil {
253+ fd, err := unix.FcntlInt(d.fd.Fd(), unix.F_DUPFD_CLOEXEC, 0)
254+ if err == nil {
255+ r.SendDrmFd(fd)
256+ unix.Close(fd)
257+ } else {
258+ d.server.log.Warn().Err(err).Str("card", d.card).Msg("lease drm fd")
259+ r.SendReleased()
260+ r.Destroy()
261+ return
262+ }
263+ }
264+ for _, c := range d.connectors {
265+ b.add(c)
266+ }
267+ r.SendDone()
268+}
269+func (b *leaseBind) add(c ports.LeaseConnector) {
270+ r, err := drmlease.NewWpDrmLeaseConnectorV1(b.res.Client(), 1, 0, leaseConnectorHandler{})
271+ if err != nil {
272+ return
273+ }
274+ b.connectors[r.Resource] = c.Name
275+ b.known[r.Resource] = c.Name
276+ r.OnDestroy = func() { delete(b.connectors, r.Resource); delete(b.known, r.Resource) }
277+ b.res.SendConnector(r)
278+ r.SendName(c.Name)
279+ r.SendDescription(c.Description)
280+ r.SendConnectorId(c.ConnectorID)
281+ r.SendDone()
282+}
283+func (b *leaseBind) CreateLeaseRequest(_ *drmlease.WpDrmLeaseDeviceV1, id uint32) {
284+ _, _ = drmlease.NewWpDrmLeaseRequestV1(b.res.Client(), 1, id, &leaseRequestHandler{bind: b})
285+}
286+func (b *leaseBind) Release(self *drmlease.WpDrmLeaseDeviceV1) { self.SendReleased(); self.Destroy() }
287+func (r *leaseRequestHandler) RequestConnector(self *drmlease.WpDrmLeaseRequestV1, c *drmlease.WpDrmLeaseConnectorV1) {
288+ if c == nil {
289+ self.PostError(uint32(drmlease.WpDrmLeaseRequestV1ErrorWrongDevice), "connector belongs to another device")
290+ return
291+ }
292+ name, ok := r.bind.known[c.Resource]
293+ if !ok {
294+ self.PostError(uint32(drmlease.WpDrmLeaseRequestV1ErrorWrongDevice), "connector belongs to another device")
295+ return
296+ }
297+ if _, available := r.bind.connectors[c.Resource]; !available {
298+ r.withdrawn = true
299+ }
300+ if slices.Contains(r.names, name) {
301+ self.PostError(uint32(drmlease.WpDrmLeaseRequestV1ErrorDuplicateConnector), "duplicate connector")
302+ return
303+ }
304+ r.names = append(r.names, name)
305+}
306+func (r *leaseRequestHandler) Submit(self *drmlease.WpDrmLeaseRequestV1, id uint32) {
307+ if len(r.names) == 0 {
308+ self.PostError(uint32(drmlease.WpDrmLeaseRequestV1ErrorEmptyLease), "empty lease")
309+ return
310+ }
311+ s := r.bind.device.server
312+ obj := &leaseObject{server: s, card: r.bind.device.card, names: append([]string(nil), r.names...)}
313+ res, err := drmlease.NewWpDrmLeaseV1(self.Client(), 1, id, obj)
314+ if err != nil {
315+ return
316+ }
317+ obj.res = res
318+ res.OnDestroy = func() { obj.destroy() }
319+ for _, name := range r.names {
320+ if _, ok := r.bind.device.connectors[name]; !ok || r.withdrawn {
321+ obj.finish()
322+ return
323+ }
324+ }
325+ s.nextLeaseID++
326+ s.pendingLeases[s.nextLeaseID] = obj
327+ s.pendingLeaseCards[s.nextLeaseID] = obj.card
328+ s.sendLeaseRequest(ports.LeaseRequest{ID: s.nextLeaseID, Card: obj.card, Connectors: obj.names})
329+}
330+func (o *leaseObject) Destroy(*drmlease.WpDrmLeaseV1) { o.destroy() }
331+func (o *leaseObject) destroy() {
332+ if o.finished {
333+ return
334+ }
335+ o.finished = true
336+ if o.id != 0 {
337+ delete(o.server.activeLeases, leaseKey{o.card, o.id})
338+ o.server.sendLeaseRequest(ports.LeaseRevoke{Card: o.card, LeaseID: o.id})
339+ }
340+ for id, p := range o.server.pendingLeases {
341+ if p == o {
342+ delete(o.server.pendingLeases, id)
343+ // Keep pendingLeaseCards as a tombstone for a late successful
344+ // reply; applyLeaseEvent revokes it and then removes the tombstone.
345+ }
346+ }
347+}
348+func (o *leaseObject) finish() {
349+ if o.finished {
350+ return
351+ }
352+ o.finished = true
353+ if o.id != 0 {
354+ delete(o.server.activeLeases, leaseKey{o.card, o.id})
355+ }
356+ if o.res.Alive() {
357+ o.res.SendFinished()
358+ }
359+}
@@ -0,0 +1,359 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "os"
6+ "slices"
7+
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/purego-libwayland/protocol/drmlease"
10+ "github.com/bnema/purego-libwayland/server"
11+ "golang.org/x/sys/unix"
12+)
13+
14+type leaseDevice struct {
15+ server *Server
16+ card string
17+ global *server.Global
18+ fd *os.File // owned by the display after receiving a duplicated DRM fd
19+ connectors map[string]ports.LeaseConnector
20+ binds map[*drmlease.WpDrmLeaseDeviceV1]*leaseBind
21+}
22+type leaseBind struct {
23+ device *leaseDevice
24+ res *drmlease.WpDrmLeaseDeviceV1
25+ connectors map[*server.Resource]string
26+ known map[*server.Resource]string
27+}
28+type leaseRequestHandler struct {
29+ bind *leaseBind
30+ names []string
31+ withdrawn bool
32+}
33+
34+// DRM lessee IDs are allocated independently on each card.
35+type leaseKey struct {
36+ card string
37+ id uint32
38+}
39+
40+type leaseObject struct {
41+ server *Server
42+ res *drmlease.WpDrmLeaseV1
43+ card string
44+ names []string
45+ id uint32
46+ finished bool
47+}
48+type leaseConnectorHandler struct{}
49+
50+func (leaseConnectorHandler) Destroy(*drmlease.WpDrmLeaseConnectorV1) {}
51+
52+func (s *Server) forwardLeases(ctx context.Context) {
53+ // Messages own their fds: close those still queued when the display stops.
54+ defer func() {
55+ for {
56+ select {
57+ case event, ok := <-s.channels.LeaseEvents:
58+ if !ok {
59+ return
60+ }
61+ ports.CloseLeaseFiles(event)
62+ default:
63+ return
64+ }
65+ }
66+ }()
67+ for {
68+ select {
69+ case <-ctx.Done():
70+ return
71+ case <-s.display.Stopped():
72+ return
73+ case event, ok := <-s.channels.LeaseEvents:
74+ if !ok {
75+ return
76+ }
77+ if !s.display.Do(func() { s.applyLeaseEvent(event) }) {
78+ ports.CloseLeaseFiles(event)
79+ return
80+ }
81+ }
82+ }
83+}
84+func (s *Server) applyLeaseEvent(event ports.LeaseMessage) {
85+ switch v := event.(type) {
86+ case ports.LeaseConnectors:
87+ s.updateLeaseConnectors(v)
88+ case ports.LeaseReply:
89+ obj := s.pendingLeases[v.ID]
90+ card := s.pendingLeaseCards[v.ID]
91+ delete(s.pendingLeases, v.ID)
92+ delete(s.pendingLeaseCards, v.ID)
93+ if obj == nil || !obj.res.Alive() {
94+ if v.FD != nil {
95+ v.FD.Close()
96+ }
97+ if v.LeaseID != 0 && card != "" {
98+ s.sendLeaseRequest(ports.LeaseRevoke{Card: card, LeaseID: v.LeaseID})
99+ }
100+ return
101+ }
102+ if v.Err != nil || v.FD == nil {
103+ if v.FD != nil {
104+ v.FD.Close()
105+ }
106+ obj.finish()
107+ return
108+ }
109+ obj.id = v.LeaseID
110+ s.activeLeases[leaseKey{obj.card, v.LeaseID}] = obj
111+ obj.res.SendLeaseFd(int(v.FD.Fd()))
112+ v.FD.Close()
113+ case ports.LeaseFinished:
114+ if obj := s.activeLeases[leaseKey{v.Card, v.LeaseID}]; obj != nil {
115+ obj.finish()
116+ }
117+ }
118+}
119+
120+// sendLeaseRequest enqueues without blocking the display. One sender drains
121+// this queue in order even when the backend channel is full.
122+func (s *Server) sendLeaseRequest(event ports.LeaseMessage) {
123+ if s.channels.LeaseRequests == nil {
124+ return
125+ }
126+ s.leaseMu.Lock()
127+ s.leaseOut = append(s.leaseOut, event)
128+ s.leaseMu.Unlock()
129+ select {
130+ case s.leaseReady <- struct{}{}:
131+ default:
132+ }
133+}
134+func (s *Server) forwardLeaseRequests(ctx context.Context) {
135+ for {
136+ s.leaseMu.Lock()
137+ if len(s.leaseOut) == 0 {
138+ s.leaseMu.Unlock()
139+ select {
140+ case <-s.leaseReady:
141+ continue
142+ case <-ctx.Done():
143+ return
144+ case <-s.display.Stopped():
145+ return
146+ }
147+ }
148+ event := s.leaseOut[0]
149+ s.leaseMu.Unlock()
150+ select {
151+ case s.channels.LeaseRequests <- event:
152+ s.leaseMu.Lock()
153+ s.leaseOut[0] = nil
154+ s.leaseOut = s.leaseOut[1:]
155+ s.leaseMu.Unlock()
156+ case <-ctx.Done():
157+ return
158+ case <-s.display.Stopped():
159+ return
160+ }
161+ }
162+}
163+func (s *Server) updateLeaseConnectors(msg ports.LeaseConnectors) {
164+ // The display owns Device from here on.
165+ copyFD := msg.Device
166+ d := s.leaseDevices[msg.Card]
167+ if d == nil {
168+ if copyFD == nil || len(msg.Connectors) == 0 {
169+ if copyFD != nil {
170+ copyFD.Close()
171+ }
172+ return
173+ }
174+ d = &leaseDevice{server: s, card: msg.Card, fd: copyFD, connectors: map[string]ports.LeaseConnector{}, binds: map[*drmlease.WpDrmLeaseDeviceV1]*leaseBind{}}
175+ g, err := s.display.AddGlobal(drmlease.WpDrmLeaseDeviceV1Interface, 1, func(c server.Client, v, id uint32) { d.bind(c, v, id) })
176+ if err != nil {
177+ copyFD.Close()
178+ s.log.Warn().Err(err).Str("card", msg.Card).Msg("drm lease global")
179+ return
180+ }
181+ d.global = g
182+ s.leaseDevices[msg.Card] = d
183+ }
184+ if copyFD == nil || len(msg.Connectors) == 0 {
185+ if copyFD != nil {
186+ copyFD.Close()
187+ }
188+ d.global.Remove()
189+ if d.fd != nil {
190+ d.fd.Close()
191+ d.fd = nil
192+ }
193+ // Bound devices stay until the client sends release, as the
194+ // protocol asks after global_remove; they offer nothing meanwhile.
195+ for _, b := range d.binds {
196+ for r := range b.connectors {
197+ drmlease.WrapWpDrmLeaseConnectorV1(r).SendWithdrawn()
198+ delete(b.connectors, r)
199+ }
200+ b.res.SendDone()
201+ }
202+ delete(s.leaseDevices, msg.Card)
203+ return
204+ }
205+ if d.fd != copyFD {
206+ if d.fd != nil {
207+ d.fd.Close()
208+ }
209+ d.fd = copyFD
210+ }
211+ next := map[string]ports.LeaseConnector{}
212+ for _, c := range msg.Connectors {
213+ next[c.Name] = c
214+ }
215+ for _, b := range d.binds {
216+ changed := false
217+ for r, name := range b.connectors {
218+ if _, ok := next[name]; !ok && r.Alive() {
219+ drmlease.WrapWpDrmLeaseConnectorV1(r).SendWithdrawn()
220+ delete(b.connectors, r)
221+ changed = true
222+ }
223+ }
224+ for name, c := range next {
225+ found := false
226+ for _, n := range b.connectors {
227+ if n == name {
228+ found = true
229+ break
230+ }
231+ }
232+ if !found {
233+ b.add(c)
234+ changed = true
235+ }
236+ }
237+ if changed {
238+ b.res.SendDone()
239+ }
240+ }
241+ d.connectors = next
242+}
243+func (d *leaseDevice) bind(c server.Client, v, id uint32) {
244+ b := &leaseBind{device: d, connectors: map[*server.Resource]string{}, known: map[*server.Resource]string{}}
245+ r, err := drmlease.NewWpDrmLeaseDeviceV1(c, int32(v), id, b)
246+ if err != nil {
247+ return
248+ }
249+ b.res = r
250+ d.binds[r] = b
251+ r.OnDestroy = func() { delete(d.binds, r) }
252+ if d.fd != nil {
253+ fd, err := unix.FcntlInt(d.fd.Fd(), unix.F_DUPFD_CLOEXEC, 0)
254+ if err == nil {
255+ r.SendDrmFd(fd)
256+ unix.Close(fd)
257+ } else {
258+ d.server.log.Warn().Err(err).Str("card", d.card).Msg("lease drm fd")
259+ r.SendReleased()
260+ r.Destroy()
261+ return
262+ }
263+ }
264+ for _, c := range d.connectors {
265+ b.add(c)
266+ }
267+ r.SendDone()
268+}
269+func (b *leaseBind) add(c ports.LeaseConnector) {
270+ r, err := drmlease.NewWpDrmLeaseConnectorV1(b.res.Client(), 1, 0, leaseConnectorHandler{})
271+ if err != nil {
272+ return
273+ }
274+ b.connectors[r.Resource] = c.Name
275+ b.known[r.Resource] = c.Name
276+ r.OnDestroy = func() { delete(b.connectors, r.Resource); delete(b.known, r.Resource) }
277+ b.res.SendConnector(r)
278+ r.SendName(c.Name)
279+ r.SendDescription(c.Description)
280+ r.SendConnectorId(c.ConnectorID)
281+ r.SendDone()
282+}
283+func (b *leaseBind) CreateLeaseRequest(_ *drmlease.WpDrmLeaseDeviceV1, id uint32) {
284+ _, _ = drmlease.NewWpDrmLeaseRequestV1(b.res.Client(), 1, id, &leaseRequestHandler{bind: b})
285+}
286+func (b *leaseBind) Release(self *drmlease.WpDrmLeaseDeviceV1) { self.SendReleased(); self.Destroy() }
287+func (r *leaseRequestHandler) RequestConnector(self *drmlease.WpDrmLeaseRequestV1, c *drmlease.WpDrmLeaseConnectorV1) {
288+ if c == nil {
289+ self.PostError(uint32(drmlease.WpDrmLeaseRequestV1ErrorWrongDevice), "connector belongs to another device")
290+ return
291+ }
292+ name, ok := r.bind.known[c.Resource]
293+ if !ok {
294+ self.PostError(uint32(drmlease.WpDrmLeaseRequestV1ErrorWrongDevice), "connector belongs to another device")
295+ return
296+ }
297+ if _, available := r.bind.connectors[c.Resource]; !available {
298+ r.withdrawn = true
299+ }
300+ if slices.Contains(r.names, name) {
301+ self.PostError(uint32(drmlease.WpDrmLeaseRequestV1ErrorDuplicateConnector), "duplicate connector")
302+ return
303+ }
304+ r.names = append(r.names, name)
305+}
306+func (r *leaseRequestHandler) Submit(self *drmlease.WpDrmLeaseRequestV1, id uint32) {
307+ if len(r.names) == 0 {
308+ self.PostError(uint32(drmlease.WpDrmLeaseRequestV1ErrorEmptyLease), "empty lease")
309+ return
310+ }
311+ s := r.bind.device.server
312+ obj := &leaseObject{server: s, card: r.bind.device.card, names: append([]string(nil), r.names...)}
313+ res, err := drmlease.NewWpDrmLeaseV1(self.Client(), 1, id, obj)
314+ if err != nil {
315+ return
316+ }
317+ obj.res = res
318+ res.OnDestroy = func() { obj.destroy() }
319+ for _, name := range r.names {
320+ if _, ok := r.bind.device.connectors[name]; !ok || r.withdrawn {
321+ obj.finish()
322+ return
323+ }
324+ }
325+ s.nextLeaseID++
326+ s.pendingLeases[s.nextLeaseID] = obj
327+ s.pendingLeaseCards[s.nextLeaseID] = obj.card
328+ s.sendLeaseRequest(ports.LeaseRequest{ID: s.nextLeaseID, Card: obj.card, Connectors: obj.names})
329+}
330+func (o *leaseObject) Destroy(*drmlease.WpDrmLeaseV1) { o.destroy() }
331+func (o *leaseObject) destroy() {
332+ if o.finished {
333+ return
334+ }
335+ o.finished = true
336+ if o.id != 0 {
337+ delete(o.server.activeLeases, leaseKey{o.card, o.id})
338+ o.server.sendLeaseRequest(ports.LeaseRevoke{Card: o.card, LeaseID: o.id})
339+ }
340+ for id, p := range o.server.pendingLeases {
341+ if p == o {
342+ delete(o.server.pendingLeases, id)
343+ // Keep pendingLeaseCards as a tombstone for a late successful
344+ // reply; applyLeaseEvent revokes it and then removes the tombstone.
345+ }
346+ }
347+}
348+func (o *leaseObject) finish() {
349+ if o.finished {
350+ return
351+ }
352+ o.finished = true
353+ if o.id != 0 {
354+ delete(o.server.activeLeases, leaseKey{o.card, o.id})
355+ }
356+ if o.res.Alive() {
357+ o.res.SendFinished()
358+ }
359+}
A internal/adapters/wayland/drmlease_test.go +550 -0
@@ -0,0 +1,550 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "fmt"
6+ "os"
7+ "testing"
8+ "time"
9+
10+ "github.com/bnema/neferwl/internal/logging"
11+ "github.com/bnema/neferwl/internal/ports"
12+ dl "github.com/bnema/purego-libwayland/protocol/drmlease"
13+ "github.com/bnema/purego-libwayland/server"
14+ "github.com/bnema/wlturbo"
15+ "golang.org/x/sys/unix"
16+)
17+
18+type leaseDeviceEvents struct {
19+ wlturbo.BaseProxy
20+ c *wlturbo.Display
21+ connectors []uint32
22+ fds []int
23+}
24+
25+func (p *leaseDeviceEvents) Dispatch(e *wlturbo.Event) {
26+ switch e.Opcode {
27+ case uint16(dl.WpDrmLeaseDeviceV1EventDrmFd):
28+ p.fds = append(p.fds, int(e.Fd()))
29+ case uint16(dl.WpDrmLeaseDeviceV1EventConnector):
30+ id := e.Uint32()
31+ p.connectors = append(p.connectors, id)
32+ child := &leaseConnectorEvents{}
33+ child.SetID(id)
34+ p.c.Context().Register(child)
35+ }
36+}
37+
38+type leaseConnectorEvents struct {
39+ wlturbo.BaseProxy
40+ name string
41+}
42+
43+func (p *leaseConnectorEvents) Dispatch(e *wlturbo.Event) {
44+ if e.Opcode == uint16(dl.WpDrmLeaseConnectorV1EventName) {
45+ p.name = e.String()
46+ }
47+}
48+
49+type leaseResultEvents struct {
50+ wlturbo.BaseProxy
51+ fds []int
52+ finished int
53+}
54+
55+func (p *leaseResultEvents) Dispatch(e *wlturbo.Event) {
56+ switch e.Opcode {
57+ case uint16(dl.WpDrmLeaseV1EventLeaseFd):
58+ p.fds = append(p.fds, int(e.Fd()))
59+ case uint16(dl.WpDrmLeaseV1EventFinished):
60+ p.finished++
61+ }
62+}
63+
64+// leaseFD opens a device fd for one LeaseConnectors message; the server
65+// owns and closes it.
66+func leaseFD(t *testing.T) *os.File {
67+ t.Helper()
68+ f, err := os.Open("/dev/null")
69+ if err != nil {
70+ t.Fatal(err)
71+ }
72+ return f
73+}
74+func leaseServer(t *testing.T) (*wlturbo.Display, chan ports.LeaseMessage, chan ports.LeaseMessage) {
75+ c, requests, events, _ := leaseServerWithState(t)
76+ return c, requests, events
77+}
78+func leaseServerWithState(t *testing.T) (*wlturbo.Display, chan ports.LeaseMessage, chan ports.LeaseMessage, *Server) {
79+ t.Helper()
80+ dir := t.TempDir()
81+ requests := make(chan ports.LeaseMessage, 16)
82+ events := make(chan ports.LeaseMessage, 16)
83+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{LeaseRequests: requests, LeaseEvents: events}, logging.For(context.Background(), "wayland"))
84+ if err != nil {
85+ t.Fatal(err)
86+ }
87+ ctx, cancel := context.WithCancel(context.Background())
88+ done := make(chan error, 1)
89+ go func() { done <- s.Run(ctx) }()
90+ t.Cleanup(func() {
91+ cancel()
92+ select {
93+ case err := <-done:
94+ if err != nil {
95+ t.Error(err)
96+ }
97+ case <-time.After(3 * time.Second):
98+ t.Error("server hung")
99+ }
100+ })
101+ c := protocolClient(t, s, dir)
102+ events <- ports.LeaseConnectors{Card: "/dev/dri/card-test", Device: leaseFD(t), Connectors: []ports.LeaseConnector{{Card: "/dev/dri/card-test", Name: "DP-1", Description: "VR headset", ConnectorID: 42}}}
103+ for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
104+ if err := c.Roundtrip(); err != nil {
105+ t.Fatal(err)
106+ }
107+ if _, ok := c.Registry().FindGlobal("wp_drm_lease_device_v1"); ok {
108+ return c, requests, events, s
109+ }
110+ time.Sleep(time.Millisecond)
111+ }
112+ t.Fatal("no lease global")
113+ return nil, nil, nil, nil
114+}
115+func TestDRMLeaseFlow(t *testing.T) {
116+ c, requests, events := leaseServer(t)
117+ dev := bindProtocol(t, c, "wp_drm_lease_device_v1")
118+ p := &leaseDeviceEvents{c: c}
119+ p.SetID(dev)
120+ c.Context().Register(p)
121+ if err := c.Roundtrip(); err != nil {
122+ t.Fatal(err)
123+ }
124+ if len(p.fds) != 1 || len(p.connectors) != 1 {
125+ t.Fatalf("device: fds=%v connectors=%v", p.fds, p.connectors)
126+ }
127+ defer unix.Close(p.fds[0])
128+ conn := p.connectors[0]
129+ req := c.AllocateID()
130+ registerProtocol(t, c, req)
131+ requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
132+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, conn)
133+ lease := c.AllocateID()
134+ l := &leaseResultEvents{}
135+ l.SetID(lease)
136+ c.Context().Register(l)
137+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, lease)
138+ if err := c.Roundtrip(); err != nil {
139+ t.Fatal(err)
140+ }
141+ var got ports.LeaseRequest
142+ select {
143+ case ev := <-requests:
144+ var ok bool
145+ got, ok = ev.(ports.LeaseRequest)
146+ if !ok {
147+ t.Fatalf("message %T", ev)
148+ }
149+ case <-time.After(2 * time.Second):
150+ t.Fatal("no request")
151+ }
152+ if len(got.Connectors) != 1 || got.Connectors[0] != "DP-1" {
153+ t.Fatalf("request %+v", got)
154+ }
155+ r, w, err := os.Pipe()
156+ if err != nil {
157+ t.Fatal(err)
158+ }
159+ defer r.Close()
160+ events <- ports.LeaseReply{ID: got.ID, FD: w, LeaseID: 7}
161+ if err := c.Roundtrip(); err != nil {
162+ t.Fatal(err)
163+ }
164+ for deadline := time.Now().Add(2 * time.Second); len(l.fds) == 0 && time.Now().Before(deadline); {
165+ if err := c.Roundtrip(); err != nil {
166+ t.Fatal(err)
167+ }
168+ }
169+ if len(l.fds) != 1 {
170+ t.Fatalf("lease fd count %d", len(l.fds))
171+ }
172+ defer unix.Close(l.fds[0])
173+ requestProtocol(t, c, lease, dl.WpDrmLeaseV1RequestDestroy)
174+ if err := c.Roundtrip(); err != nil {
175+ t.Fatal(err)
176+ }
177+ select {
178+ case ev := <-requests:
179+ if v, ok := ev.(ports.LeaseRevoke); !ok || v.LeaseID != 7 {
180+ t.Fatalf("revoke %v", ev)
181+ }
182+ case <-time.After(2 * time.Second):
183+ t.Fatal("no revoke")
184+ }
185+}
186+func TestDRMLeaseErrors(t *testing.T) {
187+ for _, tc := range []struct {
188+ name string
189+ duplicate bool
190+ code uint32
191+ }{{"empty", false, uint32(dl.WpDrmLeaseRequestV1ErrorEmptyLease)}, {"duplicate", true, uint32(dl.WpDrmLeaseRequestV1ErrorDuplicateConnector)}} {
192+ t.Run(tc.name, func(t *testing.T) {
193+ c, _, _ := leaseServer(t)
194+ dev := bindProtocol(t, c, "wp_drm_lease_device_v1")
195+ p := &leaseDeviceEvents{c: c}
196+ p.SetID(dev)
197+ c.Context().Register(p)
198+ if err := c.Roundtrip(); err != nil {
199+ t.Fatal(err)
200+ }
201+ for _, fd := range p.fds {
202+ defer unix.Close(fd)
203+ }
204+ req := c.AllocateID()
205+ registerProtocol(t, c, req)
206+ requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
207+ if tc.duplicate {
208+ conn := p.connectors[0]
209+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, conn)
210+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, conn)
211+ } else {
212+ lease := c.AllocateID()
213+ registerProtocol(t, c, lease)
214+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, lease)
215+ }
216+ expectProtocolError(t, c, req, tc.code)
217+ })
218+ }
219+}
220+func TestDRMLeaseFinished(t *testing.T) {
221+ c, requests, events := leaseServer(t)
222+ dev := bindProtocol(t, c, "wp_drm_lease_device_v1")
223+ p := &leaseDeviceEvents{c: c}
224+ p.SetID(dev)
225+ c.Context().Register(p)
226+ if err := c.Roundtrip(); err != nil {
227+ t.Fatal(err)
228+ }
229+ defer unix.Close(p.fds[0])
230+ req := c.AllocateID()
231+ registerProtocol(t, c, req)
232+ requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
233+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, p.connectors[0])
234+ lease := c.AllocateID()
235+ l := &leaseResultEvents{}
236+ l.SetID(lease)
237+ c.Context().Register(l)
238+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, lease)
239+ if err := c.Roundtrip(); err != nil {
240+ t.Fatal(err)
241+ }
242+ got := (<-requests).(ports.LeaseRequest)
243+ r, w, err := os.Pipe()
244+ if err != nil {
245+ t.Fatal(err)
246+ }
247+ defer r.Close()
248+ events <- ports.LeaseReply{ID: got.ID, FD: w, LeaseID: 8}
249+ if err := c.Roundtrip(); err != nil {
250+ t.Fatal(err)
251+ }
252+ for deadline := time.Now().Add(2 * time.Second); len(l.fds) == 0 && time.Now().Before(deadline); {
253+ if err := c.Roundtrip(); err != nil {
254+ t.Fatal(err)
255+ }
256+ }
257+ if len(l.fds) != 1 {
258+ t.Fatal("missing lease fd")
259+ }
260+ defer unix.Close(l.fds[0])
261+ events <- ports.LeaseFinished{Card: got.Card, LeaseID: 8}
262+ if err := c.Roundtrip(); err != nil {
263+ t.Fatal(err)
264+ }
265+ for deadline := time.Now().Add(2 * time.Second); l.finished == 0 && time.Now().Before(deadline); {
266+ if err := c.Roundtrip(); err != nil {
267+ t.Fatal(err)
268+ }
269+ }
270+ if l.finished != 1 {
271+ t.Fatalf("finished %d", l.finished)
272+ }
273+}
274+
275+func TestLeaseMessagesRemainOrderedWhenChannelFull(t *testing.T) {
276+ requests := make(chan ports.LeaseMessage, 1)
277+ ready := make(chan struct{}, 1)
278+ display, err := server.NewDisplay()
279+ if err != nil {
280+ t.Fatal(err)
281+ }
282+ defer display.Close()
283+ s := &Server{display: display, channels: Channels{LeaseRequests: requests}, leaseReady: ready}
284+ requests <- ports.LeaseRequest{ID: 99}
285+ ctx, cancel := context.WithCancel(context.Background())
286+ done := make(chan struct{})
287+ go func() { s.forwardLeaseRequests(ctx); close(done) }()
288+ defer func() { cancel(); <-done }()
289+ s.sendLeaseRequest(ports.LeaseRequest{ID: 1})
290+ s.sendLeaseRequest(ports.LeaseRevoke{Card: "card", LeaseID: 1})
291+ if got := <-requests; got.(ports.LeaseRequest).ID != 99 {
292+ t.Fatal(got)
293+ }
294+ select {
295+ case got := <-requests:
296+ if v, ok := got.(ports.LeaseRequest); !ok || v.ID != 1 {
297+ t.Fatalf("first %v", got)
298+ }
299+ case <-time.After(time.Second):
300+ t.Fatal("request timeout")
301+ }
302+ select {
303+ case got := <-requests:
304+ if v, ok := got.(ports.LeaseRevoke); !ok || v.LeaseID != 1 {
305+ t.Fatalf("second %v", got)
306+ }
307+ case <-time.After(time.Second):
308+ t.Fatal("revoke timeout")
309+ }
310+}
311+
312+func TestLeaseIDsAreScopedToCard(t *testing.T) {
313+ dir := t.TempDir()
314+ requests := make(chan ports.LeaseMessage, 16)
315+ events := make(chan ports.LeaseMessage, 16)
316+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{LeaseRequests: requests, LeaseEvents: events}, logging.For(context.Background(), "wayland"))
317+ if err != nil {
318+ t.Fatal(err)
319+ }
320+ ctx, cancel := context.WithCancel(context.Background())
321+ done := make(chan error, 1)
322+ go func() { done <- s.Run(ctx) }()
323+ t.Cleanup(func() {
324+ cancel()
325+ if err := <-done; err != nil {
326+ t.Error(err)
327+ }
328+ })
329+ c := protocolClient(t, s, dir)
330+ for i, card := range []string{"card-one", "card-two"} {
331+ events <- ports.LeaseConnectors{Card: card, Device: leaseFD(t), Connectors: []ports.LeaseConnector{{Card: card, Name: fmt.Sprintf("DP-%d", i+1), ConnectorID: uint32(i + 40)}}}
332+ }
333+ for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
334+ if err := c.Roundtrip(); err != nil {
335+ t.Fatal(err)
336+ }
337+ count := 0
338+ for _, g := range c.Registry().GetGlobals() {
339+ if g.Interface == "wp_drm_lease_device_v1" {
340+ count++
341+ }
342+ }
343+ if count == 2 {
344+ break
345+ }
346+ }
347+ var devices []uint32
348+ for _, g := range c.Registry().GetGlobals() {
349+ if g.Interface == "wp_drm_lease_device_v1" {
350+ id, err := c.Registry().BindID(g.Name, g.Interface, 1)
351+ if err != nil {
352+ t.Fatal(err)
353+ }
354+ devices = append(devices, id)
355+ }
356+ }
357+ if len(devices) != 2 {
358+ t.Fatalf("device globals: %d", len(devices))
359+ }
360+ var bound []*leaseDeviceEvents
361+ for _, dev := range devices {
362+ p := &leaseDeviceEvents{c: c}
363+ p.SetID(dev)
364+ c.Context().Register(p)
365+ bound = append(bound, p)
366+ }
367+ if err := c.Roundtrip(); err != nil {
368+ t.Fatal(err)
369+ }
370+ for i, dev := range devices {
371+ p := bound[i]
372+ if len(p.connectors) != 1 || len(p.fds) != 1 {
373+ t.Fatalf("device %d: %+v", dev, p)
374+ }
375+ defer unix.Close(p.fds[0])
376+ req := c.AllocateID()
377+ registerProtocol(t, c, req)
378+ requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
379+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, p.connectors[0])
380+ id := c.AllocateID()
381+ l := &leaseResultEvents{}
382+ l.SetID(id)
383+ c.Context().Register(l)
384+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, id)
385+ }
386+ if err := c.Roundtrip(); err != nil {
387+ t.Fatal(err)
388+ }
389+ first := (<-requests).(ports.LeaseRequest)
390+ second := (<-requests).(ports.LeaseRequest)
391+ if first.Card == second.Card {
392+ t.Fatalf("same card: %v %v", first, second)
393+ }
394+ for _, req := range []ports.LeaseRequest{first, second} {
395+ r, w, err := os.Pipe()
396+ if err != nil {
397+ t.Fatal(err)
398+ }
399+ defer r.Close()
400+ events <- ports.LeaseReply{ID: req.ID, FD: w, LeaseID: 7}
401+ }
402+ if err := c.Roundtrip(); err != nil {
403+ t.Fatal(err)
404+ }
405+ for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
406+ count := 0
407+ s.display.Do(func() { count = len(s.activeLeases) })
408+ if count == 2 {
409+ break
410+ }
411+ if err := c.Roundtrip(); err != nil {
412+ t.Fatal(err)
413+ }
414+ }
415+ if !s.display.Do(func() {
416+ if len(s.activeLeases) != 2 {
417+ t.Errorf("active leases %v", s.activeLeases)
418+ }
419+ }) {
420+ t.Fatal("display stopped")
421+ }
422+ events <- ports.LeaseFinished{Card: first.Card, LeaseID: 7}
423+ if err := c.Roundtrip(); err != nil {
424+ t.Fatal(err)
425+ }
426+ for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
427+ count := 0
428+ s.display.Do(func() { count = len(s.activeLeases) })
429+ if count == 1 {
430+ break
431+ }
432+ if err := c.Roundtrip(); err != nil {
433+ t.Fatal(err)
434+ }
435+ }
436+ if !s.display.Do(func() {
437+ if len(s.activeLeases) != 1 || s.activeLeases[leaseKey{second.Card, 7}] == nil {
438+ t.Errorf("wrong card finished: %v", s.activeLeases)
439+ }
440+ }) {
441+ t.Fatal("display stopped")
442+ }
443+}
444+
445+func TestLeaseDestroyPendingRevokesLateReply(t *testing.T) {
446+ c, requests, events, s := leaseServerWithState(t)
447+ dev := bindProtocol(t, c, "wp_drm_lease_device_v1")
448+ p := &leaseDeviceEvents{c: c}
449+ p.SetID(dev)
450+ c.Context().Register(p)
451+ if err := c.Roundtrip(); err != nil {
452+ t.Fatal(err)
453+ }
454+ defer unix.Close(p.fds[0])
455+ req := c.AllocateID()
456+ registerProtocol(t, c, req)
457+ requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
458+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, p.connectors[0])
459+ lease := c.AllocateID()
460+ registerProtocol(t, c, lease)
461+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, lease)
462+ if err := c.Roundtrip(); err != nil {
463+ t.Fatal(err)
464+ }
465+ var sent ports.LeaseRequest
466+ select {
467+ case msg := <-requests:
468+ sent = msg.(ports.LeaseRequest)
469+ case <-time.After(time.Second):
470+ t.Fatal("no lease request")
471+ }
472+ requestProtocol(t, c, lease, dl.WpDrmLeaseV1RequestDestroy)
473+ if err := c.Roundtrip(); err != nil {
474+ t.Fatal(err)
475+ }
476+ if !s.display.Do(func() {
477+ if len(s.pendingLeases) != 0 || s.pendingLeaseCards[sent.ID] != sent.Card {
478+ t.Errorf("pending tombstone: %v %v", s.pendingLeases, s.pendingLeaseCards)
479+ }
480+ }) {
481+ t.Fatal("display stopped")
482+ }
483+ if !safelyLeaseMaps(t, c, events, requests, sent) {
484+ t.Fatal("pending reply not revoked")
485+ }
486+ if !s.display.Do(func() {
487+ if len(s.pendingLeases) != 0 || len(s.pendingLeaseCards) != 0 {
488+ t.Errorf("pending leaked: %v %v", s.pendingLeases, s.pendingLeaseCards)
489+ }
490+ }) {
491+ t.Fatal("display stopped")
492+ }
493+}
494+
495+// A destroyed lease keeps only the card tombstone until the DRM reply arrives.
496+func safelyLeaseMaps(t *testing.T, c *wlturbo.Display, events chan ports.LeaseMessage, requests chan ports.LeaseMessage, sent ports.LeaseRequest) bool {
497+ t.Helper()
498+ r, w, err := os.Pipe()
499+ if err != nil {
500+ t.Fatal(err)
501+ }
502+ defer r.Close()
503+ events <- ports.LeaseReply{ID: sent.ID, FD: w, LeaseID: 51}
504+ for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
505+ if err := c.Roundtrip(); err != nil {
506+ t.Fatal(err)
507+ }
508+ select {
509+ case event := <-requests:
510+ revoked, ok := event.(ports.LeaseRevoke)
511+ return ok && revoked.Card == sent.Card && revoked.LeaseID == 51
512+ default:
513+ }
514+ }
515+ return false
516+}
517+
518+func TestLeaseGlobalRemoveAndReadd(t *testing.T) {
519+ c, _, events := leaseServer(t)
520+ count := func() int {
521+ n := 0
522+ for _, g := range c.Registry().GetGlobals() {
523+ if g.Interface == "wp_drm_lease_device_v1" {
524+ n++
525+ }
526+ }
527+ return n
528+ }
529+ if count() != 1 {
530+ t.Fatalf("initial globals %d", count())
531+ }
532+ events <- ports.LeaseConnectors{Card: "/dev/dri/card-test"}
533+ for deadline := time.Now().Add(2 * time.Second); count() != 0 && time.Now().Before(deadline); {
534+ if err := c.Roundtrip(); err != nil {
535+ t.Fatal(err)
536+ }
537+ }
538+ if count() != 0 {
539+ t.Fatal("lease global not removed")
540+ }
541+ events <- ports.LeaseConnectors{Card: "/dev/dri/card-test", Device: leaseFD(t), Connectors: []ports.LeaseConnector{{Card: "/dev/dri/card-test", Name: "DP-1", ConnectorID: 42}}}
542+ for deadline := time.Now().Add(2 * time.Second); count() != 1 && time.Now().Before(deadline); {
543+ if err := c.Roundtrip(); err != nil {
544+ t.Fatal(err)
545+ }
546+ }
547+ if count() != 1 {
548+ t.Fatal("lease global not re-added")
549+ }
550+}
@@ -0,0 +1,550 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "fmt"
6+ "os"
7+ "testing"
8+ "time"
9+
10+ "github.com/bnema/neferwl/internal/logging"
11+ "github.com/bnema/neferwl/internal/ports"
12+ dl "github.com/bnema/purego-libwayland/protocol/drmlease"
13+ "github.com/bnema/purego-libwayland/server"
14+ "github.com/bnema/wlturbo"
15+ "golang.org/x/sys/unix"
16+)
17+
18+type leaseDeviceEvents struct {
19+ wlturbo.BaseProxy
20+ c *wlturbo.Display
21+ connectors []uint32
22+ fds []int
23+}
24+
25+func (p *leaseDeviceEvents) Dispatch(e *wlturbo.Event) {
26+ switch e.Opcode {
27+ case uint16(dl.WpDrmLeaseDeviceV1EventDrmFd):
28+ p.fds = append(p.fds, int(e.Fd()))
29+ case uint16(dl.WpDrmLeaseDeviceV1EventConnector):
30+ id := e.Uint32()
31+ p.connectors = append(p.connectors, id)
32+ child := &leaseConnectorEvents{}
33+ child.SetID(id)
34+ p.c.Context().Register(child)
35+ }
36+}
37+
38+type leaseConnectorEvents struct {
39+ wlturbo.BaseProxy
40+ name string
41+}
42+
43+func (p *leaseConnectorEvents) Dispatch(e *wlturbo.Event) {
44+ if e.Opcode == uint16(dl.WpDrmLeaseConnectorV1EventName) {
45+ p.name = e.String()
46+ }
47+}
48+
49+type leaseResultEvents struct {
50+ wlturbo.BaseProxy
51+ fds []int
52+ finished int
53+}
54+
55+func (p *leaseResultEvents) Dispatch(e *wlturbo.Event) {
56+ switch e.Opcode {
57+ case uint16(dl.WpDrmLeaseV1EventLeaseFd):
58+ p.fds = append(p.fds, int(e.Fd()))
59+ case uint16(dl.WpDrmLeaseV1EventFinished):
60+ p.finished++
61+ }
62+}
63+
64+// leaseFD opens a device fd for one LeaseConnectors message; the server
65+// owns and closes it.
66+func leaseFD(t *testing.T) *os.File {
67+ t.Helper()
68+ f, err := os.Open("/dev/null")
69+ if err != nil {
70+ t.Fatal(err)
71+ }
72+ return f
73+}
74+func leaseServer(t *testing.T) (*wlturbo.Display, chan ports.LeaseMessage, chan ports.LeaseMessage) {
75+ c, requests, events, _ := leaseServerWithState(t)
76+ return c, requests, events
77+}
78+func leaseServerWithState(t *testing.T) (*wlturbo.Display, chan ports.LeaseMessage, chan ports.LeaseMessage, *Server) {
79+ t.Helper()
80+ dir := t.TempDir()
81+ requests := make(chan ports.LeaseMessage, 16)
82+ events := make(chan ports.LeaseMessage, 16)
83+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{LeaseRequests: requests, LeaseEvents: events}, logging.For(context.Background(), "wayland"))
84+ if err != nil {
85+ t.Fatal(err)
86+ }
87+ ctx, cancel := context.WithCancel(context.Background())
88+ done := make(chan error, 1)
89+ go func() { done <- s.Run(ctx) }()
90+ t.Cleanup(func() {
91+ cancel()
92+ select {
93+ case err := <-done:
94+ if err != nil {
95+ t.Error(err)
96+ }
97+ case <-time.After(3 * time.Second):
98+ t.Error("server hung")
99+ }
100+ })
101+ c := protocolClient(t, s, dir)
102+ events <- ports.LeaseConnectors{Card: "/dev/dri/card-test", Device: leaseFD(t), Connectors: []ports.LeaseConnector{{Card: "/dev/dri/card-test", Name: "DP-1", Description: "VR headset", ConnectorID: 42}}}
103+ for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
104+ if err := c.Roundtrip(); err != nil {
105+ t.Fatal(err)
106+ }
107+ if _, ok := c.Registry().FindGlobal("wp_drm_lease_device_v1"); ok {
108+ return c, requests, events, s
109+ }
110+ time.Sleep(time.Millisecond)
111+ }
112+ t.Fatal("no lease global")
113+ return nil, nil, nil, nil
114+}
115+func TestDRMLeaseFlow(t *testing.T) {
116+ c, requests, events := leaseServer(t)
117+ dev := bindProtocol(t, c, "wp_drm_lease_device_v1")
118+ p := &leaseDeviceEvents{c: c}
119+ p.SetID(dev)
120+ c.Context().Register(p)
121+ if err := c.Roundtrip(); err != nil {
122+ t.Fatal(err)
123+ }
124+ if len(p.fds) != 1 || len(p.connectors) != 1 {
125+ t.Fatalf("device: fds=%v connectors=%v", p.fds, p.connectors)
126+ }
127+ defer unix.Close(p.fds[0])
128+ conn := p.connectors[0]
129+ req := c.AllocateID()
130+ registerProtocol(t, c, req)
131+ requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
132+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, conn)
133+ lease := c.AllocateID()
134+ l := &leaseResultEvents{}
135+ l.SetID(lease)
136+ c.Context().Register(l)
137+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, lease)
138+ if err := c.Roundtrip(); err != nil {
139+ t.Fatal(err)
140+ }
141+ var got ports.LeaseRequest
142+ select {
143+ case ev := <-requests:
144+ var ok bool
145+ got, ok = ev.(ports.LeaseRequest)
146+ if !ok {
147+ t.Fatalf("message %T", ev)
148+ }
149+ case <-time.After(2 * time.Second):
150+ t.Fatal("no request")
151+ }
152+ if len(got.Connectors) != 1 || got.Connectors[0] != "DP-1" {
153+ t.Fatalf("request %+v", got)
154+ }
155+ r, w, err := os.Pipe()
156+ if err != nil {
157+ t.Fatal(err)
158+ }
159+ defer r.Close()
160+ events <- ports.LeaseReply{ID: got.ID, FD: w, LeaseID: 7}
161+ if err := c.Roundtrip(); err != nil {
162+ t.Fatal(err)
163+ }
164+ for deadline := time.Now().Add(2 * time.Second); len(l.fds) == 0 && time.Now().Before(deadline); {
165+ if err := c.Roundtrip(); err != nil {
166+ t.Fatal(err)
167+ }
168+ }
169+ if len(l.fds) != 1 {
170+ t.Fatalf("lease fd count %d", len(l.fds))
171+ }
172+ defer unix.Close(l.fds[0])
173+ requestProtocol(t, c, lease, dl.WpDrmLeaseV1RequestDestroy)
174+ if err := c.Roundtrip(); err != nil {
175+ t.Fatal(err)
176+ }
177+ select {
178+ case ev := <-requests:
179+ if v, ok := ev.(ports.LeaseRevoke); !ok || v.LeaseID != 7 {
180+ t.Fatalf("revoke %v", ev)
181+ }
182+ case <-time.After(2 * time.Second):
183+ t.Fatal("no revoke")
184+ }
185+}
186+func TestDRMLeaseErrors(t *testing.T) {
187+ for _, tc := range []struct {
188+ name string
189+ duplicate bool
190+ code uint32
191+ }{{"empty", false, uint32(dl.WpDrmLeaseRequestV1ErrorEmptyLease)}, {"duplicate", true, uint32(dl.WpDrmLeaseRequestV1ErrorDuplicateConnector)}} {
192+ t.Run(tc.name, func(t *testing.T) {
193+ c, _, _ := leaseServer(t)
194+ dev := bindProtocol(t, c, "wp_drm_lease_device_v1")
195+ p := &leaseDeviceEvents{c: c}
196+ p.SetID(dev)
197+ c.Context().Register(p)
198+ if err := c.Roundtrip(); err != nil {
199+ t.Fatal(err)
200+ }
201+ for _, fd := range p.fds {
202+ defer unix.Close(fd)
203+ }
204+ req := c.AllocateID()
205+ registerProtocol(t, c, req)
206+ requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
207+ if tc.duplicate {
208+ conn := p.connectors[0]
209+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, conn)
210+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, conn)
211+ } else {
212+ lease := c.AllocateID()
213+ registerProtocol(t, c, lease)
214+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, lease)
215+ }
216+ expectProtocolError(t, c, req, tc.code)
217+ })
218+ }
219+}
220+func TestDRMLeaseFinished(t *testing.T) {
221+ c, requests, events := leaseServer(t)
222+ dev := bindProtocol(t, c, "wp_drm_lease_device_v1")
223+ p := &leaseDeviceEvents{c: c}
224+ p.SetID(dev)
225+ c.Context().Register(p)
226+ if err := c.Roundtrip(); err != nil {
227+ t.Fatal(err)
228+ }
229+ defer unix.Close(p.fds[0])
230+ req := c.AllocateID()
231+ registerProtocol(t, c, req)
232+ requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
233+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, p.connectors[0])
234+ lease := c.AllocateID()
235+ l := &leaseResultEvents{}
236+ l.SetID(lease)
237+ c.Context().Register(l)
238+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, lease)
239+ if err := c.Roundtrip(); err != nil {
240+ t.Fatal(err)
241+ }
242+ got := (<-requests).(ports.LeaseRequest)
243+ r, w, err := os.Pipe()
244+ if err != nil {
245+ t.Fatal(err)
246+ }
247+ defer r.Close()
248+ events <- ports.LeaseReply{ID: got.ID, FD: w, LeaseID: 8}
249+ if err := c.Roundtrip(); err != nil {
250+ t.Fatal(err)
251+ }
252+ for deadline := time.Now().Add(2 * time.Second); len(l.fds) == 0 && time.Now().Before(deadline); {
253+ if err := c.Roundtrip(); err != nil {
254+ t.Fatal(err)
255+ }
256+ }
257+ if len(l.fds) != 1 {
258+ t.Fatal("missing lease fd")
259+ }
260+ defer unix.Close(l.fds[0])
261+ events <- ports.LeaseFinished{Card: got.Card, LeaseID: 8}
262+ if err := c.Roundtrip(); err != nil {
263+ t.Fatal(err)
264+ }
265+ for deadline := time.Now().Add(2 * time.Second); l.finished == 0 && time.Now().Before(deadline); {
266+ if err := c.Roundtrip(); err != nil {
267+ t.Fatal(err)
268+ }
269+ }
270+ if l.finished != 1 {
271+ t.Fatalf("finished %d", l.finished)
272+ }
273+}
274+
275+func TestLeaseMessagesRemainOrderedWhenChannelFull(t *testing.T) {
276+ requests := make(chan ports.LeaseMessage, 1)
277+ ready := make(chan struct{}, 1)
278+ display, err := server.NewDisplay()
279+ if err != nil {
280+ t.Fatal(err)
281+ }
282+ defer display.Close()
283+ s := &Server{display: display, channels: Channels{LeaseRequests: requests}, leaseReady: ready}
284+ requests <- ports.LeaseRequest{ID: 99}
285+ ctx, cancel := context.WithCancel(context.Background())
286+ done := make(chan struct{})
287+ go func() { s.forwardLeaseRequests(ctx); close(done) }()
288+ defer func() { cancel(); <-done }()
289+ s.sendLeaseRequest(ports.LeaseRequest{ID: 1})
290+ s.sendLeaseRequest(ports.LeaseRevoke{Card: "card", LeaseID: 1})
291+ if got := <-requests; got.(ports.LeaseRequest).ID != 99 {
292+ t.Fatal(got)
293+ }
294+ select {
295+ case got := <-requests:
296+ if v, ok := got.(ports.LeaseRequest); !ok || v.ID != 1 {
297+ t.Fatalf("first %v", got)
298+ }
299+ case <-time.After(time.Second):
300+ t.Fatal("request timeout")
301+ }
302+ select {
303+ case got := <-requests:
304+ if v, ok := got.(ports.LeaseRevoke); !ok || v.LeaseID != 1 {
305+ t.Fatalf("second %v", got)
306+ }
307+ case <-time.After(time.Second):
308+ t.Fatal("revoke timeout")
309+ }
310+}
311+
312+func TestLeaseIDsAreScopedToCard(t *testing.T) {
313+ dir := t.TempDir()
314+ requests := make(chan ports.LeaseMessage, 16)
315+ events := make(chan ports.LeaseMessage, 16)
316+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{LeaseRequests: requests, LeaseEvents: events}, logging.For(context.Background(), "wayland"))
317+ if err != nil {
318+ t.Fatal(err)
319+ }
320+ ctx, cancel := context.WithCancel(context.Background())
321+ done := make(chan error, 1)
322+ go func() { done <- s.Run(ctx) }()
323+ t.Cleanup(func() {
324+ cancel()
325+ if err := <-done; err != nil {
326+ t.Error(err)
327+ }
328+ })
329+ c := protocolClient(t, s, dir)
330+ for i, card := range []string{"card-one", "card-two"} {
331+ events <- ports.LeaseConnectors{Card: card, Device: leaseFD(t), Connectors: []ports.LeaseConnector{{Card: card, Name: fmt.Sprintf("DP-%d", i+1), ConnectorID: uint32(i + 40)}}}
332+ }
333+ for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
334+ if err := c.Roundtrip(); err != nil {
335+ t.Fatal(err)
336+ }
337+ count := 0
338+ for _, g := range c.Registry().GetGlobals() {
339+ if g.Interface == "wp_drm_lease_device_v1" {
340+ count++
341+ }
342+ }
343+ if count == 2 {
344+ break
345+ }
346+ }
347+ var devices []uint32
348+ for _, g := range c.Registry().GetGlobals() {
349+ if g.Interface == "wp_drm_lease_device_v1" {
350+ id, err := c.Registry().BindID(g.Name, g.Interface, 1)
351+ if err != nil {
352+ t.Fatal(err)
353+ }
354+ devices = append(devices, id)
355+ }
356+ }
357+ if len(devices) != 2 {
358+ t.Fatalf("device globals: %d", len(devices))
359+ }
360+ var bound []*leaseDeviceEvents
361+ for _, dev := range devices {
362+ p := &leaseDeviceEvents{c: c}
363+ p.SetID(dev)
364+ c.Context().Register(p)
365+ bound = append(bound, p)
366+ }
367+ if err := c.Roundtrip(); err != nil {
368+ t.Fatal(err)
369+ }
370+ for i, dev := range devices {
371+ p := bound[i]
372+ if len(p.connectors) != 1 || len(p.fds) != 1 {
373+ t.Fatalf("device %d: %+v", dev, p)
374+ }
375+ defer unix.Close(p.fds[0])
376+ req := c.AllocateID()
377+ registerProtocol(t, c, req)
378+ requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
379+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, p.connectors[0])
380+ id := c.AllocateID()
381+ l := &leaseResultEvents{}
382+ l.SetID(id)
383+ c.Context().Register(l)
384+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, id)
385+ }
386+ if err := c.Roundtrip(); err != nil {
387+ t.Fatal(err)
388+ }
389+ first := (<-requests).(ports.LeaseRequest)
390+ second := (<-requests).(ports.LeaseRequest)
391+ if first.Card == second.Card {
392+ t.Fatalf("same card: %v %v", first, second)
393+ }
394+ for _, req := range []ports.LeaseRequest{first, second} {
395+ r, w, err := os.Pipe()
396+ if err != nil {
397+ t.Fatal(err)
398+ }
399+ defer r.Close()
400+ events <- ports.LeaseReply{ID: req.ID, FD: w, LeaseID: 7}
401+ }
402+ if err := c.Roundtrip(); err != nil {
403+ t.Fatal(err)
404+ }
405+ for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
406+ count := 0
407+ s.display.Do(func() { count = len(s.activeLeases) })
408+ if count == 2 {
409+ break
410+ }
411+ if err := c.Roundtrip(); err != nil {
412+ t.Fatal(err)
413+ }
414+ }
415+ if !s.display.Do(func() {
416+ if len(s.activeLeases) != 2 {
417+ t.Errorf("active leases %v", s.activeLeases)
418+ }
419+ }) {
420+ t.Fatal("display stopped")
421+ }
422+ events <- ports.LeaseFinished{Card: first.Card, LeaseID: 7}
423+ if err := c.Roundtrip(); err != nil {
424+ t.Fatal(err)
425+ }
426+ for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
427+ count := 0
428+ s.display.Do(func() { count = len(s.activeLeases) })
429+ if count == 1 {
430+ break
431+ }
432+ if err := c.Roundtrip(); err != nil {
433+ t.Fatal(err)
434+ }
435+ }
436+ if !s.display.Do(func() {
437+ if len(s.activeLeases) != 1 || s.activeLeases[leaseKey{second.Card, 7}] == nil {
438+ t.Errorf("wrong card finished: %v", s.activeLeases)
439+ }
440+ }) {
441+ t.Fatal("display stopped")
442+ }
443+}
444+
445+func TestLeaseDestroyPendingRevokesLateReply(t *testing.T) {
446+ c, requests, events, s := leaseServerWithState(t)
447+ dev := bindProtocol(t, c, "wp_drm_lease_device_v1")
448+ p := &leaseDeviceEvents{c: c}
449+ p.SetID(dev)
450+ c.Context().Register(p)
451+ if err := c.Roundtrip(); err != nil {
452+ t.Fatal(err)
453+ }
454+ defer unix.Close(p.fds[0])
455+ req := c.AllocateID()
456+ registerProtocol(t, c, req)
457+ requestProtocol(t, c, dev, dl.WpDrmLeaseDeviceV1RequestCreateLeaseRequest, req)
458+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestRequestConnector, p.connectors[0])
459+ lease := c.AllocateID()
460+ registerProtocol(t, c, lease)
461+ requestProtocol(t, c, req, dl.WpDrmLeaseRequestV1RequestSubmit, lease)
462+ if err := c.Roundtrip(); err != nil {
463+ t.Fatal(err)
464+ }
465+ var sent ports.LeaseRequest
466+ select {
467+ case msg := <-requests:
468+ sent = msg.(ports.LeaseRequest)
469+ case <-time.After(time.Second):
470+ t.Fatal("no lease request")
471+ }
472+ requestProtocol(t, c, lease, dl.WpDrmLeaseV1RequestDestroy)
473+ if err := c.Roundtrip(); err != nil {
474+ t.Fatal(err)
475+ }
476+ if !s.display.Do(func() {
477+ if len(s.pendingLeases) != 0 || s.pendingLeaseCards[sent.ID] != sent.Card {
478+ t.Errorf("pending tombstone: %v %v", s.pendingLeases, s.pendingLeaseCards)
479+ }
480+ }) {
481+ t.Fatal("display stopped")
482+ }
483+ if !safelyLeaseMaps(t, c, events, requests, sent) {
484+ t.Fatal("pending reply not revoked")
485+ }
486+ if !s.display.Do(func() {
487+ if len(s.pendingLeases) != 0 || len(s.pendingLeaseCards) != 0 {
488+ t.Errorf("pending leaked: %v %v", s.pendingLeases, s.pendingLeaseCards)
489+ }
490+ }) {
491+ t.Fatal("display stopped")
492+ }
493+}
494+
495+// A destroyed lease keeps only the card tombstone until the DRM reply arrives.
496+func safelyLeaseMaps(t *testing.T, c *wlturbo.Display, events chan ports.LeaseMessage, requests chan ports.LeaseMessage, sent ports.LeaseRequest) bool {
497+ t.Helper()
498+ r, w, err := os.Pipe()
499+ if err != nil {
500+ t.Fatal(err)
501+ }
502+ defer r.Close()
503+ events <- ports.LeaseReply{ID: sent.ID, FD: w, LeaseID: 51}
504+ for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
505+ if err := c.Roundtrip(); err != nil {
506+ t.Fatal(err)
507+ }
508+ select {
509+ case event := <-requests:
510+ revoked, ok := event.(ports.LeaseRevoke)
511+ return ok && revoked.Card == sent.Card && revoked.LeaseID == 51
512+ default:
513+ }
514+ }
515+ return false
516+}
517+
518+func TestLeaseGlobalRemoveAndReadd(t *testing.T) {
519+ c, _, events := leaseServer(t)
520+ count := func() int {
521+ n := 0
522+ for _, g := range c.Registry().GetGlobals() {
523+ if g.Interface == "wp_drm_lease_device_v1" {
524+ n++
525+ }
526+ }
527+ return n
528+ }
529+ if count() != 1 {
530+ t.Fatalf("initial globals %d", count())
531+ }
532+ events <- ports.LeaseConnectors{Card: "/dev/dri/card-test"}
533+ for deadline := time.Now().Add(2 * time.Second); count() != 0 && time.Now().Before(deadline); {
534+ if err := c.Roundtrip(); err != nil {
535+ t.Fatal(err)
536+ }
537+ }
538+ if count() != 0 {
539+ t.Fatal("lease global not removed")
540+ }
541+ events <- ports.LeaseConnectors{Card: "/dev/dri/card-test", Device: leaseFD(t), Connectors: []ports.LeaseConnector{{Card: "/dev/dri/card-test", Name: "DP-1", ConnectorID: 42}}}
542+ for deadline := time.Now().Add(2 * time.Second); count() != 1 && time.Now().Before(deadline); {
543+ if err := c.Roundtrip(); err != nil {
544+ t.Fatal(err)
545+ }
546+ }
547+ if count() != 1 {
548+ t.Fatal("lease global not re-added")
549+ }
550+}
M internal/adapters/wayland/fifo.go +1 -1
@@ -294,7 +294,7 @@ func sameLayout(a, b []childLayout) bool {
294294 func (s *surface) takePending() update {
295295 u := update{pendingCommit: s.next}
296296 // One-shot requests apply once; sticky ones carry over.
297- s.next = pendingCommit{scale: s.next.scale, transform: s.next.transform, async: s.next.async, color: s.next.color, representation: s.next.representation, kind: s.next.kind}
297+ s.next = pendingCommit{scale: s.next.scale, transform: s.next.transform, async: s.next.async, color: s.next.color, representation: s.next.representation, kind: s.next.kind, fade: s.next.fade}
298298 if v := s.viewport; v != nil {
299299 u.vp, u.vpW, u.vpH, u.vpSet, u.vpSrc, u.vpCrop = v, v.pendingW, v.pendingH, v.pendingSet, v.pendingSrc, v.pendingCrop
300300 }
@@ -294,7 +294,7 @@ func sameLayout(a, b []childLayout) bool {
294 func (s *surface) takePending() update {294 func (s *surface) takePending() update {
295 u := update{pendingCommit: s.next}295 u := update{pendingCommit: s.next}
296 // One-shot requests apply once; sticky ones carry over.296 // One-shot requests apply once; sticky ones carry over.
297- s.next = pendingCommit{scale: s.next.scale, transform: s.next.transform, async: s.next.async, color: s.next.color, representation: s.next.representation, kind: s.next.kind}297+ s.next = pendingCommit{scale: s.next.scale, transform: s.next.transform, async: s.next.async, color: s.next.color, representation: s.next.representation, kind: s.next.kind, fade: s.next.fade}
298 if v := s.viewport; v != nil {298 if v := s.viewport; v != nil {
299 u.vp, u.vpW, u.vpH, u.vpSet, u.vpSrc, u.vpCrop = v, v.pendingW, v.pendingH, v.pendingSet, v.pendingSrc, v.pendingCrop299 u.vp, u.vpW, u.vpH, u.vpSet, u.vpSrc, u.vpCrop = v, v.pendingW, v.pendingH, v.pendingSet, v.pendingSrc, v.pendingCrop
300 }300 }
M internal/adapters/wayland/globals.go +30 -0
@@ -67,6 +67,8 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
6767 })
6868 },
6969 func() error { return registerClipboard(d, s) },
70+ func() error { return registerFixes(d) },
71+ func() error { return registerAlphaModifier(d, s) },
7072 } {
7173 if err := register(); err != nil {
7274 return err
@@ -75,6 +77,32 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
7577 return nil
7678 }
7779
80+// registerFixes advertises wl_fixes: clients can destroy their registry,
81+// and acknowledge removed globals (v2, libwayland 1.26+).
82+func registerFixes(d *server.Display) error {
83+ version := int32(1)
84+ if server.AckGlobalRemove(nil, nil, 0) {
85+ version = 2
86+ }
87+ return wayland.NewFixesGlobal(d, version, func(c server.Client, v, id uint32) {
88+ _, _ = wayland.NewFixes(c, int32(v), id, fixes{})
89+ })
90+}
91+
92+type fixes struct{}
93+
94+func (fixes) Destroy(*wayland.Fixes) {}
95+func (fixes) DestroyRegistry(_ *wayland.Fixes, r *wayland.Registry) {
96+ if r != nil && r.Resource != nil {
97+ r.Destroy()
98+ }
99+}
100+func (fixes) AckGlobalRemove(f *wayland.Fixes, r *wayland.Registry, name uint32) {
101+ if r != nil {
102+ server.AckGlobalRemove(f.Resource, r.Resource, name)
103+ }
104+}
105+
78106 type compositor struct{ server *Server }
79107
80108 func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) {
@@ -386,6 +414,7 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) {
386414 s := h.server
387415 s.seat.pointers[r.Client()] = append(s.seat.pointers[r.Client()], p)
388416 p.OnDestroy = func() {
417+ delete(s.seat.enters, p.Resource)
389418 list := s.seat.pointers[r.Client()]
390419 for i, item := range list {
391420 if item == p {
@@ -402,6 +431,7 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) {
402431 if s.hasPointerFocus(r.Client()) {
403432 if surf, _, x, y := s.pointerSurface(s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY); surf != nil {
404433 s.serial++
434+ s.seat.enters[p.Resource] = s.serial
405435 p.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y))
406436 pointerFrame(p)
407437 }
@@ -67,6 +67,8 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
67 })67 })
68 },68 },
69 func() error { return registerClipboard(d, s) },69 func() error { return registerClipboard(d, s) },
70+ func() error { return registerFixes(d) },
71+ func() error { return registerAlphaModifier(d, s) },
70 } {72 } {
71 if err := register(); err != nil {73 if err := register(); err != nil {
72 return err74 return err
@@ -75,6 +77,32 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
75 return nil77 return nil
76 }78 }
77 79
80+// registerFixes advertises wl_fixes: clients can destroy their registry,
81+// and acknowledge removed globals (v2, libwayland 1.26+).
82+func registerFixes(d *server.Display) error {
83+ version := int32(1)
84+ if server.AckGlobalRemove(nil, nil, 0) {
85+ version = 2
86+ }
87+ return wayland.NewFixesGlobal(d, version, func(c server.Client, v, id uint32) {
88+ _, _ = wayland.NewFixes(c, int32(v), id, fixes{})
89+ })
90+}
91+
92+type fixes struct{}
93+
94+func (fixes) Destroy(*wayland.Fixes) {}
95+func (fixes) DestroyRegistry(_ *wayland.Fixes, r *wayland.Registry) {
96+ if r != nil && r.Resource != nil {
97+ r.Destroy()
98+ }
99+}
100+func (fixes) AckGlobalRemove(f *wayland.Fixes, r *wayland.Registry, name uint32) {
101+ if r != nil {
102+ server.AckGlobalRemove(f.Resource, r.Resource, name)
103+ }
104+}
105+
78 type compositor struct{ server *Server }106 type compositor struct{ server *Server }
79 107
80 func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) {108 func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) {
@@ -386,6 +414,7 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) {
386 s := h.server414 s := h.server
387 s.seat.pointers[r.Client()] = append(s.seat.pointers[r.Client()], p)415 s.seat.pointers[r.Client()] = append(s.seat.pointers[r.Client()], p)
388 p.OnDestroy = func() {416 p.OnDestroy = func() {
417+ delete(s.seat.enters, p.Resource)
389 list := s.seat.pointers[r.Client()]418 list := s.seat.pointers[r.Client()]
390 for i, item := range list {419 for i, item := range list {
391 if item == p {420 if item == p {
@@ -402,6 +431,7 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) {
402 if s.hasPointerFocus(r.Client()) {431 if s.hasPointerFocus(r.Client()) {
403 if surf, _, x, y := s.pointerSurface(s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY); surf != nil {432 if surf, _, x, y := s.pointerSurface(s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY); surf != nil {
404 s.serial++433 s.serial++
434+ s.seat.enters[p.Resource] = s.serial
405 p.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y))435 p.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y))
406 pointerFrame(p)436 pointerFrame(p)
407 }437 }
M internal/adapters/wayland/outputs_test.go +36 -0
@@ -70,6 +70,42 @@ func (p *closedProxy) Dispatch(e *wlturbo.Event) {
7070 }
7171 }
7272
73+// wl_fixes: a client acknowledges a removed output global and destroys its
74+// registry; a bogus acknowledgement is a protocol error.
75+func TestFixesAckAndDestroyRegistry(t *testing.T) {
76+ s, _, commands, dir := lifecycleServer(t)
77+ c := protocolClient(t, s, dir)
78+ g, ok := c.Registry().FindGlobal("wl_fixes")
79+ if !ok || g.Version != 2 {
80+ t.Fatalf("wl_fixes %+v", g)
81+ }
82+ fixes := bindVersion(t, c, "wl_fixes", 2)
83+ commands <- ports.SetOutputs{Outputs: ports.Layout{testOutputs[0], second}, Focused: "HEADLESS-1"}
84+ waitOutputs(t, c, 2)
85+ var name uint32
86+ for n, g := range c.Registry().GetGlobals() {
87+ if g.Interface == "wl_output" && n > name {
88+ name = n
89+ }
90+ }
91+ commands <- ports.SetOutputs{Outputs: ports.Layout{testOutputs[0]}, Focused: "HEADLESS-1"}
92+ waitOutputs(t, c, 1)
93+ requestProtocol(t, c, fixes, wayland.FixesRequestAckGlobalRemove, c.Registry().ID(), name)
94+ requestProtocol(t, c, fixes, wayland.FixesRequestDestroyRegistry, c.Registry().ID())
95+ if err := c.Roundtrip(); err != nil {
96+ t.Fatalf("valid ack and destroy: %v", err)
97+ }
98+
99+ // A global that is still announced cannot be acknowledged.
100+ c2 := protocolClient(t, s, dir)
101+ fixes2 := bindVersion(t, c2, "wl_fixes", 2)
102+ out, _ := c2.Registry().FindGlobal("wl_output")
103+ requestProtocol(t, c2, fixes2, wayland.FixesRequestAckGlobalRemove, c2.Registry().ID(), out.Name)
104+ if err := c2.Roundtrip(); err == nil {
105+ t.Fatal("ack of a live global accepted")
106+ }
107+}
108+
73109 func TestOutputHotplugAndLayerOutput(t *testing.T) {
74110 s, _, commands, dir := lifecycleServer(t)
75111 c := protocolClient(t, s, dir)
@@ -70,6 +70,42 @@ func (p *closedProxy) Dispatch(e *wlturbo.Event) {
70 }70 }
71 }71 }
72 72
73+// wl_fixes: a client acknowledges a removed output global and destroys its
74+// registry; a bogus acknowledgement is a protocol error.
75+func TestFixesAckAndDestroyRegistry(t *testing.T) {
76+ s, _, commands, dir := lifecycleServer(t)
77+ c := protocolClient(t, s, dir)
78+ g, ok := c.Registry().FindGlobal("wl_fixes")
79+ if !ok || g.Version != 2 {
80+ t.Fatalf("wl_fixes %+v", g)
81+ }
82+ fixes := bindVersion(t, c, "wl_fixes", 2)
83+ commands <- ports.SetOutputs{Outputs: ports.Layout{testOutputs[0], second}, Focused: "HEADLESS-1"}
84+ waitOutputs(t, c, 2)
85+ var name uint32
86+ for n, g := range c.Registry().GetGlobals() {
87+ if g.Interface == "wl_output" && n > name {
88+ name = n
89+ }
90+ }
91+ commands <- ports.SetOutputs{Outputs: ports.Layout{testOutputs[0]}, Focused: "HEADLESS-1"}
92+ waitOutputs(t, c, 1)
93+ requestProtocol(t, c, fixes, wayland.FixesRequestAckGlobalRemove, c.Registry().ID(), name)
94+ requestProtocol(t, c, fixes, wayland.FixesRequestDestroyRegistry, c.Registry().ID())
95+ if err := c.Roundtrip(); err != nil {
96+ t.Fatalf("valid ack and destroy: %v", err)
97+ }
98+
99+ // A global that is still announced cannot be acknowledged.
100+ c2 := protocolClient(t, s, dir)
101+ fixes2 := bindVersion(t, c2, "wl_fixes", 2)
102+ out, _ := c2.Registry().FindGlobal("wl_output")
103+ requestProtocol(t, c2, fixes2, wayland.FixesRequestAckGlobalRemove, c2.Registry().ID(), out.Name)
104+ if err := c2.Roundtrip(); err == nil {
105+ t.Fatal("ack of a live global accepted")
106+ }
107+}
108+
73 func TestOutputHotplugAndLayerOutput(t *testing.T) {109 func TestOutputHotplugAndLayerOutput(t *testing.T) {
74 s, _, commands, dir := lifecycleServer(t)110 s, _, commands, dir := lifecycleServer(t)
75 c := protocolClient(t, s, dir)111 c := protocolClient(t, s, dir)
M internal/adapters/wayland/seat.go +18 -13
@@ -20,19 +20,22 @@ type seatState struct {
2020 // pointerX, pointerY is the pointer as core placed it in pointerFocus;
2121 // pointerSurface and surfacePoint turn it into surface coordinates.
2222 pointerX, pointerY float64
23- wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis
24- wheelHeld [2]float64 // axis value held back with it for pre-v8 clients
25- pointers map[server.Client][]*wayland.Pointer
26- keyboards map[server.Client][]*wayland.Keyboard
27- modState ports.ModState
28- heldKeys map[uint32]bool
29- grabKeys map[uint32]bool // pressed through the input method grab
30- keymapFD int
31- keymapSize uint32
32- keymapText string // the seat keymap, to compare virtual keymaps with
33- keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
34- repeatRate int
35- repeatDelay int
23+ // enters holds the serial of the enter event each wl_pointer received
24+ // for pointerFocus; a pointer warp must name its pointer's serial.
25+ enters map[*server.Resource]uint32
26+ wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis
27+ wheelHeld [2]float64 // axis value held back with it for pre-v8 clients
28+ pointers map[server.Client][]*wayland.Pointer
29+ keyboards map[server.Client][]*wayland.Keyboard
30+ modState ports.ModState
31+ heldKeys map[uint32]bool
32+ grabKeys map[uint32]bool // pressed through the input method grab
33+ keymapFD int
34+ keymapSize uint32
35+ keymapText string // the seat keymap, to compare virtual keymaps with
36+ keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
37+ repeatRate int
38+ repeatDelay int
3639 // press is the serial of the last button or key press, sent to
3740 // pressClient: popup grabs must come from it.
3841 press uint32
@@ -433,10 +436,12 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
433436 }
434437 }
435438 s.seat.pointerFocus = 0
439+ clear(s.seat.enters)
436440 if surface, pointers, sx, sy := s.pointerSurface(id, x, y); surface != nil {
437441 s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY = id, x, y
438442 for _, p := range pointers {
439443 s.serial++
444+ s.seat.enters[p.Resource] = s.serial
440445 p.SendEnter(s.serial, surface, server.FixedFromFloat(sx), server.FixedFromFloat(sy))
441446 pointerFrame(p)
442447 }
@@ -20,19 +20,22 @@ type seatState struct {
20 // pointerX, pointerY is the pointer as core placed it in pointerFocus;20 // pointerX, pointerY is the pointer as core placed it in pointerFocus;
21 // pointerSurface and surfacePoint turn it into surface coordinates.21 // pointerSurface and surfacePoint turn it into surface coordinates.
22 pointerX, pointerY float6422 pointerX, pointerY float64
23- wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis23+ // enters holds the serial of the enter event each wl_pointer received
24- wheelHeld [2]float64 // axis value held back with it for pre-v8 clients24+ // for pointerFocus; a pointer warp must name its pointer's serial.
25- pointers map[server.Client][]*wayland.Pointer25+ enters map[*server.Resource]uint32
26- keyboards map[server.Client][]*wayland.Keyboard26+ wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis
27- modState ports.ModState27+ wheelHeld [2]float64 // axis value held back with it for pre-v8 clients
28- heldKeys map[uint32]bool28+ pointers map[server.Client][]*wayland.Pointer
29- grabKeys map[uint32]bool // pressed through the input method grab29+ keyboards map[server.Client][]*wayland.Keyboard
30- keymapFD int30+ modState ports.ModState
31- keymapSize uint3231+ heldKeys map[uint32]bool
32- keymapText string // the seat keymap, to compare virtual keymaps with32+ grabKeys map[uint32]bool // pressed through the input method grab
33- keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap33+ keymapFD int
34- repeatRate int34+ keymapSize uint32
35- repeatDelay int35+ keymapText string // the seat keymap, to compare virtual keymaps with
36+ keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
37+ repeatRate int
38+ repeatDelay int
36 // press is the serial of the last button or key press, sent to39 // press is the serial of the last button or key press, sent to
37 // pressClient: popup grabs must come from it.40 // pressClient: popup grabs must come from it.
38 press uint3241 press uint32
@@ -433,10 +436,12 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
433 }436 }
434 }437 }
435 s.seat.pointerFocus = 0438 s.seat.pointerFocus = 0
439+ clear(s.seat.enters)
436 if surface, pointers, sx, sy := s.pointerSurface(id, x, y); surface != nil {440 if surface, pointers, sx, sy := s.pointerSurface(id, x, y); surface != nil {
437 s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY = id, x, y441 s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY = id, x, y
438 for _, p := range pointers {442 for _, p := range pointers {
439 s.serial++443 s.serial++
444+ s.seat.enters[p.Resource] = s.serial
440 p.SendEnter(s.serial, surface, server.FixedFromFloat(sx), server.FixedFromFloat(sy))445 p.SendEnter(s.serial, surface, server.FixedFromFloat(sx), server.FixedFromFloat(sy))
441 pointerFrame(p)446 pointerFrame(p)
442 }447 }
M internal/adapters/wayland/server.go +41 -17
@@ -39,10 +39,12 @@ type Options struct {
3939
4040 // Channels carries client notifications and commands. Events may be unbuffered.
4141 type Channels struct {
42- Events chan<- ports.ClientEvent
43- Contents chan<- ports.SurfaceContent
44- Commands <-chan ports.ClientCommand
45- Workspaces <-chan ports.Workspaces
42+ LeaseRequests chan<- ports.LeaseMessage
43+ LeaseEvents <-chan ports.LeaseMessage
44+ Events chan<- ports.ClientEvent
45+ Contents chan<- ports.SurfaceContent
46+ Commands <-chan ports.ClientCommand
47+ Workspaces <-chan ports.Workspaces
4648 // Cursors receives the cursor the client under the pointer asks for.
4749 Cursors chan<- ports.CursorChange
4850 // Presented paces frame callbacks on the outputs' page flips; outputs
@@ -58,17 +60,25 @@ type Channels struct {
5860 OutputApplied <-chan ports.OutputApplied
5961 }
6062 type Server struct {
61- nextCapture uint64
62- captureReplies map[uint64]func(ports.CaptureDone)
63- captureSources map[*server.Resource]*output
64- captureSessions map[*captureSession]struct{}
65- display *server.Display
66- env procEnv
67- slotsPending bool // core waits for a slot window
68- name string
69- cleanup func()
70- log zerowrap.Logger
71- channels Channels
63+ leaseDevices map[string]*leaseDevice
64+ pendingLeases map[uint64]*leaseObject
65+ pendingLeaseCards map[uint64]string
66+ activeLeases map[leaseKey]*leaseObject
67+ leaseMu sync.Mutex // only the outbound lease queue; never window state
68+ leaseOut []ports.LeaseMessage
69+ leaseReady chan struct{}
70+ nextLeaseID uint64
71+ nextCapture uint64
72+ captureReplies map[uint64]func(ports.CaptureDone)
73+ captureSources map[*server.Resource]*output
74+ captureSessions map[*captureSession]struct{}
75+ display *server.Display
76+ env procEnv
77+ slotsPending bool // core waits for a slot window
78+ name string
79+ cleanup func()
80+ log zerowrap.Logger
81+ channels Channels
7282 // awaiting holds frame callbacks by output name, due at its next
7383 // frame (frameDue, or its page flip).
7484 awaiting map[string][]*wayland.Callback
@@ -216,7 +226,12 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
216226 d.Close()
217227 return nil, err
218228 }
219- s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}}
229+ s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}}
230+ s.leaseDevices = map[string]*leaseDevice{}
231+ s.pendingLeases = map[uint64]*leaseObject{}
232+ s.pendingLeaseCards = map[uint64]string{}
233+ s.activeLeases = map[leaseKey]*leaseObject{}
234+ s.leaseReady = make(chan struct{}, 1)
220235 s.clock = opts.Clock
221236 if s.clock == nil {
222237 s.clock = clock.System{}
@@ -298,11 +313,20 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
298313 func (s *Server) SocketName() string { return s.name }
299314 func (s *Server) Run(ctx context.Context) error {
300315 defer s.cleanup()
316+ defer func() {
317+ for _, device := range s.leaseDevices {
318+ if device.fd != nil {
319+ device.fd.Close()
320+ }
321+ }
322+ }()
301323 s.log.Info().Str("socket", s.name).Msg("starting wayland")
302324 s.started = time.Now()
303325 s.ctx = ctx
304326 var wg sync.WaitGroup
305- wg.Add(10)
327+ wg.Add(12)
328+ go func() { defer wg.Done(); s.forwardLeases(ctx) }()
329+ go func() { defer wg.Done(); s.forwardLeaseRequests(ctx) }()
306330 go func() { defer wg.Done(); s.forwardWorkspaces(ctx) }()
307331 go func() { defer wg.Done(); s.forward(ctx) }()
308332 go func() { defer wg.Done(); s.forwardCursors(ctx) }()
@@ -39,10 +39,12 @@ type Options struct {
39 39
40 // Channels carries client notifications and commands. Events may be unbuffered.40 // Channels carries client notifications and commands. Events may be unbuffered.
41 type Channels struct {41 type Channels struct {
42- Events chan<- ports.ClientEvent42+ LeaseRequests chan<- ports.LeaseMessage
43- Contents chan<- ports.SurfaceContent43+ LeaseEvents <-chan ports.LeaseMessage
44- Commands <-chan ports.ClientCommand44+ Events chan<- ports.ClientEvent
45- Workspaces <-chan ports.Workspaces45+ Contents chan<- ports.SurfaceContent
46+ Commands <-chan ports.ClientCommand
47+ Workspaces <-chan ports.Workspaces
46 // Cursors receives the cursor the client under the pointer asks for.48 // Cursors receives the cursor the client under the pointer asks for.
47 Cursors chan<- ports.CursorChange49 Cursors chan<- ports.CursorChange
48 // Presented paces frame callbacks on the outputs' page flips; outputs50 // Presented paces frame callbacks on the outputs' page flips; outputs
@@ -58,17 +60,25 @@ type Channels struct {
58 OutputApplied <-chan ports.OutputApplied60 OutputApplied <-chan ports.OutputApplied
59 }61 }
60 type Server struct {62 type Server struct {
61- nextCapture uint6463+ leaseDevices map[string]*leaseDevice
62- captureReplies map[uint64]func(ports.CaptureDone)64+ pendingLeases map[uint64]*leaseObject
63- captureSources map[*server.Resource]*output65+ pendingLeaseCards map[uint64]string
64- captureSessions map[*captureSession]struct{}66+ activeLeases map[leaseKey]*leaseObject
65- display *server.Display67+ leaseMu sync.Mutex // only the outbound lease queue; never window state
66- env procEnv68+ leaseOut []ports.LeaseMessage
67- slotsPending bool // core waits for a slot window69+ leaseReady chan struct{}
68- name string70+ nextLeaseID uint64
69- cleanup func()71+ nextCapture uint64
70- log zerowrap.Logger72+ captureReplies map[uint64]func(ports.CaptureDone)
71- channels Channels73+ captureSources map[*server.Resource]*output
74+ captureSessions map[*captureSession]struct{}
75+ display *server.Display
76+ env procEnv
77+ slotsPending bool // core waits for a slot window
78+ name string
79+ cleanup func()
80+ log zerowrap.Logger
81+ channels Channels
72 // awaiting holds frame callbacks by output name, due at its next82 // awaiting holds frame callbacks by output name, due at its next
73 // frame (frameDue, or its page flip).83 // frame (frameDue, or its page flip).
74 awaiting map[string][]*wayland.Callback84 awaiting map[string][]*wayland.Callback
@@ -216,7 +226,12 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
216 d.Close()226 d.Close()
217 return nil, err227 return nil, err
218 }228 }
219- s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}}229+ s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}}
230+ s.leaseDevices = map[string]*leaseDevice{}
231+ s.pendingLeases = map[uint64]*leaseObject{}
232+ s.pendingLeaseCards = map[uint64]string{}
233+ s.activeLeases = map[leaseKey]*leaseObject{}
234+ s.leaseReady = make(chan struct{}, 1)
220 s.clock = opts.Clock235 s.clock = opts.Clock
221 if s.clock == nil {236 if s.clock == nil {
222 s.clock = clock.System{}237 s.clock = clock.System{}
@@ -298,11 +313,20 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
298 func (s *Server) SocketName() string { return s.name }313 func (s *Server) SocketName() string { return s.name }
299 func (s *Server) Run(ctx context.Context) error {314 func (s *Server) Run(ctx context.Context) error {
300 defer s.cleanup()315 defer s.cleanup()
316+ defer func() {
317+ for _, device := range s.leaseDevices {
318+ if device.fd != nil {
319+ device.fd.Close()
320+ }
321+ }
322+ }()
301 s.log.Info().Str("socket", s.name).Msg("starting wayland")323 s.log.Info().Str("socket", s.name).Msg("starting wayland")
302 s.started = time.Now()324 s.started = time.Now()
303 s.ctx = ctx325 s.ctx = ctx
304 var wg sync.WaitGroup326 var wg sync.WaitGroup
305- wg.Add(10)327+ wg.Add(12)
328+ go func() { defer wg.Done(); s.forwardLeases(ctx) }()
329+ go func() { defer wg.Done(); s.forwardLeaseRequests(ctx) }()
306 go func() { defer wg.Done(); s.forwardWorkspaces(ctx) }()330 go func() { defer wg.Done(); s.forwardWorkspaces(ctx) }()
307 go func() { defer wg.Done(); s.forward(ctx) }()331 go func() { defer wg.Done(); s.forward(ctx) }()
308 go func() { defer wg.Done(); s.forwardCursors(ctx) }()332 go func() { defer wg.Done(); s.forwardCursors(ctx) }()
M internal/adapters/wayland/surface.go +11 -4
@@ -84,6 +84,8 @@ type surface struct {
8484 queue []*update
8585 // contentType is the wp_content_type_v1; contentKind the committed type.
8686 contentType *contentTypeHandler
87+ // alpha is the surface's wp_alpha_modifier_surface_v1.
88+ alpha *alphaHandler
8789 contentKind uint32
8890 // committed is what the last commit changed, in buffer pixels (full:
8991 // everything), read by the window's damage history.
@@ -115,6 +117,8 @@ type pendingCommit struct {
115117 color SurfaceColor
116118 representation surfaceRepresentation
117119 kind uint32
120+ // fade is 1 - the wp_alpha_modifier_v1 multiplier (sticky).
121+ fade float32
118122 // barrier and wait are fifo requests; at the commit timestamp.
119123 barrier, wait bool
120124 at time.Time
@@ -375,6 +379,7 @@ func (s *surface) applyCommit(u *update) {
375379 if u.opaqueSet {
376380 s.opaque = u.opaque
377381 }
382+ oldFade := s.content.Fade
378383 hinted := s.async != u.async || s.color != u.color || s.representation != u.representation
379384 if s.async != u.async {
380385 // Contents carry the hint of every surface in the tree.
@@ -425,7 +430,8 @@ func (s *surface) applyCommit(u *update) {
425430 c.Source, _ = s.source(c.Width, c.Height)
426431 c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)
427432 s.formatOpaque = c.Opaque
428- c.Opaque = c.Opaque || s.opaqueCovers(c.LogicalW, c.LogicalH)
433+ c.Opaque = (c.Opaque || s.opaqueCovers(c.LogicalW, c.LogicalH)) && u.fade == 0
434+ c.Fade = u.fade
429435 resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH
430436 s.commitDamage(u, true, resized, c.Width, c.Height)
431437 if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) {
@@ -453,13 +459,14 @@ func (s *surface) applyCommit(u *update) {
453459 s.content.Transform = s.transform
454460 s.content.Source, _ = s.source(s.content.Width, s.content.Height)
455461 s.content.LogicalW, s.content.LogicalH = s.logicalSize(s.content.Width, s.content.Height)
456- s.content.Opaque = s.formatOpaque || s.opaqueCovers(s.content.LogicalW, s.content.LogicalH)
462+ s.content.Opaque = (s.formatOpaque || s.opaqueCovers(s.content.LogicalW, s.content.LogicalH)) && u.fade == 0
463+ s.content.Fade = u.fade
457464 }
458465 }
459466 if s.current == nil {
460467 s.content, s.has = ports.SurfaceContent{}, false
461468 }
462- if fresh || damaged || oldW != s.content.LogicalW || oldH != s.content.LogicalH || oldSource != s.content.Source || oldColor != s.color || oldRepresentation != s.representation || oldTransform != s.content.Transform || oldOpaque != s.content.Opaque {
469+ if fresh || damaged || oldW != s.content.LogicalW || oldH != s.content.LogicalH || oldSource != s.content.Source || oldColor != s.color || oldRepresentation != s.representation || oldTransform != s.content.Transform || oldOpaque != s.content.Opaque || oldFade != s.content.Fade {
463470 s.version++
464471 s.root().treeDirty = true
465472 }
@@ -483,7 +490,7 @@ func (s *surface) applyCommit(u *update) {
483490 }
484491 reshaped := s.has && (s.content.LogicalW != oldW || s.content.LogicalH != oldH || s.content.Source != oldSource || s.content.Transform != oldTransform)
485492 // Opacity changes how every pixel blends, so it repaints the whole surface.
486- opacity := s.has && s.content.Opaque != oldOpaque
493+ opacity := s.has && (s.content.Opaque != oldOpaque || s.content.Fade != oldFade)
487494 drawn := fresh || damaged || moved || geometry || hinted || reshaped || opacity || s.sub.parent != nil
488495 if drawn {
489496 if moved || geometry || reshaped || opacity {
@@ -84,6 +84,8 @@ type surface struct {
84 queue []*update84 queue []*update
85 // contentType is the wp_content_type_v1; contentKind the committed type.85 // contentType is the wp_content_type_v1; contentKind the committed type.
86 contentType *contentTypeHandler86 contentType *contentTypeHandler
87+ // alpha is the surface's wp_alpha_modifier_surface_v1.
88+ alpha *alphaHandler
87 contentKind uint3289 contentKind uint32
88 // committed is what the last commit changed, in buffer pixels (full:90 // committed is what the last commit changed, in buffer pixels (full:
89 // everything), read by the window's damage history.91 // everything), read by the window's damage history.
@@ -115,6 +117,8 @@ type pendingCommit struct {
115 color SurfaceColor117 color SurfaceColor
116 representation surfaceRepresentation118 representation surfaceRepresentation
117 kind uint32119 kind uint32
120+ // fade is 1 - the wp_alpha_modifier_v1 multiplier (sticky).
121+ fade float32
118 // barrier and wait are fifo requests; at the commit timestamp.122 // barrier and wait are fifo requests; at the commit timestamp.
119 barrier, wait bool123 barrier, wait bool
120 at time.Time124 at time.Time
@@ -375,6 +379,7 @@ func (s *surface) applyCommit(u *update) {
375 if u.opaqueSet {379 if u.opaqueSet {
376 s.opaque = u.opaque380 s.opaque = u.opaque
377 }381 }
382+ oldFade := s.content.Fade
378 hinted := s.async != u.async || s.color != u.color || s.representation != u.representation383 hinted := s.async != u.async || s.color != u.color || s.representation != u.representation
379 if s.async != u.async {384 if s.async != u.async {
380 // Contents carry the hint of every surface in the tree.385 // Contents carry the hint of every surface in the tree.
@@ -425,7 +430,8 @@ func (s *surface) applyCommit(u *update) {
425 c.Source, _ = s.source(c.Width, c.Height)430 c.Source, _ = s.source(c.Width, c.Height)
426 c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)431 c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)
427 s.formatOpaque = c.Opaque432 s.formatOpaque = c.Opaque
428- c.Opaque = c.Opaque || s.opaqueCovers(c.LogicalW, c.LogicalH)433+ c.Opaque = (c.Opaque || s.opaqueCovers(c.LogicalW, c.LogicalH)) && u.fade == 0
434+ c.Fade = u.fade
429 resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH435 resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH
430 s.commitDamage(u, true, resized, c.Width, c.Height)436 s.commitDamage(u, true, resized, c.Width, c.Height)
431 if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) {437 if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) {
@@ -453,13 +459,14 @@ func (s *surface) applyCommit(u *update) {
453 s.content.Transform = s.transform459 s.content.Transform = s.transform
454 s.content.Source, _ = s.source(s.content.Width, s.content.Height)460 s.content.Source, _ = s.source(s.content.Width, s.content.Height)
455 s.content.LogicalW, s.content.LogicalH = s.logicalSize(s.content.Width, s.content.Height)461 s.content.LogicalW, s.content.LogicalH = s.logicalSize(s.content.Width, s.content.Height)
456- s.content.Opaque = s.formatOpaque || s.opaqueCovers(s.content.LogicalW, s.content.LogicalH)462+ s.content.Opaque = (s.formatOpaque || s.opaqueCovers(s.content.LogicalW, s.content.LogicalH)) && u.fade == 0
463+ s.content.Fade = u.fade
457 }464 }
458 }465 }
459 if s.current == nil {466 if s.current == nil {
460 s.content, s.has = ports.SurfaceContent{}, false467 s.content, s.has = ports.SurfaceContent{}, false
461 }468 }
462- if fresh || damaged || oldW != s.content.LogicalW || oldH != s.content.LogicalH || oldSource != s.content.Source || oldColor != s.color || oldRepresentation != s.representation || oldTransform != s.content.Transform || oldOpaque != s.content.Opaque {469+ if fresh || damaged || oldW != s.content.LogicalW || oldH != s.content.LogicalH || oldSource != s.content.Source || oldColor != s.color || oldRepresentation != s.representation || oldTransform != s.content.Transform || oldOpaque != s.content.Opaque || oldFade != s.content.Fade {
463 s.version++470 s.version++
464 s.root().treeDirty = true471 s.root().treeDirty = true
465 }472 }
@@ -483,7 +490,7 @@ func (s *surface) applyCommit(u *update) {
483 }490 }
484 reshaped := s.has && (s.content.LogicalW != oldW || s.content.LogicalH != oldH || s.content.Source != oldSource || s.content.Transform != oldTransform)491 reshaped := s.has && (s.content.LogicalW != oldW || s.content.LogicalH != oldH || s.content.Source != oldSource || s.content.Transform != oldTransform)
485 // Opacity changes how every pixel blends, so it repaints the whole surface.492 // Opacity changes how every pixel blends, so it repaints the whole surface.
486- opacity := s.has && s.content.Opaque != oldOpaque493+ opacity := s.has && (s.content.Opaque != oldOpaque || s.content.Fade != oldFade)
487 drawn := fresh || damaged || moved || geometry || hinted || reshaped || opacity || s.sub.parent != nil494 drawn := fresh || damaged || moved || geometry || hinted || reshaped || opacity || s.sub.parent != nil
488 if drawn {495 if drawn {
489 if moved || geometry || reshaped || opacity {496 if moved || geometry || reshaped || opacity {
M internal/app/app.go +4 -2
@@ -102,6 +102,8 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
102102 captured := make(chan ports.CaptureDone, 64)
103103 outputFormats := make(chan ports.OutputFormats, 8)
104104 outputHeads := make(chan ports.OutputHeads, 8)
105+ leaseRequests := make(chan ports.LeaseMessage, 32)
106+ leaseEvents := make(chan ports.LeaseMessage, 32)
105107 applyOutput := make(chan ports.OutputApply, 8)
106108 appliedOutput := make(chan ports.OutputApplied, 8)
107109 var scales chan ports.ScaleChanged
@@ -123,7 +125,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
123125 // Every output renders on the same GPU: its formats are the clients'.
124126 dmabuf := vulkan.Probe()
125127 log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")
126- server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, SyncobjNode: renderNode(dmabuf.Device), Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Workspaces: workspaces, Contents: contents, Cursors: cursorChanges, Presented: presented, Captures: captures, Captured: captured, OutputFormats: outputFormats, OutputHeads: outputHeads, OutputApply: applyOutput, OutputApplied: appliedOutput}, logging.For(ctx, "wayland"))
128+ server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, SyncobjNode: renderNode(dmabuf.Device), Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Workspaces: workspaces, Contents: contents, Cursors: cursorChanges, Presented: presented, Captures: captures, Captured: captured, OutputFormats: outputFormats, OutputHeads: outputHeads, LeaseRequests: leaseRequests, LeaseEvents: leaseEvents, OutputApply: applyOutput, OutputApplied: appliedOutput}, logging.For(ctx, "wayland"))
127129 if err != nil {
128130 km.Close()
129131 return err
@@ -247,7 +249,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
247249
248250 go func() {
249251 defer workers.Done()
250- outputIO := outputChannels{events: output, scenes: renderScenes, contents: contents, cursorChanges: cursorChanges, presented: flips, captures: captures, captured: captured, formats: outputFormats, heads: outputHeads, reloads: filtered, configured: configChanges, requests: applyOutput, replies: appliedOutput}
252+ outputIO := outputChannels{events: output, scenes: renderScenes, contents: contents, cursorChanges: cursorChanges, presented: flips, captures: captures, captured: captured, formats: outputFormats, heads: outputHeads, leaseRequests: leaseRequests, leaseEvents: leaseEvents, reloads: filtered, configured: configChanges, requests: applyOutput, replies: appliedOutput}
251253 apply := newOutputApply(newOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app"))
252254 renderLog := logging.For(ctx, "render")
253255 newRenderer := func(w, h int) (ports.Renderer, error) {
@@ -102,6 +102,8 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
102 captured := make(chan ports.CaptureDone, 64)102 captured := make(chan ports.CaptureDone, 64)
103 outputFormats := make(chan ports.OutputFormats, 8)103 outputFormats := make(chan ports.OutputFormats, 8)
104 outputHeads := make(chan ports.OutputHeads, 8)104 outputHeads := make(chan ports.OutputHeads, 8)
105+ leaseRequests := make(chan ports.LeaseMessage, 32)
106+ leaseEvents := make(chan ports.LeaseMessage, 32)
105 applyOutput := make(chan ports.OutputApply, 8)107 applyOutput := make(chan ports.OutputApply, 8)
106 appliedOutput := make(chan ports.OutputApplied, 8)108 appliedOutput := make(chan ports.OutputApplied, 8)
107 var scales chan ports.ScaleChanged109 var scales chan ports.ScaleChanged
@@ -123,7 +125,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
123 // Every output renders on the same GPU: its formats are the clients'.125 // Every output renders on the same GPU: its formats are the clients'.
124 dmabuf := vulkan.Probe()126 dmabuf := vulkan.Probe()
125 log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")127 log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")
126- server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, SyncobjNode: renderNode(dmabuf.Device), Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Workspaces: workspaces, Contents: contents, Cursors: cursorChanges, Presented: presented, Captures: captures, Captured: captured, OutputFormats: outputFormats, OutputHeads: outputHeads, OutputApply: applyOutput, OutputApplied: appliedOutput}, logging.For(ctx, "wayland"))128+ server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, SyncobjNode: renderNode(dmabuf.Device), Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Workspaces: workspaces, Contents: contents, Cursors: cursorChanges, Presented: presented, Captures: captures, Captured: captured, OutputFormats: outputFormats, OutputHeads: outputHeads, LeaseRequests: leaseRequests, LeaseEvents: leaseEvents, OutputApply: applyOutput, OutputApplied: appliedOutput}, logging.For(ctx, "wayland"))
127 if err != nil {129 if err != nil {
128 km.Close()130 km.Close()
129 return err131 return err
@@ -247,7 +249,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
247 249
248 go func() {250 go func() {
249 defer workers.Done()251 defer workers.Done()
250- outputIO := outputChannels{events: output, scenes: renderScenes, contents: contents, cursorChanges: cursorChanges, presented: flips, captures: captures, captured: captured, formats: outputFormats, heads: outputHeads, reloads: filtered, configured: configChanges, requests: applyOutput, replies: appliedOutput}252+ outputIO := outputChannels{events: output, scenes: renderScenes, contents: contents, cursorChanges: cursorChanges, presented: flips, captures: captures, captured: captured, formats: outputFormats, heads: outputHeads, leaseRequests: leaseRequests, leaseEvents: leaseEvents, reloads: filtered, configured: configChanges, requests: applyOutput, replies: appliedOutput}
251 apply := newOutputApply(newOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app"))253 apply := newOutputApply(newOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app"))
252 renderLog := logging.For(ctx, "render")254 renderLog := logging.For(ctx, "render")
253 newRenderer := func(w, h int) (ports.Renderer, error) {255 newRenderer := func(w, h int) (ports.Renderer, error) {
M internal/app/drm.go +105 -0
@@ -4,9 +4,11 @@ import (
44 "context"
55 "errors"
66 "fmt"
7+ "os"
78 "path/filepath"
89 "runtime"
910 "runtime/debug"
11+ "slices"
1012 "sort"
1113 "sync"
1214 "time"
@@ -19,6 +21,7 @@ import (
1921 "github.com/bnema/neferwl/internal/logging"
2022 "github.com/bnema/neferwl/internal/ports"
2123 "github.com/bnema/zerowrap"
24+ "golang.org/x/sys/unix"
2225 )
2326
2427 var errTimeout = errors.New("timeout")
@@ -158,6 +161,60 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
158161 })
159162 }
160163 defer readers.Wait()
164+ active := b.seat.Subscribe()
165+ defer b.seat.Unsubscribe(active)
166+ seatActive := true // openDRM waits for the first seat enable
167+ clientFDs := map[*drmCard]*os.File{}
168+ lastLeaseConnectors := map[*drmCard][]ports.LeaseConnector{}
169+ defer func() {
170+ for _, f := range clientFDs {
171+ f.Close()
172+ }
173+ }()
174+ sendLease := func(msg ports.LeaseMessage) {
175+ select {
176+ case ch.leaseEvents <- msg:
177+ case <-ctx.Done():
178+ ports.CloseLeaseFiles(msg)
179+ }
180+ }
181+ publishLeases := func(c *drmCard) {
182+ for _, id := range c.FinishedLeases() {
183+ sendLease(ports.LeaseFinished{Card: c.Path(), LeaseID: id})
184+ }
185+ var next []ports.LeaseConnector
186+ if seatActive {
187+ next = c.Leasable()
188+ }
189+ if len(next) == 0 {
190+ if clientFDs[c] != nil {
191+ sendLease(ports.LeaseConnectors{Card: c.Path()})
192+ clientFDs[c].Close()
193+ delete(clientFDs, c)
194+ }
195+ delete(lastLeaseConnectors, c)
196+ return
197+ }
198+ if slices.Equal(lastLeaseConnectors[c], next) && clientFDs[c] != nil {
199+ return
200+ }
201+ if clientFDs[c] == nil {
202+ f, err := c.ClientFD()
203+ if err != nil {
204+ log.Warn().Err(err).Str("card", c.Path()).Msg("lease client fd")
205+ return
206+ }
207+ clientFDs[c] = f
208+ }
209+ // Each message owns its fd: a queued inventory outlives clientFDs[c].
210+ fd, err := unix.FcntlInt(clientFDs[c].Fd(), unix.F_DUPFD_CLOEXEC, 0)
211+ if err != nil {
212+ log.Warn().Err(err).Str("card", c.Path()).Msg("lease client fd")
213+ return
214+ }
215+ lastLeaseConnectors[c] = next
216+ sendLease(ports.LeaseConnectors{Card: c.Path(), Device: os.NewFile(uintptr(fd), c.Path()), Connectors: next})
217+ }
161218 hotplug := make(chan struct{}, 1)
162219 go func() {
163220 if err := safe("hotplug", func() error { return drm.WatchHotplug(ctx, hotplug) }); err != nil {
@@ -226,6 +283,7 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
226283 scanErrors = append(scanErrors, fmt.Errorf("scan %s: %w", c.Path(), err))
227284 continue
228285 }
286+ publishLeases(c)
229287 stop := func(name string, restart bool) {
230288 if r := set.outs[name]; r != nil && cards[name] == c {
231289 if _, ok := stopping[name]; !ok {
@@ -360,6 +418,53 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
360418 }
361419 complete(progress.scanned(running))
362420 }
421+ case enabled := <-active:
422+ seatActive = enabled
423+ if !enabled {
424+ for _, c := range b.cards {
425+ for _, id := range c.LeaseIDs() {
426+ if err := c.Revoke(id); err != nil {
427+ log.Warn().Err(err).Uint32("lease", id).Msg("VT lease revoke")
428+ } else {
429+ sendLease(ports.LeaseFinished{Card: c.Path(), LeaseID: id})
430+ }
431+ }
432+ publishLeases(c)
433+ }
434+ } else {
435+ for _, c := range b.cards {
436+ publishLeases(c)
437+ }
438+ }
439+ case event := <-ch.leaseRequests:
440+ switch req := event.(type) {
441+ case ports.LeaseRequest:
442+ reply := ports.LeaseReply{ID: req.ID, Err: fmt.Errorf("unknown card %s", req.Card)}
443+ if !seatActive {
444+ reply.Err = errors.New("DRM seat inactive")
445+ sendLease(reply)
446+ break
447+ }
448+ for _, c := range b.cards {
449+ if c.Path() == req.Card {
450+ reply.FD, reply.LeaseID, reply.Err = c.Lease(req.Connectors)
451+ publishLeases(c)
452+ break
453+ }
454+ }
455+ sendLease(reply)
456+ case ports.LeaseRevoke:
457+ for _, c := range b.cards {
458+ if c.Path() == req.Card {
459+ if err := c.Revoke(req.LeaseID); err != nil {
460+ log.Warn().Err(err).Uint32("lease", req.LeaseID).Msg("revoke lease")
461+ } else {
462+ publishLeases(c)
463+ }
464+ break
465+ }
466+ }
467+ }
363468 case ev := <-ch.reloads:
364469 apply.reload(ev.Config)
365470 case req := <-ch.requests:
@@ -4,9 +4,11 @@ import (
4 "context"4 "context"
5 "errors"5 "errors"
6 "fmt"6 "fmt"
7+ "os"
7 "path/filepath"8 "path/filepath"
8 "runtime"9 "runtime"
9 "runtime/debug"10 "runtime/debug"
11+ "slices"
10 "sort"12 "sort"
11 "sync"13 "sync"
12 "time"14 "time"
@@ -19,6 +21,7 @@ import (
19 "github.com/bnema/neferwl/internal/logging"21 "github.com/bnema/neferwl/internal/logging"
20 "github.com/bnema/neferwl/internal/ports"22 "github.com/bnema/neferwl/internal/ports"
21 "github.com/bnema/zerowrap"23 "github.com/bnema/zerowrap"
24+ "golang.org/x/sys/unix"
22 )25 )
23 26
24 var errTimeout = errors.New("timeout")27 var errTimeout = errors.New("timeout")
@@ -158,6 +161,60 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
158 })161 })
159 }162 }
160 defer readers.Wait()163 defer readers.Wait()
164+ active := b.seat.Subscribe()
165+ defer b.seat.Unsubscribe(active)
166+ seatActive := true // openDRM waits for the first seat enable
167+ clientFDs := map[*drmCard]*os.File{}
168+ lastLeaseConnectors := map[*drmCard][]ports.LeaseConnector{}
169+ defer func() {
170+ for _, f := range clientFDs {
171+ f.Close()
172+ }
173+ }()
174+ sendLease := func(msg ports.LeaseMessage) {
175+ select {
176+ case ch.leaseEvents <- msg:
177+ case <-ctx.Done():
178+ ports.CloseLeaseFiles(msg)
179+ }
180+ }
181+ publishLeases := func(c *drmCard) {
182+ for _, id := range c.FinishedLeases() {
183+ sendLease(ports.LeaseFinished{Card: c.Path(), LeaseID: id})
184+ }
185+ var next []ports.LeaseConnector
186+ if seatActive {
187+ next = c.Leasable()
188+ }
189+ if len(next) == 0 {
190+ if clientFDs[c] != nil {
191+ sendLease(ports.LeaseConnectors{Card: c.Path()})
192+ clientFDs[c].Close()
193+ delete(clientFDs, c)
194+ }
195+ delete(lastLeaseConnectors, c)
196+ return
197+ }
198+ if slices.Equal(lastLeaseConnectors[c], next) && clientFDs[c] != nil {
199+ return
200+ }
201+ if clientFDs[c] == nil {
202+ f, err := c.ClientFD()
203+ if err != nil {
204+ log.Warn().Err(err).Str("card", c.Path()).Msg("lease client fd")
205+ return
206+ }
207+ clientFDs[c] = f
208+ }
209+ // Each message owns its fd: a queued inventory outlives clientFDs[c].
210+ fd, err := unix.FcntlInt(clientFDs[c].Fd(), unix.F_DUPFD_CLOEXEC, 0)
211+ if err != nil {
212+ log.Warn().Err(err).Str("card", c.Path()).Msg("lease client fd")
213+ return
214+ }
215+ lastLeaseConnectors[c] = next
216+ sendLease(ports.LeaseConnectors{Card: c.Path(), Device: os.NewFile(uintptr(fd), c.Path()), Connectors: next})
217+ }
161 hotplug := make(chan struct{}, 1)218 hotplug := make(chan struct{}, 1)
162 go func() {219 go func() {
163 if err := safe("hotplug", func() error { return drm.WatchHotplug(ctx, hotplug) }); err != nil {220 if err := safe("hotplug", func() error { return drm.WatchHotplug(ctx, hotplug) }); err != nil {
@@ -226,6 +283,7 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
226 scanErrors = append(scanErrors, fmt.Errorf("scan %s: %w", c.Path(), err))283 scanErrors = append(scanErrors, fmt.Errorf("scan %s: %w", c.Path(), err))
227 continue284 continue
228 }285 }
286+ publishLeases(c)
229 stop := func(name string, restart bool) {287 stop := func(name string, restart bool) {
230 if r := set.outs[name]; r != nil && cards[name] == c {288 if r := set.outs[name]; r != nil && cards[name] == c {
231 if _, ok := stopping[name]; !ok {289 if _, ok := stopping[name]; !ok {
@@ -360,6 +418,53 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
360 }418 }
361 complete(progress.scanned(running))419 complete(progress.scanned(running))
362 }420 }
421+ case enabled := <-active:
422+ seatActive = enabled
423+ if !enabled {
424+ for _, c := range b.cards {
425+ for _, id := range c.LeaseIDs() {
426+ if err := c.Revoke(id); err != nil {
427+ log.Warn().Err(err).Uint32("lease", id).Msg("VT lease revoke")
428+ } else {
429+ sendLease(ports.LeaseFinished{Card: c.Path(), LeaseID: id})
430+ }
431+ }
432+ publishLeases(c)
433+ }
434+ } else {
435+ for _, c := range b.cards {
436+ publishLeases(c)
437+ }
438+ }
439+ case event := <-ch.leaseRequests:
440+ switch req := event.(type) {
441+ case ports.LeaseRequest:
442+ reply := ports.LeaseReply{ID: req.ID, Err: fmt.Errorf("unknown card %s", req.Card)}
443+ if !seatActive {
444+ reply.Err = errors.New("DRM seat inactive")
445+ sendLease(reply)
446+ break
447+ }
448+ for _, c := range b.cards {
449+ if c.Path() == req.Card {
450+ reply.FD, reply.LeaseID, reply.Err = c.Lease(req.Connectors)
451+ publishLeases(c)
452+ break
453+ }
454+ }
455+ sendLease(reply)
456+ case ports.LeaseRevoke:
457+ for _, c := range b.cards {
458+ if c.Path() == req.Card {
459+ if err := c.Revoke(req.LeaseID); err != nil {
460+ log.Warn().Err(err).Uint32("lease", req.LeaseID).Msg("revoke lease")
461+ } else {
462+ publishLeases(c)
463+ }
464+ break
465+ }
466+ }
467+ }
363 case ev := <-ch.reloads:468 case ev := <-ch.reloads:
364 apply.reload(ev.Config)469 apply.reload(ev.Config)
365 case req := <-ch.requests:470 case req := <-ch.requests:
M internal/app/outputrouting.go +2 -0
@@ -7,6 +7,8 @@ import "github.com/bnema/neferwl/internal/ports"
77 // forwarding queue. heads, configured and replies are fed from the
88 // outputApply outbox.
99 type outputChannels struct {
10+ leaseRequests <-chan ports.LeaseMessage
11+ leaseEvents chan<- ports.LeaseMessage
1012 events chan<- ports.OutputEvent
1113 scenes <-chan []ports.Scene
1214 contents <-chan ports.SurfaceContent
@@ -7,6 +7,8 @@ import "github.com/bnema/neferwl/internal/ports"
7 // forwarding queue. heads, configured and replies are fed from the7 // forwarding queue. heads, configured and replies are fed from the
8 // outputApply outbox.8 // outputApply outbox.
9 type outputChannels struct {9 type outputChannels struct {
10+ leaseRequests <-chan ports.LeaseMessage
11+ leaseEvents chan<- ports.LeaseMessage
10 events chan<- ports.OutputEvent12 events chan<- ports.OutputEvent
11 scenes <-chan []ports.Scene13 scenes <-chan []ports.Scene
12 contents <-chan ports.SurfaceContent14 contents <-chan ports.SurfaceContent
M internal/core/core.go +58 -18
@@ -578,26 +578,16 @@ func (c *Core) updateInhibit(ctx context.Context) error {
578578 // and sends it to input when it changed. A hidden window holds nothing.
579579 func (c *Core) resolveConstraint() {
580580 var g ports.PointerConstraint
581- if id := c.constrained.ID; id != 0 {
582- if s, _ := c.screenOf(id); s != nil {
583- for _, p := range s.mon.Layout() {
584- if p.ID != id || p.Hidden || p.Preview > 0 {
585- continue
586- }
587- r := c.clientRect(p)
588- r.X += s.x
589- r.Y += s.y
590- if w := c.constrained.Rect; w.W > 0 && w.H > 0 {
591- // The region is clipped to the window.
592- x0, y0 := max(r.X, r.X+w.X), max(r.Y, r.Y+w.Y)
593- x1, y1 := min(r.X+r.W, r.X+w.X+w.W), min(r.Y+r.H, r.Y+w.Y+w.H)
594- if x1 > x0 && y1 > y0 {
595- r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
596- }
597- }
598- g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r}
581+ if r, ok := c.shownClientRect(c.constrained.ID); ok {
582+ if w := c.constrained.Rect; w.W > 0 && w.H > 0 {
583+ // The region is clipped to the window.
584+ x0, y0 := max(r.X, r.X+w.X), max(r.Y, r.Y+w.Y)
585+ x1, y1 := min(r.X+r.W, r.X+w.X+w.W), min(r.Y+r.H, r.Y+w.Y+w.H)
586+ if x1 > x0 && y1 > y0 {
587+ r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
599588 }
600589 }
590+ g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r}
601591 }
602592 // The cursor follows core; input resyncs to it only on a change.
603593 if g.Mode == c.constraint.Mode && g.Rect == c.constraint.Rect {
@@ -612,6 +602,52 @@ func (c *Core) resolveConstraint() {
612602 }
613603 }
614604
605+// shownClientRect is the global logical client rectangle of a window
606+// drawn on its output; false when it is hidden or a preview.
607+func (c *Core) shownClientRect(id WindowID) (Rect, bool) {
608+ if id == 0 {
609+ return Rect{}, false
610+ }
611+ s, _ := c.screenOf(id)
612+ if s == nil {
613+ return Rect{}, false
614+ }
615+ for _, p := range s.mon.Layout() {
616+ if p.ID != id || p.Hidden || p.Preview > 0 {
617+ continue
618+ }
619+ r := c.clientRect(p)
620+ r.X += s.x
621+ r.Y += s.y
622+ return r, true
623+ }
624+ return Rect{}, false
625+}
626+
627+// warpPointer moves the cursor to a window-local point of the window under
628+// it (wp_pointer_warp_v1). Input takes the new position, and the window
629+// gets the motion; the active constraint still bounds the point.
630+func (c *Core) warpPointer(ctx context.Context, v ports.PointerWarp) error {
631+ if v.ID == 0 || v.ID != c.pointer || c.grab != 0 && c.grab != v.ID {
632+ return nil
633+ }
634+ r, ok := c.shownClientRect(v.ID)
635+ if !ok || v.X < 0 || v.Y < 0 || v.X >= float64(r.W) || v.Y >= float64(r.H) {
636+ return nil
637+ }
638+ c.cursorX, c.cursorY = c.constraint.Clamp(float64(r.X)+v.X, float64(r.Y)+v.Y)
639+ if c.ch.Constraints != nil {
640+ g := c.constraint
641+ g.X, g.Y, g.Warp = c.cursorX, c.cursorY, true
642+ latest(c.ch.Constraints, g)
643+ }
644+ // A locked pointer moved: the client knows where it put it.
645+ if c.constraint.Mode == ports.ConstraintLock {
646+ return nil
647+ }
648+ return c.rehit(ctx)
649+}
650+
615651 // latest replaces any unread value: only the owner sends and drains;
616652 // consumers only receive.
617653 func latest[T any](ch chan T, v T) {
@@ -792,6 +828,10 @@ func (c *Core) Run(ctx context.Context) error {
792828 }
793829 case ports.PointerConstrained:
794830 c.constrained = v
831+ case ports.PointerWarp:
832+ if c.warpPointer(ctx, v) != nil {
833+ return nil
834+ }
795835 case ports.WindowFullscreenRequest:
796836 if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace {
797837 continue
@@ -578,26 +578,16 @@ func (c *Core) updateInhibit(ctx context.Context) error {
578 // and sends it to input when it changed. A hidden window holds nothing.578 // and sends it to input when it changed. A hidden window holds nothing.
579 func (c *Core) resolveConstraint() {579 func (c *Core) resolveConstraint() {
580 var g ports.PointerConstraint580 var g ports.PointerConstraint
581- if id := c.constrained.ID; id != 0 {581+ if r, ok := c.shownClientRect(c.constrained.ID); ok {
582- if s, _ := c.screenOf(id); s != nil {582+ if w := c.constrained.Rect; w.W > 0 && w.H > 0 {
583- for _, p := range s.mon.Layout() {583+ // The region is clipped to the window.
584- if p.ID != id || p.Hidden || p.Preview > 0 {584+ x0, y0 := max(r.X, r.X+w.X), max(r.Y, r.Y+w.Y)
585- continue585+ x1, y1 := min(r.X+r.W, r.X+w.X+w.W), min(r.Y+r.H, r.Y+w.Y+w.H)
586- }586+ if x1 > x0 && y1 > y0 {
587- r := c.clientRect(p)587+ r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
588- r.X += s.x
589- r.Y += s.y
590- if w := c.constrained.Rect; w.W > 0 && w.H > 0 {
591- // The region is clipped to the window.
592- x0, y0 := max(r.X, r.X+w.X), max(r.Y, r.Y+w.Y)
593- x1, y1 := min(r.X+r.W, r.X+w.X+w.W), min(r.Y+r.H, r.Y+w.Y+w.H)
594- if x1 > x0 && y1 > y0 {
595- r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
596- }
597- }
598- g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r}
599 }588 }
600 }589 }
590+ g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r}
601 }591 }
602 // The cursor follows core; input resyncs to it only on a change.592 // The cursor follows core; input resyncs to it only on a change.
603 if g.Mode == c.constraint.Mode && g.Rect == c.constraint.Rect {593 if g.Mode == c.constraint.Mode && g.Rect == c.constraint.Rect {
@@ -612,6 +602,52 @@ func (c *Core) resolveConstraint() {
612 }602 }
613 }603 }
614 604
605+// shownClientRect is the global logical client rectangle of a window
606+// drawn on its output; false when it is hidden or a preview.
607+func (c *Core) shownClientRect(id WindowID) (Rect, bool) {
608+ if id == 0 {
609+ return Rect{}, false
610+ }
611+ s, _ := c.screenOf(id)
612+ if s == nil {
613+ return Rect{}, false
614+ }
615+ for _, p := range s.mon.Layout() {
616+ if p.ID != id || p.Hidden || p.Preview > 0 {
617+ continue
618+ }
619+ r := c.clientRect(p)
620+ r.X += s.x
621+ r.Y += s.y
622+ return r, true
623+ }
624+ return Rect{}, false
625+}
626+
627+// warpPointer moves the cursor to a window-local point of the window under
628+// it (wp_pointer_warp_v1). Input takes the new position, and the window
629+// gets the motion; the active constraint still bounds the point.
630+func (c *Core) warpPointer(ctx context.Context, v ports.PointerWarp) error {
631+ if v.ID == 0 || v.ID != c.pointer || c.grab != 0 && c.grab != v.ID {
632+ return nil
633+ }
634+ r, ok := c.shownClientRect(v.ID)
635+ if !ok || v.X < 0 || v.Y < 0 || v.X >= float64(r.W) || v.Y >= float64(r.H) {
636+ return nil
637+ }
638+ c.cursorX, c.cursorY = c.constraint.Clamp(float64(r.X)+v.X, float64(r.Y)+v.Y)
639+ if c.ch.Constraints != nil {
640+ g := c.constraint
641+ g.X, g.Y, g.Warp = c.cursorX, c.cursorY, true
642+ latest(c.ch.Constraints, g)
643+ }
644+ // A locked pointer moved: the client knows where it put it.
645+ if c.constraint.Mode == ports.ConstraintLock {
646+ return nil
647+ }
648+ return c.rehit(ctx)
649+}
650+
615 // latest replaces any unread value: only the owner sends and drains;651 // latest replaces any unread value: only the owner sends and drains;
616 // consumers only receive.652 // consumers only receive.
617 func latest[T any](ch chan T, v T) {653 func latest[T any](ch chan T, v T) {
@@ -792,6 +828,10 @@ func (c *Core) Run(ctx context.Context) error {
792 }828 }
793 case ports.PointerConstrained:829 case ports.PointerConstrained:
794 c.constrained = v830 c.constrained = v
831+ case ports.PointerWarp:
832+ if c.warpPointer(ctx, v) != nil {
833+ return nil
834+ }
795 case ports.WindowFullscreenRequest:835 case ports.WindowFullscreenRequest:
796 if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace {836 if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace {
797 continue837 continue
M internal/core/core_test.go +52 -0
@@ -583,6 +583,58 @@ func TestOutputScale(t *testing.T) {
583583 }
584584 }
585585
586+// A pointer warp moves the cursor on the window under it: input takes the
587+// position and the window gets the motion. Other windows cannot warp.
588+func TestPointerWarp(t *testing.T) {
589+ cfg := config.Defaults()
590+ cfg.Border.Width = 0
591+ cfg.Layout.Gaps = 0
592+ client := make(chan ports.ClientEvent, 8)
593+ input := make(chan ports.InputEvent, 8)
594+ output := make(chan ports.OutputEvent, 8)
595+ commands := make(chan ports.ClientCommand, 64)
596+ scenes := make(chan []ports.Scene, 1)
597+ constraints := make(chan ports.PointerConstraint, 1)
598+ c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Constraints: constraints})
599+ if err != nil {
600+ t.Fatal(err)
601+ }
602+ ctx, cancel := context.WithCancel(context.Background())
603+ defer cancel()
604+ go c.Run(ctx)
605+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "A", Width: 100, Height: 80}}
606+ scene(t, scenes)
607+ client <- ports.WindowMapped{ID: 1}
608+ r := receive(t, scenes)[0].Windows[0].Rect
609+ input <- ports.PointerMotion{X: 50, Y: 40}
610+ for {
611+ if _, ok := command(t, commands).(ports.PointerMotionTo); ok {
612+ break
613+ }
614+ }
615+ // Another window cannot move the pointer.
616+ client <- ports.PointerWarp{ID: 2, X: 5, Y: 5}
617+ client <- ports.PointerWarp{ID: 1, X: 10, Y: 20}
618+ got := receive(t, constraints)
619+ if !got.Warp || got.X != float64(r.X+10) || got.Y != float64(r.Y+20) {
620+ t.Fatalf("warp %+v, window %+v", got, r)
621+ }
622+ for {
623+ if v, ok := command(t, commands).(ports.PointerMotionTo); ok {
624+ if v.ID != 1 || v.X != 10 || v.Y != 20 || v.DX != 0 || v.DY != 0 {
625+ t.Fatalf("motion %+v", v)
626+ }
627+ break
628+ }
629+ }
630+ // A point outside the window is refused.
631+ client <- ports.PointerWarp{ID: 1, X: float64(r.W), Y: 0}
632+ client <- ports.PointerWarp{ID: 1, X: 1, Y: 1}
633+ if got := receive(t, constraints); got.X != float64(r.X+1) || got.Y != float64(r.Y+1) {
634+ t.Fatalf("warp %+v", got)
635+ }
636+}
637+
586638 func TestPointerConstraint(t *testing.T) {
587639 cfg := config.Defaults()
588640 cfg.Border.Width = 0
@@ -583,6 +583,58 @@ func TestOutputScale(t *testing.T) {
583 }583 }
584 }584 }
585 585
586+// A pointer warp moves the cursor on the window under it: input takes the
587+// position and the window gets the motion. Other windows cannot warp.
588+func TestPointerWarp(t *testing.T) {
589+ cfg := config.Defaults()
590+ cfg.Border.Width = 0
591+ cfg.Layout.Gaps = 0
592+ client := make(chan ports.ClientEvent, 8)
593+ input := make(chan ports.InputEvent, 8)
594+ output := make(chan ports.OutputEvent, 8)
595+ commands := make(chan ports.ClientCommand, 64)
596+ scenes := make(chan []ports.Scene, 1)
597+ constraints := make(chan ports.PointerConstraint, 1)
598+ c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Constraints: constraints})
599+ if err != nil {
600+ t.Fatal(err)
601+ }
602+ ctx, cancel := context.WithCancel(context.Background())
603+ defer cancel()
604+ go c.Run(ctx)
605+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "A", Width: 100, Height: 80}}
606+ scene(t, scenes)
607+ client <- ports.WindowMapped{ID: 1}
608+ r := receive(t, scenes)[0].Windows[0].Rect
609+ input <- ports.PointerMotion{X: 50, Y: 40}
610+ for {
611+ if _, ok := command(t, commands).(ports.PointerMotionTo); ok {
612+ break
613+ }
614+ }
615+ // Another window cannot move the pointer.
616+ client <- ports.PointerWarp{ID: 2, X: 5, Y: 5}
617+ client <- ports.PointerWarp{ID: 1, X: 10, Y: 20}
618+ got := receive(t, constraints)
619+ if !got.Warp || got.X != float64(r.X+10) || got.Y != float64(r.Y+20) {
620+ t.Fatalf("warp %+v, window %+v", got, r)
621+ }
622+ for {
623+ if v, ok := command(t, commands).(ports.PointerMotionTo); ok {
624+ if v.ID != 1 || v.X != 10 || v.Y != 20 || v.DX != 0 || v.DY != 0 {
625+ t.Fatalf("motion %+v", v)
626+ }
627+ break
628+ }
629+ }
630+ // A point outside the window is refused.
631+ client <- ports.PointerWarp{ID: 1, X: float64(r.W), Y: 0}
632+ client <- ports.PointerWarp{ID: 1, X: 1, Y: 1}
633+ if got := receive(t, constraints); got.X != float64(r.X+1) || got.Y != float64(r.Y+1) {
634+ t.Fatalf("warp %+v", got)
635+ }
636+}
637+
586 func TestPointerConstraint(t *testing.T) {638 func TestPointerConstraint(t *testing.T) {
587 cfg := config.Defaults()639 cfg := config.Defaults()
588 cfg.Border.Width = 0640 cfg.Border.Width = 0
A internal/ports/lease_test.go +28 -0
@@ -0,0 +1,28 @@
1+package ports
2+
3+import (
4+ "errors"
5+ "os"
6+ "testing"
7+)
8+
9+// A dropped lease message closes the fd it owns; others own none.
10+func TestCloseLeaseFiles(t *testing.T) {
11+ open := func() *os.File {
12+ f, err := os.Open(os.DevNull)
13+ if err != nil {
14+ t.Fatal(err)
15+ }
16+ return f
17+ }
18+ device, lease := open(), open()
19+ CloseLeaseFiles(LeaseConnectors{Device: device})
20+ CloseLeaseFiles(LeaseReply{FD: lease})
21+ CloseLeaseFiles(LeaseConnectors{})
22+ CloseLeaseFiles(LeaseRevoke{})
23+ for _, f := range []*os.File{device, lease} {
24+ if err := f.Close(); !errors.Is(err, os.ErrClosed) {
25+ t.Fatalf("%v not closed: %v", f.Name(), err)
26+ }
27+ }
28+}
@@ -0,0 +1,28 @@
1+package ports
2+
3+import (
4+ "errors"
5+ "os"
6+ "testing"
7+)
8+
9+// A dropped lease message closes the fd it owns; others own none.
10+func TestCloseLeaseFiles(t *testing.T) {
11+ open := func() *os.File {
12+ f, err := os.Open(os.DevNull)
13+ if err != nil {
14+ t.Fatal(err)
15+ }
16+ return f
17+ }
18+ device, lease := open(), open()
19+ CloseLeaseFiles(LeaseConnectors{Device: device})
20+ CloseLeaseFiles(LeaseReply{FD: lease})
21+ CloseLeaseFiles(LeaseConnectors{})
22+ CloseLeaseFiles(LeaseRevoke{})
23+ for _, f := range []*os.File{device, lease} {
24+ if err := f.Close(); !errors.Is(err, os.ErrClosed) {
25+ t.Fatalf("%v not closed: %v", f.Name(), err)
26+ }
27+ }
28+}
M internal/ports/messages.go +85 -2
@@ -255,11 +255,13 @@ const (
255255 // PointerConstraint is an active pointer constraint. Rect is logical: in
256256 // PointerConstrained it is window-local and empty means the whole window;
257257 // core sends input the resolved global rectangle, and in X, Y its cursor
258-// position, where input holds a locked pointer.
258+// position, where input holds a locked pointer. Warp moves input's
259+// pointer to X, Y whatever the mode (wp_pointer_warp_v1).
259260 type PointerConstraint struct {
260261 Mode ConstraintMode
261262 Rect Rect
262263 X, Y float64
264+ Warp bool
263265 }
264266
265267 // Clamp keeps a global point inside the rectangle of a lock or confine;
@@ -284,6 +286,16 @@ type PointerConstrained struct {
284286
285287 func (PointerConstrained) clientEvent() {}
286288
289+// PointerWarp carries wayland → core a client's request to move the
290+// pointer to X, Y, window-local logical (wp_pointer_warp_v1). Wayland has
291+// checked that the window has the pointer and the point is on it.
292+type PointerWarp struct {
293+ ID WindowID
294+ X, Y float64
295+}
296+
297+func (PointerWarp) clientEvent() {}
298+
287299 // Mods carries input → core modifier flags.
288300 type Mods uint8
289301
@@ -879,8 +891,11 @@ type SurfaceContent struct {
879891 // buffer: readers apply its inverse. Width and Height stay the buffer's.
880892 Transform BufferTransform
881893 // Opaque ignores the alpha byte: an x format, or an opaque region that
882- // covers the whole surface.
894+ // covers the whole surface. It is false while Fade is set.
883895 Opaque bool
896+ // Fade is how much wp_alpha_modifier_v1 fades the surface: 0 none,
897+ // 1 invisible. Its opacity is multiplied by 1-Fade.
898+ Fade float32
884899 Color SurfaceColor
885900 SHM *SHMBuffer
886901 DMABuf *DMABuf
@@ -1086,3 +1101,71 @@ type WindowState struct {
10861101 // Hidden is set for a stashed window while its stash is hidden.
10871102 Hidden bool
10881103 }
1104+
1105+// LeaseConnector identifies a non-desktop connector available for DRM leasing.
1106+type LeaseConnector struct {
1107+ Card, Name, Description string
1108+ ConnectorID uint32
1109+}
1110+
1111+// LeaseMessage is an adapter-to-adapter DRM lease channel message.
1112+type LeaseMessage interface{ leaseMessage() }
1113+
1114+// LeaseConnectors replaces the available connector inventory for Card.
1115+// Device is a non-master DRM fd owned by the receiver, which closes it.
1116+// An empty Connectors list with nil Device removes the card's global and closes
1117+// the display's fd. A later nonempty inventory creates a new global.
1118+type LeaseConnectors struct {
1119+ Card string
1120+ Device *os.File
1121+ Connectors []LeaseConnector
1122+}
1123+
1124+func (LeaseConnectors) leaseMessage() {}
1125+
1126+// CloseLeaseFiles closes the fd a lease message owns, for a message that is
1127+// dropped instead of delivered.
1128+func CloseLeaseFiles(msg LeaseMessage) {
1129+ switch m := msg.(type) {
1130+ case LeaseConnectors:
1131+ if m.Device != nil {
1132+ m.Device.Close()
1133+ }
1134+ case LeaseReply:
1135+ if m.FD != nil {
1136+ m.FD.Close()
1137+ }
1138+ }
1139+}
1140+
1141+type LeaseRequest struct {
1142+ ID uint64
1143+ Card string
1144+ Connectors []string
1145+}
1146+
1147+func (LeaseRequest) leaseMessage() {}
1148+
1149+// LeaseReply transfers ownership of FD to Wayland.
1150+type LeaseReply struct {
1151+ ID uint64
1152+ FD *os.File
1153+ LeaseID uint32
1154+ Err error
1155+}
1156+
1157+func (LeaseReply) leaseMessage() {}
1158+
1159+type LeaseRevoke struct {
1160+ Card string
1161+ LeaseID uint32
1162+}
1163+
1164+func (LeaseRevoke) leaseMessage() {}
1165+
1166+type LeaseFinished struct {
1167+ Card string
1168+ LeaseID uint32
1169+}
1170+
1171+func (LeaseFinished) leaseMessage() {}
@@ -255,11 +255,13 @@ const (
255 // PointerConstraint is an active pointer constraint. Rect is logical: in255 // PointerConstraint is an active pointer constraint. Rect is logical: in
256 // PointerConstrained it is window-local and empty means the whole window;256 // PointerConstrained it is window-local and empty means the whole window;
257 // core sends input the resolved global rectangle, and in X, Y its cursor257 // core sends input the resolved global rectangle, and in X, Y its cursor
258-// position, where input holds a locked pointer.258+// position, where input holds a locked pointer. Warp moves input's
259+// pointer to X, Y whatever the mode (wp_pointer_warp_v1).
259 type PointerConstraint struct {260 type PointerConstraint struct {
260 Mode ConstraintMode261 Mode ConstraintMode
261 Rect Rect262 Rect Rect
262 X, Y float64263 X, Y float64
264+ Warp bool
263 }265 }
264 266
265 // Clamp keeps a global point inside the rectangle of a lock or confine;267 // Clamp keeps a global point inside the rectangle of a lock or confine;
@@ -284,6 +286,16 @@ type PointerConstrained struct {
284 286
285 func (PointerConstrained) clientEvent() {}287 func (PointerConstrained) clientEvent() {}
286 288
289+// PointerWarp carries wayland → core a client's request to move the
290+// pointer to X, Y, window-local logical (wp_pointer_warp_v1). Wayland has
291+// checked that the window has the pointer and the point is on it.
292+type PointerWarp struct {
293+ ID WindowID
294+ X, Y float64
295+}
296+
297+func (PointerWarp) clientEvent() {}
298+
287 // Mods carries input → core modifier flags.299 // Mods carries input → core modifier flags.
288 type Mods uint8300 type Mods uint8
289 301
@@ -879,8 +891,11 @@ type SurfaceContent struct {
879 // buffer: readers apply its inverse. Width and Height stay the buffer's.891 // buffer: readers apply its inverse. Width and Height stay the buffer's.
880 Transform BufferTransform892 Transform BufferTransform
881 // Opaque ignores the alpha byte: an x format, or an opaque region that893 // Opaque ignores the alpha byte: an x format, or an opaque region that
882- // covers the whole surface.894+ // covers the whole surface. It is false while Fade is set.
883 Opaque bool895 Opaque bool
896+ // Fade is how much wp_alpha_modifier_v1 fades the surface: 0 none,
897+ // 1 invisible. Its opacity is multiplied by 1-Fade.
898+ Fade float32
884 Color SurfaceColor899 Color SurfaceColor
885 SHM *SHMBuffer900 SHM *SHMBuffer
886 DMABuf *DMABuf901 DMABuf *DMABuf
@@ -1086,3 +1101,71 @@ type WindowState struct {
1086 // Hidden is set for a stashed window while its stash is hidden.1101 // Hidden is set for a stashed window while its stash is hidden.
1087 Hidden bool1102 Hidden bool
1088 }1103 }
1104+
1105+// LeaseConnector identifies a non-desktop connector available for DRM leasing.
1106+type LeaseConnector struct {
1107+ Card, Name, Description string
1108+ ConnectorID uint32
1109+}
1110+
1111+// LeaseMessage is an adapter-to-adapter DRM lease channel message.
1112+type LeaseMessage interface{ leaseMessage() }
1113+
1114+// LeaseConnectors replaces the available connector inventory for Card.
1115+// Device is a non-master DRM fd owned by the receiver, which closes it.
1116+// An empty Connectors list with nil Device removes the card's global and closes
1117+// the display's fd. A later nonempty inventory creates a new global.
1118+type LeaseConnectors struct {
1119+ Card string
1120+ Device *os.File
1121+ Connectors []LeaseConnector
1122+}
1123+
1124+func (LeaseConnectors) leaseMessage() {}
1125+
1126+// CloseLeaseFiles closes the fd a lease message owns, for a message that is
1127+// dropped instead of delivered.
1128+func CloseLeaseFiles(msg LeaseMessage) {
1129+ switch m := msg.(type) {
1130+ case LeaseConnectors:
1131+ if m.Device != nil {
1132+ m.Device.Close()
1133+ }
1134+ case LeaseReply:
1135+ if m.FD != nil {
1136+ m.FD.Close()
1137+ }
1138+ }
1139+}
1140+
1141+type LeaseRequest struct {
1142+ ID uint64
1143+ Card string
1144+ Connectors []string
1145+}
1146+
1147+func (LeaseRequest) leaseMessage() {}
1148+
1149+// LeaseReply transfers ownership of FD to Wayland.
1150+type LeaseReply struct {
1151+ ID uint64
1152+ FD *os.File
1153+ LeaseID uint32
1154+ Err error
1155+}
1156+
1157+func (LeaseReply) leaseMessage() {}
1158+
1159+type LeaseRevoke struct {
1160+ Card string
1161+ LeaseID uint32
1162+}
1163+
1164+func (LeaseRevoke) leaseMessage() {}
1165+
1166+type LeaseFinished struct {
1167+ Card string
1168+ LeaseID uint32
1169+}
1170+
1171+func (LeaseFinished) leaseMessage() {}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close