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 Unverified
B5690EEEBB952194The 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 | |||
| 63 | 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 | 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 | 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 | 68 | ## Hardware support | |
| 68 | 69 | ||
| @@ -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 support | 68 | ## Hardware support | ||
| 68 | 69 | ||||
M go.mod +1 -1
| @@ -4,7 +4,7 @@ go 1.27 | |||
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | 6 | 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 | |
| 8 | 8 | github.com/bnema/purego-vulkan v0.6.0 | |
| 9 | 9 | github.com/bnema/wlturbo v0.2.0 | |
| 10 | 10 | 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.1 | 6 | 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 |
| 8 | github.com/bnema/purego-vulkan v0.6.0 | 8 | github.com/bnema/purego-vulkan v0.6.0 | ||
| 9 | github.com/bnema/wlturbo v0.2.0 | 9 | github.com/bnema/wlturbo v0.2.0 | ||
| 10 | github.com/bnema/zerowrap v1.4.1 | 10 | 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 | |||
| 3 | 3 | import ( | |
| 4 | 4 | "encoding/binary" | |
| 5 | 5 | "errors" | |
| 6 | + | "os" | |
| 6 | 7 | "runtime" | |
| 7 | 8 | "slices" | |
| 8 | 9 | "sync" | |
| @@ -85,6 +86,11 @@ type kms interface { | |||
| 85 | 86 | // addFB imports a single-plane dmabuf as a framebuffer of format. | |
| 86 | 87 | addFB(b *ports.DMABuf, format uint32) (uint32, error) | |
| 87 | 88 | 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 | 96 | // 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) error | 88 | 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 { | |||
| 24 | 24 | log zerowrap.Logger | |
| 25 | 25 | crtcs []uint32 | |
| 26 | 26 | // 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 | |
| 30 | 33 | // taken are planes driven by an output, by plane ID. | |
| 31 | 34 | taken map[uint32]bool | |
| 32 | 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 | 56 | flips[c] = make(chan flipEvent, 4) | |
| 54 | 57 | } | |
| 55 | 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)}, 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 | |
| 57 | 60 | } | |
| 58 | 61 | ||
| 59 | 62 | // Path is the device path, e.g. /dev/dri/card1. | |
| @@ -84,17 +87,22 @@ func (c *Card) SetWant(w Want) { | |||
| 84 | 87 | // names to stop because they need a new mode. A stopped output must be | |
| 85 | 88 | // closed by its goroutine before its CRTC is reused: call Release. | |
| 86 | 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 | 91 | if err != nil { | |
| 89 | 92 | return nil, nil, nil, err | |
| 90 | 93 | } | |
| 91 | 94 | seen := map[string]bool{} | |
| 95 | + | available := map[string]connector{} | |
| 92 | 96 | for _, id := range ids { | |
| 93 | - | conn, err := readConnector(c.fd, id) | |
| 97 | + | conn, err := c.k.connector(id) | |
| 94 | 98 | if err != nil { | |
| 95 | 99 | c.log.Warn().Err(err).Uint32("connector", id).Msg("read connector") | |
| 96 | 100 | continue | |
| 97 | 101 | } | |
| 102 | + | if conn.nonDesktop && conn.connected { | |
| 103 | + | available[conn.name] = conn | |
| 104 | + | continue | |
| 105 | + | } | |
| 98 | 106 | if !c.want.usable(conn) { | |
| 99 | 107 | continue | |
| 100 | 108 | } | |
| @@ -115,6 +123,19 @@ func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) { | |||
| 115 | 123 | c.outputs[conn.name] = o | |
| 116 | 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 | 139 | for name := range c.outputs { | |
| 119 | 140 | if !seen[name] { | |
| 120 | 141 | removed = append(removed, name) | |
| @@ -128,18 +149,18 @@ func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) { | |||
| 128 | 149 | // ConnectedHeads inventories physical connectors independently of the desired | |
| 129 | 150 | // enabled state. The caller owns Scan and must invoke this on the same goroutine. | |
| 130 | 151 | func (c *Card) ConnectedHeads() ([]ports.OutputHead, error) { | |
| 131 | - | _, ids, err := resources(c.fd) | |
| 152 | + | _, ids, err := c.k.resources() | |
| 132 | 153 | if err != nil { | |
| 133 | 154 | return nil, err | |
| 134 | 155 | } | |
| 135 | 156 | var heads []ports.OutputHead | |
| 136 | 157 | for _, id := range ids { | |
| 137 | - | conn, err := readConnector(c.fd, id) | |
| 158 | + | conn, err := c.k.connector(id) | |
| 138 | 159 | if err != nil { | |
| 139 | 160 | c.log.Warn().Err(err).Uint32("connector", id).Msg("read connector for inventory") | |
| 140 | 161 | continue | |
| 141 | 162 | } | |
| 142 | - | if !conn.connected || len(conn.modes) == 0 { | |
| 163 | + | if !conn.connected || conn.nonDesktop || len(conn.modes) == 0 { | |
| 143 | 164 | continue | |
| 144 | 165 | } | |
| 145 | 166 | monitor := readMonitor(c.path, conn.name) | |
| @@ -195,7 +216,12 @@ func (c *Card) open(conn connector, mode modeInfo) (*Output, error) { | |||
| 195 | 216 | for _, o := range c.outputs { | |
| 196 | 217 | 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 | 225 | if err != nil { | |
| 200 | 226 | return nil, err | |
| 201 | 227 | } | |
| @@ -24,9 +24,12 @@ type Card struct { | |||||
| 24 | log zerowrap.Logger | 24 | log zerowrap.Logger | ||
| 25 | crtcs []uint32 | 25 | 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]*Output | 27 | + | outputs map[string]*Output |
| 28 | - | flips map[uint32]chan flipEvent // by CRTC; set before the reader starts | 28 | + | leasable map[string]connector |
| 29 | - | k kmsDevice | 29 | + | 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]bool | 34 | 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)}, 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 |
| 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 be | 87 | // 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, err | 92 | 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 | continue | 100 | 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 | continue | 107 | continue | ||
| 100 | } | 108 | } | ||
| @@ -115,6 +123,19 @@ func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) { | |||||
| 115 | c.outputs[conn.name] = o | 123 | 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 desired | 149 | // 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, err | 154 | return nil, err | ||
| 134 | } | 155 | } | ||
| 135 | var heads []ports.OutputHead | 156 | 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 | continue | 161 | continue | ||
| 141 | } | 162 | } | ||
| 142 | - | if !conn.connected || len(conn.modes) == 0 { | 163 | + | if !conn.connected || conn.nonDesktop || len(conn.modes) == 0 { |
| 143 | continue | 164 | 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] = true | 217 | 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, err | 226 | return nil, err | ||
| 201 | } | 227 | } | ||
M internal/adapters/drm/kms.go +8 -2
| @@ -82,6 +82,7 @@ type connector struct { | |||
| 82 | 82 | id, encoderID uint32 | |
| 83 | 83 | name string | |
| 84 | 84 | connected bool | |
| 85 | + | nonDesktop bool | |
| 85 | 86 | mmW, mmH int | |
| 86 | 87 | modes []modeInfo | |
| 87 | 88 | encoders []uint32 | |
| @@ -117,7 +118,7 @@ type HDRSettings struct { | |||
| 117 | 118 | ||
| 118 | 119 | // usable reports whether a connector should be driven. | |
| 119 | 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 | 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 | 217 | if name == "" { | |
| 217 | 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 | 224 | return connector{ | |
| 220 | 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 | 228 | modes: modes[:min(int(g2.countModes), len(modes))], | |
| 223 | 229 | encoders: encs[:min(int(g2.countEncoders), len(encs))], | |
| 224 | 230 | }, nil | |
| @@ -82,6 +82,7 @@ type connector struct { | |||||
| 82 | id, encoderID uint32 | 82 | id, encoderID uint32 | ||
| 83 | name string | 83 | name string | ||
| 84 | connected bool | 84 | connected bool | ||
| 85 | + | nonDesktop bool | |||
| 85 | mmW, mmH int | 86 | mmW, mmH int | ||
| 86 | modes []modeInfo | 87 | modes []modeInfo | ||
| 87 | encoders []uint32 | 88 | 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 | }, nil | 230 | }, nil | ||
M internal/adapters/drm/kms_mock_test.go +316 -0
| @@ -5,6 +5,8 @@ | |||
| 5 | 5 | package drm | |
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | + | "os" | |
| 9 | + | ||
| 8 | 10 | "github.com/bnema/neferwl/internal/ports" | |
| 9 | 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 | 176 | 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 | 239 | // createBlob provides a mock function for the type mockkms | |
| 178 | 240 | func (_mock *mockkms) createBlob(data []byte) (uint32, error) { | |
| 179 | 241 | ret := _mock.Called(data) | |
| @@ -234,6 +296,74 @@ func (_c *mockkms_createBlob_Call) RunAndReturn(run func(data []byte) (uint32, e | |||
| 234 | 296 | 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 | 367 | // destroyBlob provides a mock function for the type mockkms | |
| 238 | 368 | func (_mock *mockkms) destroyBlob(id uint32) error { | |
| 239 | 369 | ret := _mock.Called(id) | |
| @@ -415,6 +545,78 @@ func (_c *mockkms_objProps_Call) RunAndReturn(run func(obj uint32, typ uint32) ( | |||
| 415 | 545 | 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 | 620 | // planes provides a mock function for the type mockkms | |
| 419 | 621 | func (_mock *mockkms) planes() ([]planeRes, error) { | |
| 420 | 622 | ret := _mock.Called() | |
| @@ -470,6 +672,120 @@ func (_c *mockkms_planes_Call) RunAndReturn(run func() ([]planeRes, error)) *moc | |||
| 470 | 672 | 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 | 789 | // rmFB provides a mock function for the type mockkms | |
| 474 | 790 | func (_mock *mockkms) rmFB(id uint32) error { | |
| 475 | 791 | ret := _mock.Called(id) | |
| @@ -5,6 +5,8 @@ | |||||
| 5 | package drm | 5 | 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 _c | 176 | 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 mockkms | 239 | // 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 _c | 296 | 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 mockkms | 367 | // 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 _c | 545 | 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 mockkms | 620 | // 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 _c | 672 | 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 mockkms | 789 | // 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) { | |||
| 68 | 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) { | |
| 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") | |
| 73 | 75 | } | |
| 74 | 76 | } | |
| 75 | 77 | ||
| @@ -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 | |||
| 120 | 120 | switch { | |
| 121 | 121 | case c.DMABuf == nil: | |
| 122 | 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 | 126 | case len(c.Children) > 0: | |
| 124 | 127 | return c, "subsurfaces" | |
| 125 | 128 | 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) { | |||
| 26 | 26 | }, nil, ""}, | |
| 27 | 27 | {"no window", func(s *ports.Scene) { s.Windows = nil }, nil, "no_fullscreen"}, | |
| 28 | 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 | 30 | {"tiled", func(s *ports.Scene) { s.Windows[0].Fullscreen = false }, nil, "other_windows"}, | |
| 30 | 31 | {"float above", func(s *ports.Scene) { | |
| 31 | 32 | 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) { | |||
| 605 | 605 | ||
| 606 | 606 | // constrain applies a new constraint. Entering or leaving a lock resyncs | |
| 607 | 607 | // 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 | 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 | 611 | p.x, p.y = p.layout.Clamp(c.X, c.Y, c.X, c.Y) | |
| 612 | 612 | } | |
| 613 | 613 | 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 resyncs | 606 | // constrain applies a new constraint. Entering or leaving a lock resyncs | ||
| 607 | // the pointer to core's cursor, which stood still meanwhile; a confined | 607 | // 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 = c | 613 | p.constraint = c | ||
M internal/adapters/libinput/libinput_test.go +5 -0
| @@ -169,4 +169,9 @@ func TestPointerConstraints(t *testing.T) { | |||
| 169 | 169 | if p.x != 21 || p.y != 31 { | |
| 170 | 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 | } | |
| @@ -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 | |||
| 360 | 360 | flags |= flagSmooth | |
| 361 | 361 | } | |
| 362 | 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 | 365 | return dr | |
| 364 | 366 | } | |
| 365 | 367 | ||
| @@ -360,6 +360,8 @@ func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, source [4]f | |||||
| 360 | flags |= flagSmooth | 360 | 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 dr | 365 | return dr | ||
| 364 | } | 366 | } | ||
| 365 | 367 | ||||
M internal/adapters/vulkan/renderer_test.go +19 -0
| @@ -92,6 +92,25 @@ func TestRendererRender(t *testing.T) { | |||
| 92 | 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 | 114 | func TestRendererContents(t *testing.T) { | |
| 96 | 115 | r, err := New(64, 48) | |
| 97 | 116 | 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 | |||
| 288 | 288 | return | |
| 289 | 289 | } | |
| 290 | 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 | 292 | if im.yuv { | |
| 293 | 293 | dr.pc.misc[1] |= flagYUV | |
| 294 | 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 | 324 | return | |
| 325 | 325 | } | |
| 326 | 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 | 328 | dr.set = c.set | |
| 329 | 329 | dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0} | |
| 330 | 330 | w.draws = append(w.draws, dr) | |
| 331 | 331 | } | |
| 332 | 332 | ||
| 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 | |
| 335 | 338 | if c.IsPQ2020() { | |
| 336 | 339 | dr.pc.misc[1] |= flagPQ | |
| 337 | 340 | } else if c.IsExtendedLinear() { | |
| @@ -288,7 +288,7 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon | |||||
| 288 | return | 288 | 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] |= flagYUV | 293 | 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 | return | 324 | 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.set | 328 | 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] |= flagPQ | 339 | 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() { | |||
| 132 | 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 | 139 | color = c; | |
| 136 | 140 | } | |
| @@ -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() { | |||
| 130 | 130 | (d.misc.y & flagExtendedLinear) != 0u ? c.rgb / c.a * (80.0 / d.color.x) : decodeSRGB(c.rgb / c.a); | |
| 131 | 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 | 137 | color = c; | |
| 134 | 138 | } | |
| @@ -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 { | |||
| 60 | 60 | return d | |
| 61 | 61 | } | |
| 62 | 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 | 64 | r, err := cm.NewWpColorManagerV1(c, int32(v), id, colorManager{s}) | |
| 65 | 65 | if err != nil { | |
| 66 | 66 | return | |
| 67 | 67 | } | |
| 68 | 68 | r.SendSupportedIntent(uint32(cm.WpColorManagerV1RenderIntentPerceptual)) | |
| 69 | 69 | r.SendSupportedFeature(uint32(cm.WpColorManagerV1FeatureParametric)) | |
| 70 | + | if v >= 3 { | |
| 71 | + | r.SendSupportedFeature(uint32(cm.WpColorManagerV1FeatureWindowsBt2100)) | |
| 72 | + | } | |
| 70 | 73 | for _, tf := range []uint32{pq, extLinear, uint32(cm.WpColorManagerV1TransferFunctionSrgb)} { | |
| 71 | 74 | r.SendSupportedTfNamed(tf) | |
| 72 | 75 | } | |
| @@ -138,8 +141,15 @@ func (m colorManager) CreateParametricCreator(r *cm.WpColorManagerV1, id uint32) | |||
| 138 | 141 | func (colorManager) CreateWindowsScrgb(r *cm.WpColorManagerV1, _ uint32) { | |
| 139 | 142 | r.PostError(uint32(cm.WpColorManagerV1ErrorUnsupportedFeature), "scRGB unsupported") | |
| 140 | 143 | } | |
| 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) | |
| 143 | 153 | } | |
| 144 | 154 | func (colorManager) GetImageDescription(r *cm.WpColorManagerV1, _ uint32, _ *cm.WpImageDescriptionReferenceV1) { | |
| 145 | 155 | r.PostError(uint32(cm.WpColorManagerV1ErrorUnsupportedFeature), "references unsupported") | |
| @@ -60,13 +60,16 @@ func (s *Server) outputColor(o *output) colorDescription { | |||||
| 60 | return d | 60 | 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 | return | 66 | 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) { | |||
| 135 | 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 | 190 | func (s *Server) surfaceColor(id uint32) SurfaceColor { | |
| 139 | 191 | for r, v := range s.surfaces { | |
| 140 | 192 | 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 | |||
| 3 | 3 | import ( | |
| 4 | 4 | "github.com/bnema/neferwl/internal/ports" | |
| 5 | 5 | "github.com/bnema/purego-libwayland/protocol/pointerconstraints" | |
| 6 | + | "github.com/bnema/purego-libwayland/protocol/pointerwarp" | |
| 6 | 7 | "github.com/bnema/purego-libwayland/protocol/relativepointer" | |
| 7 | 8 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 8 | 9 | "github.com/bnema/purego-libwayland/server" | |
| @@ -14,6 +15,11 @@ func registerPointerConstraints(d *server.Display, s *Server) error { | |||
| 14 | 15 | }); err != nil { | |
| 15 | 16 | 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 | 23 | return pointerconstraints.NewZwpPointerConstraintsV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 18 | 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 | 70 | type relativeHandler struct{} | |
| 42 | 71 | ||
| 43 | 72 | 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 err | 16 | 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 ( | |||
| 6 | 6 | ||
| 7 | 7 | "github.com/bnema/neferwl/internal/ports" | |
| 8 | 8 | "github.com/bnema/purego-libwayland/protocol/pointerconstraints" | |
| 9 | + | "github.com/bnema/purego-libwayland/protocol/pointerwarp" | |
| 9 | 10 | "github.com/bnema/purego-libwayland/protocol/relativepointer" | |
| 10 | 11 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 11 | 12 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| @@ -15,18 +16,22 @@ import ( | |||
| 15 | 16 | // eventLog records the opcodes (and first fixed args) a proxy receives. | |
| 16 | 17 | type eventLog struct { | |
| 17 | 18 | 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 | |
| 19 | 21 | } | |
| 20 | 22 | ||
| 21 | 23 | func (p *eventLog) Dispatch(e *wlturbo.Event) { | |
| 22 | 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 | 26 | e.Uint32() | |
| 25 | 27 | e.Uint32() | |
| 26 | 28 | for range 4 { | |
| 27 | 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 | 35 | 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 | 123 | func TestPointerLockAndRelativeMotion(t *testing.T) { | |
| 69 | 124 | s, events, commands, dir := lifecycleServer(t) | |
| 70 | 125 | 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.BaseProxy | 18 | 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 | |||
| 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 <- ev | 35 | 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 { | |||
| 294 | 294 | func (s *surface) takePending() update { | |
| 295 | 295 | u := update{pendingCommit: s.next} | |
| 296 | 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 | 298 | if v := s.viewport; v != nil { | |
| 299 | 299 | 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 | } | |
| @@ -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.pendingCrop | 299 | 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 { | |||
| 67 | 67 | }) | |
| 68 | 68 | }, | |
| 69 | 69 | func() error { return registerClipboard(d, s) }, | |
| 70 | + | func() error { return registerFixes(d) }, | |
| 71 | + | func() error { return registerAlphaModifier(d, s) }, | |
| 70 | 72 | } { | |
| 71 | 73 | if err := register(); err != nil { | |
| 72 | 74 | return err | |
| @@ -75,6 +77,32 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||
| 75 | 77 | 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 | 106 | type compositor struct{ server *Server } | |
| 79 | 107 | ||
| 80 | 108 | func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | |
| @@ -386,6 +414,7 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) { | |||
| 386 | 414 | s := h.server | |
| 387 | 415 | s.seat.pointers[r.Client()] = append(s.seat.pointers[r.Client()], p) | |
| 388 | 416 | p.OnDestroy = func() { | |
| 417 | + | delete(s.seat.enters, p.Resource) | |
| 389 | 418 | list := s.seat.pointers[r.Client()] | |
| 390 | 419 | for i, item := range list { | |
| 391 | 420 | if item == p { | |
| @@ -402,6 +431,7 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) { | |||
| 402 | 431 | if s.hasPointerFocus(r.Client()) { | |
| 403 | 432 | if surf, _, x, y := s.pointerSurface(s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY); surf != nil { | |
| 404 | 433 | s.serial++ | |
| 434 | + | s.seat.enters[p.Resource] = s.serial | |
| 405 | 435 | p.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y)) | |
| 406 | 436 | pointerFrame(p) | |
| 407 | 437 | } | |
| @@ -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 err | 74 | return err | ||
| @@ -75,6 +77,32 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||||
| 75 | return nil | 77 | 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.server | 414 | 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) { | |||
| 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 | 109 | func TestOutputHotplugAndLayerOutput(t *testing.T) { | |
| 74 | 110 | s, _, commands, dir := lifecycleServer(t) | |
| 75 | 111 | 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 { | |||
| 20 | 20 | // pointerX, pointerY is the pointer as core placed it in pointerFocus; | |
| 21 | 21 | // pointerSurface and surfacePoint turn it into surface coordinates. | |
| 22 | 22 | 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 | |
| 36 | 39 | // press is the serial of the last button or key press, sent to | |
| 37 | 40 | // pressClient: popup grabs must come from it. | |
| 38 | 41 | press uint32 | |
| @@ -433,10 +436,12 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | |||
| 433 | 436 | } | |
| 434 | 437 | } | |
| 435 | 438 | s.seat.pointerFocus = 0 | |
| 439 | + | clear(s.seat.enters) | |
| 436 | 440 | if surface, pointers, sx, sy := s.pointerSurface(id, x, y); surface != nil { | |
| 437 | 441 | s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY = id, x, y | |
| 438 | 442 | for _, p := range pointers { | |
| 439 | 443 | s.serial++ | |
| 444 | + | s.seat.enters[p.Resource] = s.serial | |
| 440 | 445 | p.SendEnter(s.serial, surface, server.FixedFromFloat(sx), server.FixedFromFloat(sy)) | |
| 441 | 446 | pointerFrame(p) | |
| 442 | 447 | } | |
| @@ -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 float64 | 22 | pointerX, pointerY float64 | ||
| 23 | - | wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis | 23 | + | // 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 clients | 24 | + | // for pointerFocus; a pointer warp must name its pointer's serial. |
| 25 | - | pointers map[server.Client][]*wayland.Pointer | 25 | + | enters map[*server.Resource]uint32 |
| 26 | - | keyboards map[server.Client][]*wayland.Keyboard | 26 | + | wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis |
| 27 | - | modState ports.ModState | 27 | + | wheelHeld [2]float64 // axis value held back with it for pre-v8 clients |
| 28 | - | heldKeys map[uint32]bool | 28 | + | pointers map[server.Client][]*wayland.Pointer |
| 29 | - | grabKeys map[uint32]bool // pressed through the input method grab | 29 | + | keyboards map[server.Client][]*wayland.Keyboard |
| 30 | - | keymapFD int | 30 | + | modState ports.ModState |
| 31 | - | keymapSize uint32 | 31 | + | heldKeys map[uint32]bool |
| 32 | - | keymapText string // the seat keymap, to compare virtual keymaps with | 32 | + | grabKeys map[uint32]bool // pressed through the input method grab |
| 33 | - | keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap | 33 | + | keymapFD int |
| 34 | - | repeatRate int | 34 | + | keymapSize uint32 |
| 35 | - | repeatDelay int | 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 | |||
| 36 | // press is the serial of the last button or key press, sent to | 39 | // 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 uint32 | 41 | press uint32 | ||
| @@ -433,10 +436,12 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | |||||
| 433 | } | 436 | } | ||
| 434 | } | 437 | } | ||
| 435 | s.seat.pointerFocus = 0 | 438 | 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, y | 441 | 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 { | |||
| 39 | 39 | ||
| 40 | 40 | // Channels carries client notifications and commands. Events may be unbuffered. | |
| 41 | 41 | 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 | |
| 46 | 48 | // Cursors receives the cursor the client under the pointer asks for. | |
| 47 | 49 | Cursors chan<- ports.CursorChange | |
| 48 | 50 | // Presented paces frame callbacks on the outputs' page flips; outputs | |
| @@ -58,17 +60,25 @@ type Channels struct { | |||
| 58 | 60 | OutputApplied <-chan ports.OutputApplied | |
| 59 | 61 | } | |
| 60 | 62 | 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 | |
| 72 | 82 | // awaiting holds frame callbacks by output name, due at its next | |
| 73 | 83 | // frame (frameDue, or its page flip). | |
| 74 | 84 | awaiting map[string][]*wayland.Callback | |
| @@ -216,7 +226,12 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 216 | 226 | d.Close() | |
| 217 | 227 | 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 | 235 | s.clock = opts.Clock | |
| 221 | 236 | if s.clock == nil { | |
| 222 | 237 | s.clock = clock.System{} | |
| @@ -298,11 +313,20 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 298 | 313 | func (s *Server) SocketName() string { return s.name } | |
| 299 | 314 | func (s *Server) Run(ctx context.Context) error { | |
| 300 | 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 | 323 | s.log.Info().Str("socket", s.name).Msg("starting wayland") | |
| 302 | 324 | s.started = time.Now() | |
| 303 | 325 | s.ctx = ctx | |
| 304 | 326 | 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 | 330 | go func() { defer wg.Done(); s.forwardWorkspaces(ctx) }() | |
| 307 | 331 | go func() { defer wg.Done(); s.forward(ctx) }() | |
| 308 | 332 | 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.ClientEvent | 42 | + | LeaseRequests chan<- ports.LeaseMessage |
| 43 | - | Contents chan<- ports.SurfaceContent | 43 | + | LeaseEvents <-chan ports.LeaseMessage |
| 44 | - | Commands <-chan ports.ClientCommand | 44 | + | Events chan<- ports.ClientEvent |
| 45 | - | Workspaces <-chan ports.Workspaces | 45 | + | 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.CursorChange | 49 | Cursors chan<- ports.CursorChange | ||
| 48 | // Presented paces frame callbacks on the outputs' page flips; outputs | 50 | // Presented paces frame callbacks on the outputs' page flips; outputs | ||
| @@ -58,17 +60,25 @@ type Channels struct { | |||||
| 58 | OutputApplied <-chan ports.OutputApplied | 60 | OutputApplied <-chan ports.OutputApplied | ||
| 59 | } | 61 | } | ||
| 60 | type Server struct { | 62 | type Server struct { | ||
| 61 | - | nextCapture uint64 | 63 | + | leaseDevices map[string]*leaseDevice |
| 62 | - | captureReplies map[uint64]func(ports.CaptureDone) | 64 | + | pendingLeases map[uint64]*leaseObject |
| 63 | - | captureSources map[*server.Resource]*output | 65 | + | pendingLeaseCards map[uint64]string |
| 64 | - | captureSessions map[*captureSession]struct{} | 66 | + | activeLeases map[leaseKey]*leaseObject |
| 65 | - | display *server.Display | 67 | + | leaseMu sync.Mutex // only the outbound lease queue; never window state |
| 66 | - | env procEnv | 68 | + | leaseOut []ports.LeaseMessage |
| 67 | - | slotsPending bool // core waits for a slot window | 69 | + | leaseReady chan struct{} |
| 68 | - | name string | 70 | + | nextLeaseID uint64 |
| 69 | - | cleanup func() | 71 | + | nextCapture uint64 |
| 70 | - | log zerowrap.Logger | 72 | + | captureReplies map[uint64]func(ports.CaptureDone) |
| 71 | - | channels Channels | 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 | |||
| 72 | // awaiting holds frame callbacks by output name, due at its next | 82 | // 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.Callback | 84 | 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, err | 227 | 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.Clock | 235 | 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 = ctx | 325 | s.ctx = ctx | ||
| 304 | var wg sync.WaitGroup | 326 | 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 { | |||
| 84 | 84 | queue []*update | |
| 85 | 85 | // contentType is the wp_content_type_v1; contentKind the committed type. | |
| 86 | 86 | contentType *contentTypeHandler | |
| 87 | + | // alpha is the surface's wp_alpha_modifier_surface_v1. | |
| 88 | + | alpha *alphaHandler | |
| 87 | 89 | contentKind uint32 | |
| 88 | 90 | // committed is what the last commit changed, in buffer pixels (full: | |
| 89 | 91 | // everything), read by the window's damage history. | |
| @@ -115,6 +117,8 @@ type pendingCommit struct { | |||
| 115 | 117 | color SurfaceColor | |
| 116 | 118 | representation surfaceRepresentation | |
| 117 | 119 | kind uint32 | |
| 120 | + | // fade is 1 - the wp_alpha_modifier_v1 multiplier (sticky). | |
| 121 | + | fade float32 | |
| 118 | 122 | // barrier and wait are fifo requests; at the commit timestamp. | |
| 119 | 123 | barrier, wait bool | |
| 120 | 124 | at time.Time | |
| @@ -375,6 +379,7 @@ func (s *surface) applyCommit(u *update) { | |||
| 375 | 379 | if u.opaqueSet { | |
| 376 | 380 | s.opaque = u.opaque | |
| 377 | 381 | } | |
| 382 | + | oldFade := s.content.Fade | |
| 378 | 383 | hinted := s.async != u.async || s.color != u.color || s.representation != u.representation | |
| 379 | 384 | if s.async != u.async { | |
| 380 | 385 | // Contents carry the hint of every surface in the tree. | |
| @@ -425,7 +430,8 @@ func (s *surface) applyCommit(u *update) { | |||
| 425 | 430 | c.Source, _ = s.source(c.Width, c.Height) | |
| 426 | 431 | c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height) | |
| 427 | 432 | 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 | 435 | resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH | |
| 430 | 436 | s.commitDamage(u, true, resized, c.Width, c.Height) | |
| 431 | 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 | 459 | s.content.Transform = s.transform | |
| 454 | 460 | s.content.Source, _ = s.source(s.content.Width, s.content.Height) | |
| 455 | 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 | 466 | if s.current == nil { | |
| 460 | 467 | 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 | 470 | s.version++ | |
| 464 | 471 | s.root().treeDirty = true | |
| 465 | 472 | } | |
| @@ -483,7 +490,7 @@ func (s *surface) applyCommit(u *update) { | |||
| 483 | 490 | } | |
| 484 | 491 | reshaped := s.has && (s.content.LogicalW != oldW || s.content.LogicalH != oldH || s.content.Source != oldSource || s.content.Transform != oldTransform) | |
| 485 | 492 | // 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) | |
| 487 | 494 | drawn := fresh || damaged || moved || geometry || hinted || reshaped || opacity || s.sub.parent != nil | |
| 488 | 495 | if drawn { | |
| 489 | 496 | if moved || geometry || reshaped || opacity { | |
| @@ -84,6 +84,8 @@ type surface struct { | |||||
| 84 | queue []*update | 84 | 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 *contentTypeHandler | 86 | contentType *contentTypeHandler | ||
| 87 | + | // alpha is the surface's wp_alpha_modifier_surface_v1. | |||
| 88 | + | alpha *alphaHandler | |||
| 87 | contentKind uint32 | 89 | 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 SurfaceColor | 117 | color SurfaceColor | ||
| 116 | representation surfaceRepresentation | 118 | representation surfaceRepresentation | ||
| 117 | kind uint32 | 119 | 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 bool | 123 | barrier, wait bool | ||
| 120 | at time.Time | 124 | 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.opaque | 380 | s.opaque = u.opaque | ||
| 377 | } | 381 | } | ||
| 382 | + | oldFade := s.content.Fade | |||
| 378 | hinted := s.async != u.async || s.color != u.color || s.representation != u.representation | 383 | 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.Opaque | 432 | 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.LogicalH | 435 | 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.transform | 459 | 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{}, false | 467 | 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 = true | 471 | 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 != oldOpaque | 493 | + | opacity := s.has && (s.content.Opaque != oldOpaque || s.content.Fade != oldFade) |
| 487 | drawn := fresh || damaged || moved || geometry || hinted || reshaped || opacity || s.sub.parent != nil | 494 | 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) | |||
| 102 | 102 | captured := make(chan ports.CaptureDone, 64) | |
| 103 | 103 | outputFormats := make(chan ports.OutputFormats, 8) | |
| 104 | 104 | outputHeads := make(chan ports.OutputHeads, 8) | |
| 105 | + | leaseRequests := make(chan ports.LeaseMessage, 32) | |
| 106 | + | leaseEvents := make(chan ports.LeaseMessage, 32) | |
| 105 | 107 | applyOutput := make(chan ports.OutputApply, 8) | |
| 106 | 108 | appliedOutput := make(chan ports.OutputApplied, 8) | |
| 107 | 109 | var scales chan ports.ScaleChanged | |
| @@ -123,7 +125,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 123 | 125 | // Every output renders on the same GPU: its formats are the clients'. | |
| 124 | 126 | dmabuf := vulkan.Probe() | |
| 125 | 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 | 129 | if err != nil { | |
| 128 | 130 | km.Close() | |
| 129 | 131 | return err | |
| @@ -247,7 +249,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 247 | 249 | ||
| 248 | 250 | go func() { | |
| 249 | 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 | 253 | apply := newOutputApply(newOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app")) | |
| 252 | 254 | renderLog := logging.For(ctx, "render") | |
| 253 | 255 | 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.ScaleChanged | 109 | 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 err | 131 | 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 ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | + | "os" | |
| 7 | 8 | "path/filepath" | |
| 8 | 9 | "runtime" | |
| 9 | 10 | "runtime/debug" | |
| 11 | + | "slices" | |
| 10 | 12 | "sort" | |
| 11 | 13 | "sync" | |
| 12 | 14 | "time" | |
| @@ -19,6 +21,7 @@ import ( | |||
| 19 | 21 | "github.com/bnema/neferwl/internal/logging" | |
| 20 | 22 | "github.com/bnema/neferwl/internal/ports" | |
| 21 | 23 | "github.com/bnema/zerowrap" | |
| 24 | + | "golang.org/x/sys/unix" | |
| 22 | 25 | ) | |
| 23 | 26 | ||
| 24 | 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 | 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 | 218 | hotplug := make(chan struct{}, 1) | |
| 162 | 219 | go func() { | |
| 163 | 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 | 283 | scanErrors = append(scanErrors, fmt.Errorf("scan %s: %w", c.Path(), err)) | |
| 227 | 284 | continue | |
| 228 | 285 | } | |
| 286 | + | publishLeases(c) | |
| 229 | 287 | stop := func(name string, restart bool) { | |
| 230 | 288 | if r := set.outs[name]; r != nil && cards[name] == c { | |
| 231 | 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 | 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 | 468 | case ev := <-ch.reloads: | |
| 364 | 469 | apply.reload(ev.Config) | |
| 365 | 470 | 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 | continue | 284 | 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" | |||
| 7 | 7 | // forwarding queue. heads, configured and replies are fed from the | |
| 8 | 8 | // outputApply outbox. | |
| 9 | 9 | type outputChannels struct { | |
| 10 | + | leaseRequests <-chan ports.LeaseMessage | |
| 11 | + | leaseEvents chan<- ports.LeaseMessage | |
| 10 | 12 | events chan<- ports.OutputEvent | |
| 11 | 13 | scenes <-chan []ports.Scene | |
| 12 | 14 | 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 the | 7 | // 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.OutputEvent | 12 | events chan<- ports.OutputEvent | ||
| 11 | scenes <-chan []ports.Scene | 13 | scenes <-chan []ports.Scene | ||
| 12 | contents <-chan ports.SurfaceContent | 14 | contents <-chan ports.SurfaceContent | ||
M internal/core/core.go +58 -18
| @@ -578,26 +578,16 @@ func (c *Core) updateInhibit(ctx context.Context) error { | |||
| 578 | 578 | // and sends it to input when it changed. A hidden window holds nothing. | |
| 579 | 579 | func (c *Core) resolveConstraint() { | |
| 580 | 580 | 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} | |
| 599 | 588 | } | |
| 600 | 589 | } | |
| 590 | + | g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r} | |
| 601 | 591 | } | |
| 602 | 592 | // The cursor follows core; input resyncs to it only on a change. | |
| 603 | 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 | 651 | // latest replaces any unread value: only the owner sends and drains; | |
| 616 | 652 | // consumers only receive. | |
| 617 | 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 | 829 | case ports.PointerConstrained: | |
| 794 | 830 | c.constrained = v | |
| 831 | + | case ports.PointerWarp: | |
| 832 | + | if c.warpPointer(ctx, v) != nil { | |
| 833 | + | return nil | |
| 834 | + | } | |
| 795 | 835 | case ports.WindowFullscreenRequest: | |
| 796 | 836 | if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace { | |
| 797 | 837 | 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.PointerConstraint | 580 | 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 | - | continue | 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 | - | } | 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 = v | 830 | 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 | continue | 837 | continue | ||
M internal/core/core_test.go +52 -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 | 638 | func TestPointerConstraint(t *testing.T) { | |
| 587 | 639 | cfg := config.Defaults() | |
| 588 | 640 | 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 = 0 | 640 | 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 ( | |||
| 255 | 255 | // PointerConstraint is an active pointer constraint. Rect is logical: in | |
| 256 | 256 | // PointerConstrained it is window-local and empty means the whole window; | |
| 257 | 257 | // 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 | 260 | type PointerConstraint struct { | |
| 260 | 261 | Mode ConstraintMode | |
| 261 | 262 | Rect Rect | |
| 262 | 263 | X, Y float64 | |
| 264 | + | Warp bool | |
| 263 | 265 | } | |
| 264 | 266 | ||
| 265 | 267 | // Clamp keeps a global point inside the rectangle of a lock or confine; | |
| @@ -284,6 +286,16 @@ type PointerConstrained struct { | |||
| 284 | 286 | ||
| 285 | 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 | 299 | // Mods carries input → core modifier flags. | |
| 288 | 300 | type Mods uint8 | |
| 289 | 301 | ||
| @@ -879,8 +891,11 @@ type SurfaceContent struct { | |||
| 879 | 891 | // buffer: readers apply its inverse. Width and Height stay the buffer's. | |
| 880 | 892 | Transform BufferTransform | |
| 881 | 893 | // 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 | 895 | 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 | 899 | Color SurfaceColor | |
| 885 | 900 | SHM *SHMBuffer | |
| 886 | 901 | DMABuf *DMABuf | |
| @@ -1086,3 +1101,71 @@ type WindowState struct { | |||
| 1086 | 1101 | // Hidden is set for a stashed window while its stash is hidden. | |
| 1087 | 1102 | 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() {} | |
| @@ -255,11 +255,13 @@ const ( | |||||
| 255 | // PointerConstraint is an active pointer constraint. Rect is logical: in | 255 | // 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 cursor | 257 | // 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 ConstraintMode | 261 | Mode ConstraintMode | ||
| 261 | Rect Rect | 262 | Rect Rect | ||
| 262 | X, Y float64 | 263 | 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 uint8 | 300 | 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 BufferTransform | 892 | Transform BufferTransform | ||
| 881 | // Opaque ignores the alpha byte: an x format, or an opaque region that | 893 | // 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 bool | 895 | 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 SurfaceColor | 899 | Color SurfaceColor | ||
| 885 | SHM *SHMBuffer | 900 | SHM *SHMBuffer | ||
| 886 | DMABuf *DMABuf | 901 | 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 bool | 1102 | 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() {} | |||