1package drm
2
3import (
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
15const (
16 ioctlCreateLease = 0xC01864C6
17 ioctlRevokeLease = 0xC00464C9
18 ioctlDropMaster = 0x641f
19)
20
21type modeCreateLease struct {
22 objects uint64
23 count, flags, lessee, fd uint32
24}
25type modeRevokeLease struct{ lessee uint32 }
26
27func (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}
39func (k kmsDevice) revokeLease(id uint32) error {
40 return ioctl(k.fd, ioctlRevokeLease, unsafe.Pointer(&modeRevokeLease{lessee: id}))
41}
42func (k kmsDevice) resources() ([]uint32, []uint32, error) { return resources(k.fd) }
43func (k kmsDevice) connector(id uint32) (connector, error) { return readConnector(k.fd, id) }
44func (k kmsDevice) pickCrtc(c connector, crtcs []uint32, used map[uint32]bool) (uint32, error) {
45 return pickCrtc(k.fd, c, crtcs, used)
46}
47
48type 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.
55func (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}
75func (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}
83func (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}
165func (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}
179func (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.
189func (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.
202func (c *Card) FinishedLeases() []uint32 {
203 ids := c.finished
204 c.finished = nil
205 return ids
206}