feat(drm): drive outputs with atomic commits only, fence-synced flips with kernel timestamps
Every screen change is one nonblocking atomic commit with a flip event: primary plane, cursor plane and VRR together, the frame GPU fence as IN_FENCE_FD. Tearing uses primary-only async commits, probed once per output. Legacy page flips, SETCRTC, cursor ioctls, WAIT_VBLANK, SETPROPERTY and dumb buffers are gone; a card without atomic KMS or a GPU that cannot export scanout images is refused. Flip reports carry the kernel timestamp and vblank sequence and are never merged below 16 queued; composed content counts as read only after its flip. The renderer port returns the frame fence and provides cursor images; CopyBGRX is removed. Mockery is pinned to v3.8.0 like CI.
- commit
- 7449251830e02cb0cc6666dabf1384212c3d0187
- author
- brice · · 2026-09-26 10:34 UTC
- parent
- 1d61aca
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
25 files · +2492 -1062
M .mockery.yml +2 -2
| @@ -18,11 +18,11 @@ packages: | |||
| 18 | 18 | # Unexported adapter seams: mocks live next to the code, in test files. | |
| 19 | 19 | github.com/bnema/nefertty/internal/adapters/drm: | |
| 20 | 20 | interfaces: | |
| 21 | - | cursorPlane: | |
| 21 | + | kms: | |
| 22 | 22 | config: | |
| 23 | 23 | dir: internal/adapters/drm | |
| 24 | 24 | pkgname: drm | |
| 25 | - | filename: cursor_plane_mock_test.go | |
| 25 | + | filename: kms_mock_test.go | |
| 26 | 26 | github.com/bnema/nefertty/internal/adapters/wayland: | |
| 27 | 27 | interfaces: | |
| 28 | 28 | procEnv: | |
A internal/adapters/drm/atomic.go +351 -0
| @@ -0,0 +1,351 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "errors" | |
| 6 | + | "runtime" | |
| 7 | + | "slices" | |
| 8 | + | "sync" | |
| 9 | + | "time" | |
| 10 | + | "unsafe" | |
| 11 | + | ||
| 12 | + | "github.com/bnema/nefertty/internal/ports" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // Atomic KMS (ADR 015): every change on screen is one atomic commit. A | |
| 16 | + | // frame commit flips the primary plane, moves the cursor plane and sets | |
| 17 | + | // VRR together; the kernel waits for the frame's GPU fence (IN_FENCE_FD) | |
| 18 | + | // before flipping, so no thread waits for the GPU or a vblank. | |
| 19 | + | ||
| 20 | + | const ( | |
| 21 | + | ioctlSetClientCap = 0x4010640D // DRM_IOW('d', 0x0D, struct drm_set_client_cap) | |
| 22 | + | ioctlGetPlaneRes = 0xC01064B5 // DRM_IOWR('d', 0xB5, struct drm_mode_get_plane_res) | |
| 23 | + | ioctlGetPlane = 0xC02064B6 // DRM_IOWR('d', 0xB6, struct drm_mode_get_plane) | |
| 24 | + | ioctlGetPropBlob = 0xC01064AC // DRM_IOWR('d', 0xAC, struct drm_mode_get_blob) | |
| 25 | + | ioctlAtomic = 0xC03864BC // DRM_IOWR('d', 0xBC, struct drm_mode_atomic) | |
| 26 | + | ioctlCreateBlob = 0xC01064BD // DRM_IOWR('d', 0xBD, struct drm_mode_create_blob) | |
| 27 | + | ioctlDestroyBlob = 0xC00464BE // DRM_IOWR('d', 0xBE, struct drm_mode_destroy_blob) | |
| 28 | + | ||
| 29 | + | clientCapUniversalPlanes = 2 | |
| 30 | + | clientCapAtomic = 3 | |
| 31 | + | capTimestampMonotonic = 0x6 | |
| 32 | + | capAtomicAsync = 0x15 | |
| 33 | + | ||
| 34 | + | objPlane = 0xeeeeeeee | |
| 35 | + | ||
| 36 | + | flipEventFlag = 0x1 // DRM_MODE_PAGE_FLIP_EVENT | |
| 37 | + | flipAsyncFlag = 0x2 // DRM_MODE_PAGE_FLIP_ASYNC | |
| 38 | + | atomicTestOnly = 0x100 // DRM_MODE_ATOMIC_TEST_ONLY | |
| 39 | + | atomicNonblock = 0x200 // DRM_MODE_ATOMIC_NONBLOCK | |
| 40 | + | atomicAllowModes = 0x400 // DRM_MODE_ATOMIC_ALLOW_MODESET | |
| 41 | + | ||
| 42 | + | planeOverlay = 0 | |
| 43 | + | planePrimary = 1 | |
| 44 | + | planeCursor = 2 | |
| 45 | + | ) | |
| 46 | + | ||
| 47 | + | type setClientCap struct{ capability, value uint64 } | |
| 48 | + | ||
| 49 | + | type getPlaneRes struct { | |
| 50 | + | planeIDs uint64 | |
| 51 | + | count, _ uint32 | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | type getPlane struct { | |
| 55 | + | planeID, crtcID, fbID, possibleCrtcs, gammaSize, countFormats uint32 | |
| 56 | + | formats uint64 | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | type getBlob struct { | |
| 60 | + | blobID, length uint32 | |
| 61 | + | data uint64 | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | type createBlob struct { | |
| 65 | + | data uint64 | |
| 66 | + | length, blobID uint32 | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | type modeAtomic struct { | |
| 70 | + | flags, countObjs uint32 | |
| 71 | + | objs, countProps, props, values, reserved, userData uint64 | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // kms is the kernel side of the output path. kmsDevice is the real one; | |
| 75 | + | // tests use its Mockery mock. | |
| 76 | + | type kms interface { | |
| 77 | + | // commit issues DRM_IOCTL_MODE_ATOMIC; userData comes back in the flip event. | |
| 78 | + | commit(req *atomicReq, flags uint32, userData uint64) error | |
| 79 | + | createBlob(data []byte) (uint32, error) | |
| 80 | + | destroyBlob(id uint32) error | |
| 81 | + | getBlob(id uint32) ([]byte, error) | |
| 82 | + | // planes lists every plane with the CRTCs it can show on. | |
| 83 | + | planes() ([]planeRes, error) | |
| 84 | + | objProps(obj, typ uint32) (map[string][2]uint64, error) | |
| 85 | + | // addFB imports a single-plane dmabuf as a framebuffer of format. | |
| 86 | + | addFB(b *ports.DMABuf, format uint32) (uint32, error) | |
| 87 | + | rmFB(id uint32) error | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | // planeRes is one plane of the card: possible is a bitmask of CRTC indices. | |
| 91 | + | type planeRes struct{ id, possible uint32 } | |
| 92 | + | ||
| 93 | + | // atomicReq collects (object, property, value) triples in insertion order. | |
| 94 | + | type atomicReq struct { | |
| 95 | + | objs []uint32 | |
| 96 | + | props [][]propValue | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | type propValue struct { | |
| 100 | + | prop uint32 | |
| 101 | + | val uint64 | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // set adds a property; unknown properties (id 0) are skipped. | |
| 105 | + | func (r *atomicReq) set(obj, prop uint32, v uint64) { | |
| 106 | + | if prop == 0 || obj == 0 { | |
| 107 | + | return | |
| 108 | + | } | |
| 109 | + | i := slices.Index(r.objs, obj) | |
| 110 | + | if i < 0 { | |
| 111 | + | r.objs = append(r.objs, obj) | |
| 112 | + | r.props = append(r.props, nil) | |
| 113 | + | i = len(r.objs) - 1 | |
| 114 | + | } | |
| 115 | + | for j, p := range r.props[i] { | |
| 116 | + | if p.prop == prop { | |
| 117 | + | r.props[i][j].val = v | |
| 118 | + | return | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | r.props[i] = append(r.props[i], propValue{prop, v}) | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | // value returns a property set on an object. | |
| 125 | + | func (r *atomicReq) value(obj, prop uint32) (uint64, bool) { | |
| 126 | + | i := slices.Index(r.objs, obj) | |
| 127 | + | if i < 0 { | |
| 128 | + | return 0, false | |
| 129 | + | } | |
| 130 | + | for _, p := range r.props[i] { | |
| 131 | + | if p.prop == prop { | |
| 132 | + | return p.val, true | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | return 0, false | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | // kmsDevice drives a card fd. | |
| 139 | + | type kmsDevice struct { | |
| 140 | + | fd int | |
| 141 | + | // gemMu serialises GEM handle import and close: handles are shared by | |
| 142 | + | // every import of a buffer on the fd, and outputs run on their own | |
| 143 | + | // goroutines. modifiers is DRM_CAP_ADDFB2_MODIFIERS. | |
| 144 | + | gemMu *sync.Mutex | |
| 145 | + | modifiers bool | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | func (k kmsDevice) commit(req *atomicReq, flags uint32, userData uint64) error { | |
| 149 | + | if len(req.objs) == 0 { | |
| 150 | + | return errors.New("empty atomic commit") | |
| 151 | + | } | |
| 152 | + | counts := make([]uint32, len(req.objs)) | |
| 153 | + | var props []uint32 | |
| 154 | + | var vals []uint64 | |
| 155 | + | for i, ps := range req.props { | |
| 156 | + | counts[i] = uint32(len(ps)) | |
| 157 | + | for _, p := range ps { | |
| 158 | + | props = append(props, p.prop) | |
| 159 | + | vals = append(vals, p.val) | |
| 160 | + | } | |
| 161 | + | } | |
| 162 | + | a := modeAtomic{flags: flags, countObjs: uint32(len(req.objs)), userData: userData, | |
| 163 | + | objs: uint64(uintptr(unsafe.Pointer(&req.objs[0]))), countProps: uint64(uintptr(unsafe.Pointer(&counts[0]))), | |
| 164 | + | props: uint64(uintptr(unsafe.Pointer(&props[0]))), values: uint64(uintptr(unsafe.Pointer(&vals[0])))} | |
| 165 | + | err := ioctl(k.fd, ioctlAtomic, unsafe.Pointer(&a)) | |
| 166 | + | runtime.KeepAlive(req) | |
| 167 | + | runtime.KeepAlive(counts) | |
| 168 | + | runtime.KeepAlive(props) | |
| 169 | + | runtime.KeepAlive(vals) | |
| 170 | + | return err | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | func (k kmsDevice) createBlob(data []byte) (uint32, error) { | |
| 174 | + | b := createBlob{data: uint64(uintptr(unsafe.Pointer(&data[0]))), length: uint32(len(data))} | |
| 175 | + | err := ioctl(k.fd, ioctlCreateBlob, unsafe.Pointer(&b)) | |
| 176 | + | runtime.KeepAlive(data) | |
| 177 | + | return b.blobID, err | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | func (k kmsDevice) destroyBlob(id uint32) error { | |
| 181 | + | v := id | |
| 182 | + | return ioctl(k.fd, ioctlDestroyBlob, unsafe.Pointer(&v)) | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | func (k kmsDevice) getBlob(id uint32) ([]byte, error) { | |
| 186 | + | b := getBlob{blobID: id} | |
| 187 | + | if err := ioctl(k.fd, ioctlGetPropBlob, unsafe.Pointer(&b)); err != nil { | |
| 188 | + | return nil, err | |
| 189 | + | } | |
| 190 | + | if b.length == 0 { | |
| 191 | + | return nil, nil | |
| 192 | + | } | |
| 193 | + | data := make([]byte, b.length) | |
| 194 | + | b.data = uint64(uintptr(unsafe.Pointer(&data[0]))) | |
| 195 | + | err := ioctl(k.fd, ioctlGetPropBlob, unsafe.Pointer(&b)) | |
| 196 | + | runtime.KeepAlive(data) | |
| 197 | + | return data[:min(int(b.length), len(data))], err | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | func (k kmsDevice) planes() ([]planeRes, error) { | |
| 201 | + | var r getPlaneRes | |
| 202 | + | if err := ioctl(k.fd, ioctlGetPlaneRes, unsafe.Pointer(&r)); err != nil { | |
| 203 | + | return nil, err | |
| 204 | + | } | |
| 205 | + | if r.count == 0 { | |
| 206 | + | return nil, nil | |
| 207 | + | } | |
| 208 | + | ids := make([]uint32, r.count) | |
| 209 | + | r.planeIDs = uint64(uintptr(unsafe.Pointer(&ids[0]))) | |
| 210 | + | if err := ioctl(k.fd, ioctlGetPlaneRes, unsafe.Pointer(&r)); err != nil { | |
| 211 | + | return nil, err | |
| 212 | + | } | |
| 213 | + | runtime.KeepAlive(ids) | |
| 214 | + | var out []planeRes | |
| 215 | + | for _, id := range ids[:min(int(r.count), len(ids))] { | |
| 216 | + | p := getPlane{planeID: id} | |
| 217 | + | if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) != nil { | |
| 218 | + | continue | |
| 219 | + | } | |
| 220 | + | out = append(out, planeRes{id: id, possible: p.possibleCrtcs}) | |
| 221 | + | } | |
| 222 | + | return out, nil | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | func (k kmsDevice) objProps(obj, typ uint32) (map[string][2]uint64, error) { | |
| 226 | + | return objProps(k.fd, obj, typ) | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | func (k kmsDevice) rmFB(id uint32) error { | |
| 230 | + | v := id | |
| 231 | + | return ioctl(k.fd, ioctlRmFB, unsafe.Pointer(&v)) | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | // enableAtomic turns on atomic modesetting and checks the caps outputs rely on. | |
| 235 | + | func enableAtomic(fd int) error { | |
| 236 | + | for _, c := range []uint64{clientCapUniversalPlanes, clientCapAtomic} { | |
| 237 | + | v := setClientCap{capability: c, value: 1} | |
| 238 | + | if err := ioctl(fd, ioctlSetClientCap, unsafe.Pointer(&v)); err != nil { | |
| 239 | + | return errors.Join(errors.New("atomic modesetting unsupported"), err) | |
| 240 | + | } | |
| 241 | + | } | |
| 242 | + | if !hasCap(fd, capTimestampMonotonic) { | |
| 243 | + | return errors.New("flip timestamps are not CLOCK_MONOTONIC") | |
| 244 | + | } | |
| 245 | + | return nil | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | func hasCap(fd int, c uint64) bool { | |
| 249 | + | v := getCap{capability: c} | |
| 250 | + | return ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value == 1 | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | // plane is a KMS plane an output drives, with its property IDs by name. | |
| 254 | + | type plane struct { | |
| 255 | + | id, typ uint32 | |
| 256 | + | props map[string]uint32 | |
| 257 | + | crtc uint32 // CRTC_ID when read | |
| 258 | + | formats []ports.DMABufFormat | |
| 259 | + | } | |
| 260 | + | ||
| 261 | + | func (p *plane) prop(name string) uint32 { | |
| 262 | + | if p == nil { | |
| 263 | + | return 0 | |
| 264 | + | } | |
| 265 | + | return p.props[name] | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | // readPlanes returns the planes that can show on the CRTC at index pipe. | |
| 269 | + | func readPlanes(k kms, pipe int) ([]*plane, error) { | |
| 270 | + | res, err := k.planes() | |
| 271 | + | if err != nil { | |
| 272 | + | return nil, err | |
| 273 | + | } | |
| 274 | + | var out []*plane | |
| 275 | + | for _, r := range res { | |
| 276 | + | if r.possible&(1<<pipe) == 0 { | |
| 277 | + | continue | |
| 278 | + | } | |
| 279 | + | props, err := k.objProps(r.id, objPlane) | |
| 280 | + | if err != nil { | |
| 281 | + | continue | |
| 282 | + | } | |
| 283 | + | p := &plane{id: r.id, typ: uint32(props["type"][1]), props: map[string]uint32{}, crtc: uint32(props["CRTC_ID"][1])} | |
| 284 | + | for name, v := range props { | |
| 285 | + | p.props[name] = uint32(v[0]) | |
| 286 | + | } | |
| 287 | + | if blob := uint32(props["IN_FORMATS"][1]); blob != 0 { | |
| 288 | + | if data, err := k.getBlob(blob); err == nil { | |
| 289 | + | p.formats = parseInFormats(data) | |
| 290 | + | } | |
| 291 | + | } | |
| 292 | + | out = append(out, p) | |
| 293 | + | } | |
| 294 | + | return out, nil | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | // parseInFormats decodes an IN_FORMATS blob (struct drm_format_modifier_blob) | |
| 298 | + | // into format and modifier pairs. | |
| 299 | + | func parseInFormats(b []byte) []ports.DMABufFormat { | |
| 300 | + | if len(b) < 24 { | |
| 301 | + | return nil | |
| 302 | + | } | |
| 303 | + | le := binary.LittleEndian | |
| 304 | + | nf, fo := int(le.Uint32(b[8:])), int(le.Uint32(b[12:])) | |
| 305 | + | nm, mo := int(le.Uint32(b[16:])), int(le.Uint32(b[20:])) | |
| 306 | + | if fo < 0 || fo+nf*4 > len(b) || mo < 0 || mo+nm*24 > len(b) { | |
| 307 | + | return nil | |
| 308 | + | } | |
| 309 | + | var out []ports.DMABufFormat | |
| 310 | + | for m := range nm { | |
| 311 | + | e := b[mo+m*24:] | |
| 312 | + | mask, offset, mod := le.Uint64(e), int(le.Uint32(e[8:])), le.Uint64(e[16:]) | |
| 313 | + | for bit := range 64 { | |
| 314 | + | i := offset + bit | |
| 315 | + | if mask&(1<<bit) == 0 || i >= nf { | |
| 316 | + | continue | |
| 317 | + | } | |
| 318 | + | out = append(out, ports.DMABufFormat{Format: le.Uint32(b[fo+i*4:]), Modifier: mod}) | |
| 319 | + | } | |
| 320 | + | } | |
| 321 | + | return out | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | // flipEvent is a completed commit on a CRTC: when is its CLOCK_MONOTONIC | |
| 325 | + | // time, seq the CRTC's vblank counter, user the commit's userData. | |
| 326 | + | type flipEvent struct { | |
| 327 | + | crtc uint32 | |
| 328 | + | user uint64 | |
| 329 | + | when time.Duration | |
| 330 | + | seq uint32 | |
| 331 | + | } | |
| 332 | + | ||
| 333 | + | // parseFlips parses DRM events read from the card fd. | |
| 334 | + | func parseFlips(buf []byte) []flipEvent { | |
| 335 | + | var out []flipEvent | |
| 336 | + | le := binary.LittleEndian | |
| 337 | + | for len(buf) >= 8 { | |
| 338 | + | typ := le.Uint32(buf) | |
| 339 | + | length := int(le.Uint32(buf[4:])) | |
| 340 | + | if length < 8 || length > len(buf) { | |
| 341 | + | break | |
| 342 | + | } | |
| 343 | + | // struct drm_event_vblank: base(8) user_data(8) sec usec sequence crtc_id. | |
| 344 | + | if typ == eventFlipDone && length >= 32 { | |
| 345 | + | sec, usec := le.Uint32(buf[16:]), le.Uint32(buf[20:]) | |
| 346 | + | out = append(out, flipEvent{user: le.Uint64(buf[8:]), when: time.Duration(sec)*time.Second + time.Duration(usec)*time.Microsecond, seq: le.Uint32(buf[24:]), crtc: le.Uint32(buf[28:])}) | |
| 347 | + | } | |
| 348 | + | buf = buf[length:] | |
| 349 | + | } | |
| 350 | + | return out | |
| 351 | + | } | |
M internal/adapters/drm/card.go +42 -27
| @@ -6,15 +6,14 @@ import ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | "slices" | |
| 8 | 8 | "sync" | |
| 9 | - | "unsafe" | |
| 10 | 9 | ||
| 11 | 10 | "github.com/bnema/zerowrap" | |
| 12 | 11 | "golang.org/x/sys/unix" | |
| 13 | 12 | ) | |
| 14 | 13 | ||
| 15 | 14 | // Card is one DRM device and the outputs it drives. Scan finds connected | |
| 16 | - | // connectors; each gets its own Output (CRTC, buffers, cursor) run by one | |
| 17 | - | // goroutine. One reader goroutine reads page-flip events for all of them | |
| 15 | + | // connectors; each gets its own Output (CRTC, planes, cursor) run by one | |
| 16 | + | // goroutine. One reader goroutine reads commit events for all of them | |
| 18 | 17 | // and hands each to its output by CRTC. | |
| 19 | 18 | type Card struct { | |
| 20 | 19 | fd int | |
| @@ -24,30 +23,30 @@ type Card struct { | |||
| 24 | 23 | crtcs []uint32 | |
| 25 | 24 | // outputs by connector name; owned by the goroutine that calls Scan. | |
| 26 | 25 | outputs map[string]*Output | |
| 27 | - | flips map[uint32]chan int // by CRTC; set before the reader starts | |
| 28 | - | // gemMu serialises GEM handle import and close on fd (outputs run on | |
| 29 | - | // their own goroutines); modifiers is DRM_CAP_ADDFB2_MODIFIERS. | |
| 30 | - | gemMu sync.Mutex | |
| 31 | - | modifiers bool | |
| 32 | - | // async is DRM_CAP_ASYNC_PAGE_FLIP. | |
| 26 | + | flips map[uint32]chan flipEvent // by CRTC; set before the reader starts | |
| 27 | + | k kmsDevice | |
| 28 | + | // taken are planes driven by an output, by plane ID. | |
| 29 | + | taken map[uint32]bool | |
| 30 | + | // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. | |
| 33 | 31 | async bool | |
| 34 | 32 | } | |
| 35 | 33 | ||
| 36 | - | // OpenCard reads the card's CRTCs. fd stays owned by the caller. | |
| 34 | + | // OpenCard turns on atomic modesetting and reads the card's CRTCs. fd | |
| 35 | + | // stays owned by the caller. A card without atomic KMS is refused. | |
| 37 | 36 | func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error) { | |
| 37 | + | if err := enableAtomic(fd); err != nil { | |
| 38 | + | return nil, fmt.Errorf("%s: %w", path, err) | |
| 39 | + | } | |
| 38 | 40 | crtcs, _, err := resources(fd) | |
| 39 | 41 | if err != nil { | |
| 40 | 42 | return nil, fmt.Errorf("%s: %w", path, err) | |
| 41 | 43 | } | |
| 42 | - | flips := make(map[uint32]chan int, len(crtcs)) | |
| 44 | + | flips := make(map[uint32]chan flipEvent, len(crtcs)) | |
| 43 | 45 | for _, c := range crtcs { | |
| 44 | - | flips[c] = make(chan int, 4) | |
| 46 | + | flips[c] = make(chan flipEvent, 4) | |
| 45 | 47 | } | |
| 46 | - | cp := getCap{capability: capAddFB2Modifiers} | |
| 47 | - | mods := ioctl(fd, ioctlGetCap, unsafe.Pointer(&cp)) == nil && cp.value == 1 | |
| 48 | - | ap := getCap{capability: capAsyncPageFlip} | |
| 49 | - | async := ioctl(fd, ioctlGetCap, unsafe.Pointer(&ap)) == nil && ap.value == 1 | |
| 50 | - | return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips, modifiers: mods, async: async}, nil | |
| 48 | + | k := kmsDevice{fd: fd, gemMu: &sync.Mutex{}, modifiers: hasCap(fd, capAddFB2Modifiers)} | |
| 49 | + | 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 | |
| 51 | 50 | } | |
| 52 | 51 | ||
| 53 | 52 | // Path is the device path, e.g. /dev/dri/card1. | |
| @@ -103,9 +102,16 @@ func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) { | |||
| 103 | 102 | return added, removed, replaced, nil | |
| 104 | 103 | } | |
| 105 | 104 | ||
| 106 | - | // Release forgets an output closed by its goroutine, freeing its CRTC for the | |
| 107 | - | // next Scan. | |
| 108 | - | func (c *Card) Release(name string) { delete(c.outputs, name) } | |
| 105 | + | // Release forgets an output closed by its goroutine, freeing its CRTC and | |
| 106 | + | // planes for the next Scan. | |
| 107 | + | func (c *Card) Release(name string) { | |
| 108 | + | if o := c.outputs[name]; o != nil { | |
| 109 | + | for _, p := range o.owned() { | |
| 110 | + | delete(c.taken, p.id) | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | delete(c.outputs, name) | |
| 114 | + | } | |
| 109 | 115 | ||
| 110 | 116 | // Outputs returns the driven outputs. | |
| 111 | 117 | func (c *Card) Outputs() []*Output { | |
| @@ -131,11 +137,19 @@ func (c *Card) open(conn connector, mode modeInfo) (*Output, error) { | |||
| 131 | 137 | if err != nil { | |
| 132 | 138 | return nil, err | |
| 133 | 139 | } | |
| 134 | - | return newOutput(c, conn, mode, crtc) | |
| 140 | + | o, err := newOutput(c, conn, mode, crtc) | |
| 141 | + | if err != nil { | |
| 142 | + | return nil, err | |
| 143 | + | } | |
| 144 | + | for _, p := range o.owned() { | |
| 145 | + | c.taken[p.id] = true | |
| 146 | + | } | |
| 147 | + | return o, nil | |
| 135 | 148 | } | |
| 136 | 149 | ||
| 137 | - | // ReadEvents reads page-flip events until ctx ends and routes them to the | |
| 138 | - | // output of their CRTC. Run it once per card. | |
| 150 | + | // ReadEvents reads commit events until ctx ends and routes them to the | |
| 151 | + | // output of their CRTC. Run it once per card. An event is never dropped: | |
| 152 | + | // the output owes it a pending commit. | |
| 139 | 153 | func (c *Card) ReadEvents(ctx context.Context) error { | |
| 140 | 154 | buf := make([]byte, 1024) | |
| 141 | 155 | for ctx.Err() == nil { | |
| @@ -154,14 +168,15 @@ func (c *Card) ReadEvents(ctx context.Context) error { | |||
| 154 | 168 | } | |
| 155 | 169 | return fmt.Errorf("drm read: %w", err) | |
| 156 | 170 | } | |
| 157 | - | for _, crtc := range flipCrtcs(buf[:m]) { | |
| 158 | - | ch := c.flips[crtc] | |
| 171 | + | for _, ev := range parseFlips(buf[:m]) { | |
| 172 | + | ch := c.flips[ev.crtc] | |
| 159 | 173 | if ch == nil { | |
| 160 | 174 | continue | |
| 161 | 175 | } | |
| 162 | 176 | select { | |
| 163 | - | case ch <- 1: | |
| 164 | - | default: // the output is busy; the flip is counted on its next event | |
| 177 | + | case ch <- ev: | |
| 178 | + | case <-ctx.Done(): | |
| 179 | + | return nil | |
| 165 | 180 | } | |
| 166 | 181 | } | |
| 167 | 182 | } | |
M internal/adapters/drm/cursor.go +123 -228
| @@ -1,281 +1,176 @@ | |||
| 1 | 1 | package drm | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "fmt" | |
| 4 | + | "errors" | |
| 5 | 5 | "sync" | |
| 6 | - | "time" | |
| 7 | - | "unsafe" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | - | // Hardware cursor through the legacy cursor ioctls: the GPU overlays a small | |
| 11 | - | // buffer at a position, so moving it never re-renders the frame. | |
| 10 | + | // Hardware cursor on the CRTC's cursor plane. Move and Hide come from the | |
| 11 | + | // input goroutine: they only record the position and wake the output, | |
| 12 | + | // whose goroutine puts the cursor plane in its next atomic commit. A move | |
| 13 | + | // while a commit is pending waits for it, so the cursor moves at most once | |
| 14 | + | // per flip and never blocks input. | |
| 12 | 15 | ||
| 13 | 16 | const ( | |
| 14 | - | ioctlGetCap = 0xC010640C // DRM_IOWR('d', 0x0C, struct drm_get_cap) | |
| 15 | - | ioctlCursor2 = 0xC02464BB // DRM_IOWR('d', 0xBB, struct drm_mode_cursor2) | |
| 16 | - | ioctlVblank = 0xC018643A // DRM_IOWR('d', 0x3A, union drm_wait_vblank) | |
| 17 | - | vblankRel = 0x1 // _DRM_VBLANK_RELATIVE | |
| 18 | - | vblankCrtcSh = 1 // _DRM_VBLANK_HIGH_CRTC_SHIFT | |
| 19 | - | vblankCrtcMk = 0x3e // _DRM_VBLANK_HIGH_CRTC_MASK | |
| 20 | - | capCursorW = 0x8 | |
| 21 | - | capCursorH = 0x9 | |
| 22 | - | cursorBO = 0x01 | |
| 23 | - | cursorMove = 0x02 | |
| 17 | + | ioctlGetCap = 0xC010640C // DRM_IOWR('d', 0x0C, struct drm_get_cap) | |
| 18 | + | capCursorW = 0x8 | |
| 19 | + | capCursorH = 0x9 | |
| 24 | 20 | ) | |
| 25 | 21 | ||
| 26 | 22 | type getCap struct{ capability, value uint64 } | |
| 27 | 23 | ||
| 28 | - | // waitVblank is union drm_wait_vblank (request and reply share 24 bytes). | |
| 29 | - | type waitVblank struct { | |
| 30 | - | typ, sequence uint32 | |
| 31 | - | sec, usec int64 | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | // cursorPlane is the kernel side of the hardware cursor. | |
| 35 | - | type cursorPlane interface { | |
| 36 | - | // Set issues DRM_IOCTL_MODE_CURSOR2. | |
| 37 | - | Set(v *modeCursor2) error | |
| 38 | - | // WaitVblank blocks until the next vblank of the cursor's CRTC. | |
| 39 | - | WaitVblank() error | |
| 40 | - | } | |
| 41 | - | ||
| 42 | - | // kmsCursorPlane drives the cursor of the CRTC at index pipe. | |
| 43 | - | type kmsCursorPlane struct{ fd, pipe int } | |
| 44 | - | ||
| 45 | - | func (p kmsCursorPlane) Set(v *modeCursor2) error { | |
| 46 | - | return ioctl(p.fd, ioctlCursor2, unsafe.Pointer(v)) | |
| 47 | - | } | |
| 48 | - | ||
| 49 | - | func (p kmsCursorPlane) WaitVblank() error { | |
| 50 | - | v := waitVblank{typ: vblankRel | uint32(p.pipe<<vblankCrtcSh)&vblankCrtcMk, sequence: 1} | |
| 51 | - | return ioctl(p.fd, ioctlVblank, unsafe.Pointer(&v)) | |
| 52 | - | } | |
| 53 | - | ||
| 54 | - | type modeCursor2 struct { | |
| 55 | - | flags, crtcID uint32 | |
| 56 | - | x, y int32 | |
| 57 | - | width, height uint32 | |
| 58 | - | handle uint32 | |
| 59 | - | hotX, hotY int32 | |
| 60 | - | } | |
| 61 | - | ||
| 62 | - | // Cursor is the hardware cursor of one output. Move is safe to call from the | |
| 63 | - | // input goroutine while the output goroutine changes the image. | |
| 64 | - | // | |
| 65 | - | // Move only records the position. A worker waits for the next vblank and then | |
| 66 | - | // applies the latest one, so the cursor moves at most once per frame, between | |
| 67 | - | // scanouts: moving it mid-scanout tears it into doubled images, and input is | |
| 68 | - | // never throttled by the ioctl. | |
| 24 | + | // Cursor is the hardware cursor of one output. | |
| 69 | 25 | type Cursor struct { | |
| 70 | - | io sync.Mutex // serialises cursor ioctls and guards the fields below | |
| 71 | - | fd int | |
| 72 | - | crtc uint32 | |
| 73 | - | buf *dumbBuffer | |
| 74 | - | size int // buffer side | |
| 75 | - | hotX, hotY int | |
| 76 | - | shown bool | |
| 77 | - | hidden bool // the plane is off because the pointer left this output | |
| 78 | - | plane cursorPlane | |
| 79 | - | ||
| 80 | - | mu sync.Mutex // guards x, y, away and stats; never held across an ioctl | |
| 26 | + | mu sync.Mutex // guards x, y, away and moves | |
| 81 | 27 | x, y int // physical position of the hotspot | |
| 82 | 28 | away bool // Hide was called and no Move since | |
| 83 | - | stats CursorStats | |
| 29 | + | moves int | |
| 30 | + | wake chan struct{} | |
| 31 | + | ||
| 32 | + | // Owned by the output goroutine. | |
| 33 | + | plane *plane | |
| 34 | + | size int | |
| 35 | + | fbs [2]uint32 // the two images; cur is on screen or next | |
| 36 | + | cur int | |
| 37 | + | image bool // an image is loaded | |
| 38 | + | hotX, hotY int | |
| 39 | + | applied cursorState // what the last commit put on the plane | |
| 40 | + | commits int | |
| 41 | + | } | |
| 84 | 42 | ||
| 85 | - | wake chan struct{} | |
| 86 | - | stop chan struct{} | |
| 87 | - | done chan struct{} | |
| 43 | + | // cursorState is the cursor plane as committed: off, or fb at x, y. | |
| 44 | + | type cursorState struct { | |
| 45 | + | on bool | |
| 46 | + | fb uint32 | |
| 47 | + | x, y int | |
| 88 | 48 | } | |
| 89 | 49 | ||
| 90 | 50 | // CursorStats counts cursor work since the last TakeStats. | |
| 91 | 51 | type CursorStats struct { | |
| 92 | - | Moves int // Move calls | |
| 93 | - | Ioctls int // move ioctls issued by the worker | |
| 94 | - | MaxIoctl time.Duration // slowest move ioctl | |
| 52 | + | Moves int // Move calls | |
| 53 | + | Commits int // commits that moved or changed the cursor | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | func newCursor(p *plane, size int) *Cursor { | |
| 57 | + | return &Cursor{plane: p, size: size, wake: make(chan struct{}, 1)} | |
| 95 | 58 | } | |
| 96 | 59 | ||
| 97 | - | // TakeStats returns and resets the counters. | |
| 60 | + | // TakeStats returns and resets the counters. Output goroutine only. | |
| 98 | 61 | func (c *Cursor) TakeStats() CursorStats { | |
| 99 | 62 | c.mu.Lock() | |
| 100 | - | defer c.mu.Unlock() | |
| 101 | - | s := c.stats | |
| 102 | - | c.stats = CursorStats{} | |
| 63 | + | s := CursorStats{Moves: c.moves, Commits: c.commits} | |
| 64 | + | c.moves, c.commits = 0, 0 | |
| 65 | + | c.mu.Unlock() | |
| 103 | 66 | return s | |
| 104 | 67 | } | |
| 105 | 68 | ||
| 106 | - | // newCursor allocates a cursor buffer at the driver's preferred size (64 when | |
| 107 | - | // the driver does not say). | |
| 108 | - | func newCursor(fd int, crtc uint32, pipe int) (*Cursor, error) { | |
| 109 | - | // The plane is square: the smaller of the width and height caps. | |
| 110 | - | size := 0 | |
| 111 | - | for _, c := range []uint64{capCursorW, capCursorH} { | |
| 112 | - | v := getCap{capability: c} | |
| 113 | - | if ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value >= 16 && v.value <= 512 { | |
| 114 | - | if size == 0 || int(v.value) < size { | |
| 115 | - | size = int(v.value) | |
| 116 | - | } | |
| 117 | - | } | |
| 118 | - | } | |
| 119 | - | if size == 0 { | |
| 120 | - | size = 64 | |
| 121 | - | } | |
| 122 | - | buf, err := newDumb(fd, size, size) | |
| 123 | - | if err != nil { | |
| 124 | - | return nil, err | |
| 125 | - | } | |
| 126 | - | // The cursor ioctl passes no pitch: drivers assume width*4. | |
| 127 | - | if int(buf.pitch) != size*4 { | |
| 128 | - | buf.destroy(fd) | |
| 129 | - | return nil, fmt.Errorf("cursor buffer pitch %d, want %d", buf.pitch, size*4) | |
| 130 | - | } | |
| 131 | - | c := &Cursor{fd: fd, crtc: crtc, buf: buf, size: size, plane: kmsCursorPlane{fd: fd, pipe: pipe}} | |
| 132 | - | c.start() | |
| 133 | - | return c, nil | |
| 134 | - | } | |
| 69 | + | // Limit is the largest image side the cursor plane accepts. | |
| 70 | + | func (c *Cursor) Limit() int { return c.size } | |
| 135 | 71 | ||
| 136 | - | // start runs the worker that applies coalesced moves. | |
| 137 | - | func (c *Cursor) start() { | |
| 138 | - | c.wake = make(chan struct{}, 1) | |
| 139 | - | c.stop = make(chan struct{}) | |
| 140 | - | c.done = make(chan struct{}) | |
| 141 | - | go func() { | |
| 142 | - | defer close(c.done) | |
| 143 | - | for { | |
| 144 | - | select { | |
| 145 | - | case <-c.stop: | |
| 146 | - | return | |
| 147 | - | case <-c.wake: | |
| 148 | - | // Errors (VT switched away) fall through: the move | |
| 149 | - | // fails too and Reapply restores the cursor. | |
| 150 | - | _ = c.plane.WaitVblank() | |
| 151 | - | c.io.Lock() | |
| 152 | - | c.mu.Lock() | |
| 153 | - | away := c.away | |
| 154 | - | c.mu.Unlock() | |
| 155 | - | if away { | |
| 156 | - | // A move queued before Hide: the pointer is elsewhere. | |
| 157 | - | } else if c.hidden { | |
| 158 | - | // A move shows it again, even before its first image. | |
| 159 | - | c.hidden = false | |
| 160 | - | _ = c.apply() | |
| 161 | - | } else if c.shown && c.buf != nil { | |
| 162 | - | x, y := c.position() | |
| 163 | - | v := modeCursor2{flags: cursorMove, crtcID: c.crtc, x: int32(x - c.hotX), y: int32(y - c.hotY)} | |
| 164 | - | // Errors while another session owns the card (VT switched | |
| 165 | - | // away) are ignored; Reapply restores the cursor. | |
| 166 | - | start := time.Now() | |
| 167 | - | _ = c.plane.Set(&v) | |
| 168 | - | d := time.Since(start) | |
| 169 | - | c.mu.Lock() | |
| 170 | - | c.stats.Ioctls++ | |
| 171 | - | c.stats.MaxIoctl = max(c.stats.MaxIoctl, d) | |
| 172 | - | c.mu.Unlock() | |
| 173 | - | } | |
| 174 | - | c.io.Unlock() | |
| 175 | - | } | |
| 176 | - | } | |
| 177 | - | }() | |
| 72 | + | // Move places the hotspot at physical (x, y). It never blocks. | |
| 73 | + | func (c *Cursor) Move(x, y float64) { | |
| 74 | + | c.mu.Lock() | |
| 75 | + | c.x, c.y = int(x), int(y) | |
| 76 | + | c.away = false | |
| 77 | + | c.moves++ | |
| 78 | + | c.mu.Unlock() | |
| 79 | + | c.poke() | |
| 178 | 80 | } | |
| 179 | 81 | ||
| 180 | - | func (c *Cursor) position() (int, int) { | |
| 82 | + | // Hide removes the cursor from the output until the next move (the pointer | |
| 83 | + | // is on another output). | |
| 84 | + | func (c *Cursor) Hide() { | |
| 181 | 85 | c.mu.Lock() | |
| 182 | - | defer c.mu.Unlock() | |
| 183 | - | return c.x, c.y | |
| 86 | + | c.away = true | |
| 87 | + | c.mu.Unlock() | |
| 88 | + | c.poke() | |
| 184 | 89 | } | |
| 185 | 90 | ||
| 186 | - | // Limit is the largest image side the cursor plane accepts. | |
| 187 | - | func (c *Cursor) Limit() int { return c.size } | |
| 91 | + | func (c *Cursor) poke() { | |
| 92 | + | select { | |
| 93 | + | case c.wake <- struct{}{}: | |
| 94 | + | default: // a wake is queued; the output reads the latest state | |
| 95 | + | } | |
| 96 | + | } | |
| 188 | 97 | ||
| 189 | - | // SetImage shows premultiplied ARGB8888 pixels (w×h, w*4 per row) with the | |
| 190 | - | // click point at (hotX, hotY). Larger images are cropped to the plane; an | |
| 191 | - | // empty image hides the cursor (a client asked for none). | |
| 192 | - | func (c *Cursor) SetImage(pixels []byte, w, h, hotX, hotY int) error { | |
| 193 | - | c.io.Lock() | |
| 194 | - | defer c.io.Unlock() | |
| 195 | - | if c.buf == nil { | |
| 196 | - | return nil | |
| 98 | + | // setup allocates the two cursor images and their framebuffers. | |
| 99 | + | func (c *Cursor) setup(k kms, r ports.Renderer) error { | |
| 100 | + | bufs, err := r.CursorBuffers(c.size) | |
| 101 | + | if err != nil { | |
| 102 | + | return err | |
| 197 | 103 | } | |
| 198 | - | if w <= 0 || h <= 0 || len(pixels) < w*h*4 { | |
| 199 | - | wasShown := c.shown | |
| 200 | - | c.shown = false | |
| 201 | - | if !wasShown || c.hidden { | |
| 202 | - | return nil | |
| 104 | + | for i := range bufs { | |
| 105 | + | if err == nil { | |
| 106 | + | c.fbs[i], err = k.addFB(&bufs[i], fourccARGB) | |
| 107 | + | } | |
| 108 | + | for _, p := range bufs[i].Planes { | |
| 109 | + | p.File.Close() | |
| 203 | 110 | } | |
| 204 | - | v := modeCursor2{flags: cursorBO, crtcID: c.crtc} | |
| 205 | - | return c.plane.Set(&v) | |
| 206 | 111 | } | |
| 207 | - | pitch := int(c.buf.pitch) | |
| 208 | - | clear(c.buf.mem) | |
| 209 | - | for y := range min(h, c.size) { | |
| 210 | - | row := pixels[y*w*4:] | |
| 211 | - | copy(c.buf.mem[y*pitch:y*pitch+min(w, c.size)*4], row[:min(w, c.size)*4]) | |
| 112 | + | if err != nil { | |
| 113 | + | c.free(k) | |
| 212 | 114 | } | |
| 213 | - | c.hotX, c.hotY, c.shown = hotX, hotY, true | |
| 214 | - | return c.apply() | |
| 115 | + | return err | |
| 215 | 116 | } | |
| 216 | 117 | ||
| 217 | - | // apply (re)attaches the buffer at the current position. Callers hold c.io. | |
| 218 | - | func (c *Cursor) apply() error { | |
| 219 | - | if !c.shown || c.hidden || c.buf == nil { | |
| 118 | + | // setImage loads premultiplied ARGB8888 pixels into the image not on | |
| 119 | + | // screen; an empty image hides the cursor (a client asked for none). | |
| 120 | + | func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) error { | |
| 121 | + | if w <= 0 || h <= 0 || len(pixels) < w*h*4 { | |
| 122 | + | c.image = false | |
| 220 | 123 | return nil | |
| 221 | 124 | } | |
| 222 | - | x, y := c.position() | |
| 223 | - | v := modeCursor2{flags: cursorBO | cursorMove, crtcID: c.crtc, x: int32(x - c.hotX), y: int32(y - c.hotY), width: uint32(c.size), height: uint32(c.size), handle: c.buf.handle, hotX: int32(c.hotX), hotY: int32(c.hotY)} | |
| 224 | - | return c.plane.Set(&v) | |
| 225 | - | } | |
| 226 | - | ||
| 227 | - | // Move places the hotspot at physical (x, y). It never blocks on the GPU: | |
| 228 | - | // the worker applies the latest position. | |
| 229 | - | func (c *Cursor) Move(x, y float64) { | |
| 230 | - | c.mu.Lock() | |
| 231 | - | c.x, c.y = int(x), int(y) | |
| 232 | - | c.away = false | |
| 233 | - | c.stats.Moves++ | |
| 234 | - | c.mu.Unlock() | |
| 235 | - | select { | |
| 236 | - | case c.wake <- struct{}{}: | |
| 237 | - | default: // a move is already queued; it will read this position | |
| 125 | + | if c.fbs[0] == 0 { | |
| 126 | + | return errors.New("no cursor images") | |
| 127 | + | } | |
| 128 | + | next := 1 - c.cur | |
| 129 | + | if err := r.WriteCursor(next, pixels, w, h); err != nil { | |
| 130 | + | return err | |
| 238 | 131 | } | |
| 132 | + | c.cur, c.image, c.hotX, c.hotY = next, true, hotX, hotY | |
| 133 | + | return nil | |
| 239 | 134 | } | |
| 240 | 135 | ||
| 241 | - | // Hide removes the cursor from the output until the next move (the pointer | |
| 242 | - | // is on another output). | |
| 243 | - | func (c *Cursor) Hide() { | |
| 244 | - | c.io.Lock() | |
| 245 | - | defer c.io.Unlock() | |
| 136 | + | // desired is what the cursor plane should show now. | |
| 137 | + | func (c *Cursor) desired() cursorState { | |
| 246 | 138 | c.mu.Lock() | |
| 247 | - | c.away = true | |
| 139 | + | x, y, away := c.x, c.y, c.away | |
| 248 | 140 | c.mu.Unlock() | |
| 249 | - | if c.buf == nil || c.hidden { | |
| 250 | - | return | |
| 141 | + | if !c.image || away || c.fbs[c.cur] == 0 { | |
| 142 | + | return cursorState{} | |
| 251 | 143 | } | |
| 252 | - | c.hidden = true | |
| 253 | - | v := modeCursor2{flags: cursorBO, crtcID: c.crtc} | |
| 254 | - | _ = c.plane.Set(&v) | |
| 255 | - | } | |
| 256 | - | ||
| 257 | - | // Reapply shows the cursor again after a modeset (VT switch back). | |
| 258 | - | func (c *Cursor) Reapply() error { | |
| 259 | - | c.io.Lock() | |
| 260 | - | defer c.io.Unlock() | |
| 261 | - | return c.apply() | |
| 144 | + | return cursorState{on: true, fb: c.fbs[c.cur], x: x - c.hotX, y: y - c.hotY} | |
| 262 | 145 | } | |
| 263 | 146 | ||
| 264 | - | // close stops the worker, hides the cursor and frees its buffer. | |
| 265 | - | func (c *Cursor) close() { | |
| 266 | - | if c.stop != nil { | |
| 267 | - | close(c.stop) | |
| 268 | - | <-c.done | |
| 269 | - | c.stop = nil | |
| 270 | - | } | |
| 271 | - | c.io.Lock() | |
| 272 | - | defer c.io.Unlock() | |
| 273 | - | if c.buf == nil { | |
| 147 | + | // props adds the cursor plane in state s to req. | |
| 148 | + | func (c *Cursor) props(req *atomicReq, crtc uint32, s cursorState) { | |
| 149 | + | p := c.plane | |
| 150 | + | if !s.on { | |
| 151 | + | req.set(p.id, p.prop("FB_ID"), 0) | |
| 152 | + | req.set(p.id, p.prop("CRTC_ID"), 0) | |
| 274 | 153 | return | |
| 275 | 154 | } | |
| 276 | - | // Best effort: at exit the card may already belong to another session. | |
| 277 | - | v := modeCursor2{flags: cursorBO, crtcID: c.crtc} | |
| 278 | - | _ = c.plane.Set(&v) | |
| 279 | - | c.buf.destroy(c.fd) | |
| 280 | - | c.buf, c.shown = nil, false | |
| 155 | + | size := uint64(c.size) | |
| 156 | + | req.set(p.id, p.prop("FB_ID"), uint64(s.fb)) | |
| 157 | + | req.set(p.id, p.prop("CRTC_ID"), uint64(crtc)) | |
| 158 | + | req.set(p.id, p.prop("SRC_X"), 0) | |
| 159 | + | req.set(p.id, p.prop("SRC_Y"), 0) | |
| 160 | + | req.set(p.id, p.prop("SRC_W"), size<<16) | |
| 161 | + | req.set(p.id, p.prop("SRC_H"), size<<16) | |
| 162 | + | req.set(p.id, p.prop("CRTC_X"), uint64(int64(s.x))) | |
| 163 | + | req.set(p.id, p.prop("CRTC_Y"), uint64(int64(s.y))) | |
| 164 | + | req.set(p.id, p.prop("CRTC_W"), size) | |
| 165 | + | req.set(p.id, p.prop("CRTC_H"), size) | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | func (c *Cursor) free(k kms) { | |
| 169 | + | for i, fb := range c.fbs { | |
| 170 | + | if fb != 0 { | |
| 171 | + | _ = k.rmFB(fb) | |
| 172 | + | c.fbs[i] = 0 | |
| 173 | + | } | |
| 174 | + | } | |
| 175 | + | c.image = false | |
| 281 | 176 | } | |
D internal/adapters/drm/cursor_plane_mock_test.go +0 -140
| @@ -1,140 +0,0 @@ | |||
| 1 | - | // Code generated by mockery; DO NOT EDIT. | |
| 2 | - | // github.com/vektra/mockery | |
| 3 | - | // template: testify | |
| 4 | - | ||
| 5 | - | package drm | |
| 6 | - | ||
| 7 | - | import ( | |
| 8 | - | mock "github.com/stretchr/testify/mock" | |
| 9 | - | ) | |
| 10 | - | ||
| 11 | - | // newMockcursorPlane creates a new instance of mockcursorPlane. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 12 | - | // The first argument is typically a *testing.T value. | |
| 13 | - | func newMockcursorPlane(t interface { | |
| 14 | - | mock.TestingT | |
| 15 | - | Cleanup(func()) | |
| 16 | - | }) *mockcursorPlane { | |
| 17 | - | if helper, ok := t.(interface{ Helper() }); ok { | |
| 18 | - | helper.Helper() | |
| 19 | - | } | |
| 20 | - | ||
| 21 | - | mock := &mockcursorPlane{} | |
| 22 | - | mock.Mock.Test(t) | |
| 23 | - | ||
| 24 | - | t.Cleanup(func() { | |
| 25 | - | if helper, ok := t.(interface{ Helper() }); ok { | |
| 26 | - | helper.Helper() | |
| 27 | - | } | |
| 28 | - | mock.AssertExpectations(t) | |
| 29 | - | }) | |
| 30 | - | ||
| 31 | - | return mock | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | // mockcursorPlane is an autogenerated mock type for the cursorPlane type | |
| 35 | - | type mockcursorPlane struct { | |
| 36 | - | mock.Mock | |
| 37 | - | } | |
| 38 | - | ||
| 39 | - | type mockcursorPlane_Expecter struct { | |
| 40 | - | mock *mock.Mock | |
| 41 | - | } | |
| 42 | - | ||
| 43 | - | func (_m *mockcursorPlane) EXPECT() *mockcursorPlane_Expecter { | |
| 44 | - | return &mockcursorPlane_Expecter{mock: &_m.Mock} | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | // Set provides a mock function for the type mockcursorPlane | |
| 48 | - | func (_mock *mockcursorPlane) Set(v *modeCursor2) error { | |
| 49 | - | ret := _mock.Called(v) | |
| 50 | - | ||
| 51 | - | if len(ret) == 0 { | |
| 52 | - | panic("no return value specified for Set") | |
| 53 | - | } | |
| 54 | - | ||
| 55 | - | var r0 error | |
| 56 | - | if returnFunc, ok := ret.Get(0).(func(*modeCursor2) error); ok { | |
| 57 | - | r0 = returnFunc(v) | |
| 58 | - | } else { | |
| 59 | - | r0 = ret.Error(0) | |
| 60 | - | } | |
| 61 | - | return r0 | |
| 62 | - | } | |
| 63 | - | ||
| 64 | - | // mockcursorPlane_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' | |
| 65 | - | type mockcursorPlane_Set_Call struct { | |
| 66 | - | *mock.Call | |
| 67 | - | } | |
| 68 | - | ||
| 69 | - | // Set is a helper method to define mock.On call | |
| 70 | - | // - v *modeCursor2 | |
| 71 | - | func (_e *mockcursorPlane_Expecter) Set(v any) *mockcursorPlane_Set_Call { | |
| 72 | - | return &mockcursorPlane_Set_Call{Call: _e.mock.On("Set", v)} | |
| 73 | - | } | |
| 74 | - | ||
| 75 | - | func (_c *mockcursorPlane_Set_Call) Run(run func(v *modeCursor2)) *mockcursorPlane_Set_Call { | |
| 76 | - | _c.Call.Run(func(args mock.Arguments) { | |
| 77 | - | var arg0 *modeCursor2 | |
| 78 | - | if args[0] != nil { | |
| 79 | - | arg0 = args[0].(*modeCursor2) | |
| 80 | - | } | |
| 81 | - | run( | |
| 82 | - | arg0, | |
| 83 | - | ) | |
| 84 | - | }) | |
| 85 | - | return _c | |
| 86 | - | } | |
| 87 | - | ||
| 88 | - | func (_c *mockcursorPlane_Set_Call) Return(err error) *mockcursorPlane_Set_Call { | |
| 89 | - | _c.Call.Return(err) | |
| 90 | - | return _c | |
| 91 | - | } | |
| 92 | - | ||
| 93 | - | func (_c *mockcursorPlane_Set_Call) RunAndReturn(run func(v *modeCursor2) error) *mockcursorPlane_Set_Call { | |
| 94 | - | _c.Call.Return(run) | |
| 95 | - | return _c | |
| 96 | - | } | |
| 97 | - | ||
| 98 | - | // WaitVblank provides a mock function for the type mockcursorPlane | |
| 99 | - | func (_mock *mockcursorPlane) WaitVblank() error { | |
| 100 | - | ret := _mock.Called() | |
| 101 | - | ||
| 102 | - | if len(ret) == 0 { | |
| 103 | - | panic("no return value specified for WaitVblank") | |
| 104 | - | } | |
| 105 | - | ||
| 106 | - | var r0 error | |
| 107 | - | if returnFunc, ok := ret.Get(0).(func() error); ok { | |
| 108 | - | r0 = returnFunc() | |
| 109 | - | } else { | |
| 110 | - | r0 = ret.Error(0) | |
| 111 | - | } | |
| 112 | - | return r0 | |
| 113 | - | } | |
| 114 | - | ||
| 115 | - | // mockcursorPlane_WaitVblank_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaitVblank' | |
| 116 | - | type mockcursorPlane_WaitVblank_Call struct { | |
| 117 | - | *mock.Call | |
| 118 | - | } | |
| 119 | - | ||
| 120 | - | // WaitVblank is a helper method to define mock.On call | |
| 121 | - | func (_e *mockcursorPlane_Expecter) WaitVblank() *mockcursorPlane_WaitVblank_Call { | |
| 122 | - | return &mockcursorPlane_WaitVblank_Call{Call: _e.mock.On("WaitVblank")} | |
| 123 | - | } | |
| 124 | - | ||
| 125 | - | func (_c *mockcursorPlane_WaitVblank_Call) Run(run func()) *mockcursorPlane_WaitVblank_Call { | |
| 126 | - | _c.Call.Run(func(args mock.Arguments) { | |
| 127 | - | run() | |
| 128 | - | }) | |
| 129 | - | return _c | |
| 130 | - | } | |
| 131 | - | ||
| 132 | - | func (_c *mockcursorPlane_WaitVblank_Call) Return(err error) *mockcursorPlane_WaitVblank_Call { | |
| 133 | - | _c.Call.Return(err) | |
| 134 | - | return _c | |
| 135 | - | } | |
| 136 | - | ||
| 137 | - | func (_c *mockcursorPlane_WaitVblank_Call) RunAndReturn(run func() error) *mockcursorPlane_WaitVblank_Call { | |
| 138 | - | _c.Call.Return(run) | |
| 139 | - | return _c | |
| 140 | - | } | |
D internal/adapters/drm/cursor_test.go +0 -195
| @@ -1,195 +0,0 @@ | |||
| 1 | - | package drm | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "sync" | |
| 5 | - | "testing" | |
| 6 | - | "time" | |
| 7 | - | "unsafe" | |
| 8 | - | ||
| 9 | - | "github.com/stretchr/testify/mock" | |
| 10 | - | ) | |
| 11 | - | ||
| 12 | - | // The ioctl numbers encode the struct size; they must match the kernel ABI. | |
| 13 | - | func TestCursorABI(t *testing.T) { | |
| 14 | - | if s := unsafe.Sizeof(modeCursor2{}); s != 36 || ioctlCursor2>>16&0x3fff != 36 { | |
| 15 | - | t.Fatalf("drm_mode_cursor2 size %d", s) | |
| 16 | - | } | |
| 17 | - | if s := unsafe.Sizeof(getCap{}); s != 16 || ioctlGetCap>>16&0x3fff != 16 { | |
| 18 | - | t.Fatalf("drm_get_cap size %d", s) | |
| 19 | - | } | |
| 20 | - | if s := unsafe.Sizeof(waitVblank{}); s != 24 || ioctlVblank>>16&0x3fff != 24 { | |
| 21 | - | t.Fatalf("drm_wait_vblank size %d", s) | |
| 22 | - | } | |
| 23 | - | } | |
| 24 | - | ||
| 25 | - | // testCursor returns a shown cursor on a mocked plane; the worker is started. | |
| 26 | - | func testCursor(t *testing.T) (*Cursor, *mockcursorPlane) { | |
| 27 | - | plane := newMockcursorPlane(t) | |
| 28 | - | c := &Cursor{fd: -1, buf: &dumbBuffer{}, size: 64, shown: true, plane: plane} | |
| 29 | - | return c, plane | |
| 30 | - | } | |
| 31 | - | ||
| 32 | - | // The cursor moves once per vblank, to the latest position: moving it during | |
| 33 | - | // scanout tears it into doubled images. | |
| 34 | - | func TestCursorMovesOncePerVblank(t *testing.T) { | |
| 35 | - | c, plane := testCursor(t) | |
| 36 | - | vblank := make(chan struct{}) | |
| 37 | - | moved := make(chan modeCursor2, 16) | |
| 38 | - | plane.EXPECT().WaitVblank().RunAndReturn(func() error { <-vblank; return nil }) | |
| 39 | - | plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error { moved <- *v; return nil }) | |
| 40 | - | c.start() | |
| 41 | - | defer func() { close(vblank); c.close() }() | |
| 42 | - | for i := range 100 { | |
| 43 | - | c.Move(float64(i), 0) | |
| 44 | - | } | |
| 45 | - | select { | |
| 46 | - | case v := <-moved: | |
| 47 | - | t.Fatalf("moved to %d before vblank", v.x) | |
| 48 | - | case <-time.After(20 * time.Millisecond): | |
| 49 | - | } | |
| 50 | - | vblank <- struct{}{} | |
| 51 | - | if v := <-moved; v.x != 99 || v.flags != cursorMove { | |
| 52 | - | t.Fatalf("applied %+v, want a move to the latest x=99", v) | |
| 53 | - | } | |
| 54 | - | select { | |
| 55 | - | case v := <-moved: | |
| 56 | - | t.Fatalf("extra move to %d without new input", v.x) | |
| 57 | - | case <-time.After(20 * time.Millisecond): | |
| 58 | - | } | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | // Move must never wait for the plane, or libinput stops reading and the | |
| 62 | - | // kernel drops mouse reports. Moves made meanwhile are coalesced. | |
| 63 | - | func TestCursorMoveDoesNotBlockOnPlane(t *testing.T) { | |
| 64 | - | c, plane := testCursor(t) | |
| 65 | - | c.hotX, c.hotY = 3, 4 | |
| 66 | - | started, release := make(chan struct{}), make(chan struct{}) | |
| 67 | - | var once sync.Once | |
| 68 | - | moved := make(chan modeCursor2, 1000) | |
| 69 | - | plane.EXPECT().WaitVblank().RunAndReturn(func() error { | |
| 70 | - | once.Do(func() { close(started) }) | |
| 71 | - | <-release | |
| 72 | - | return nil | |
| 73 | - | }) | |
| 74 | - | plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error { moved <- *v; return nil }) | |
| 75 | - | c.start() | |
| 76 | - | defer c.close() | |
| 77 | - | c.Move(0, 0) | |
| 78 | - | <-started // the worker is now held in WaitVblank | |
| 79 | - | done := make(chan struct{}) | |
| 80 | - | go func() { | |
| 81 | - | for i := range 1000 { | |
| 82 | - | c.Move(float64(i), float64(2*i)) | |
| 83 | - | } | |
| 84 | - | close(done) | |
| 85 | - | }() | |
| 86 | - | select { | |
| 87 | - | case <-done: | |
| 88 | - | case <-time.After(2 * time.Second): | |
| 89 | - | t.Fatal("Move blocks while the plane waits for vblank") | |
| 90 | - | } | |
| 91 | - | close(release) | |
| 92 | - | for n := 1; ; n++ { | |
| 93 | - | v := <-moved | |
| 94 | - | if v.flags != cursorMove { | |
| 95 | - | t.Fatalf("flags %#x", v.flags) | |
| 96 | - | } | |
| 97 | - | if v.x == 999-3 && v.y == 1998-4 { | |
| 98 | - | if n >= 1000 { | |
| 99 | - | t.Fatalf("%d moves applied; want coalesced", n) | |
| 100 | - | } | |
| 101 | - | return | |
| 102 | - | } | |
| 103 | - | } | |
| 104 | - | } | |
| 105 | - | ||
| 106 | - | func TestCursorCloseStopsWorker(t *testing.T) { | |
| 107 | - | c, plane := testCursor(t) | |
| 108 | - | plane.EXPECT().WaitVblank().Return(nil).Maybe() | |
| 109 | - | plane.EXPECT().Set(mock.Anything).Return(nil) | |
| 110 | - | c.start() | |
| 111 | - | c.Move(1, 2) | |
| 112 | - | c.close() | |
| 113 | - | c.close() // idempotent | |
| 114 | - | c.Move(3, 4) // after close: no worker, must not block or panic | |
| 115 | - | } | |
| 116 | - | ||
| 117 | - | func TestCursorStats(t *testing.T) { | |
| 118 | - | c, plane := testCursor(t) | |
| 119 | - | applied := make(chan struct{}, 16) | |
| 120 | - | plane.EXPECT().WaitVblank().Return(nil) | |
| 121 | - | plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error { | |
| 122 | - | if v.flags == cursorMove { | |
| 123 | - | applied <- struct{}{} | |
| 124 | - | } | |
| 125 | - | return nil | |
| 126 | - | }) | |
| 127 | - | c.start() | |
| 128 | - | defer c.close() | |
| 129 | - | c.Move(1, 1) | |
| 130 | - | <-applied | |
| 131 | - | c.Move(2, 2) | |
| 132 | - | <-applied | |
| 133 | - | // Set signals before the worker counts the ioctl. | |
| 134 | - | s := c.TakeStats() | |
| 135 | - | if s.Moves != 2 || s.Ioctls < 1 || s.Ioctls > 2 { | |
| 136 | - | t.Fatalf("stats %+v", s) | |
| 137 | - | } | |
| 138 | - | if s := c.TakeStats(); s.Moves != 0 { | |
| 139 | - | t.Fatalf("not reset: %+v", s) | |
| 140 | - | } | |
| 141 | - | } | |
| 142 | - | ||
| 143 | - | // A move still waiting for its vblank when the pointer leaves the output | |
| 144 | - | // must not show the cursor again. | |
| 145 | - | func TestCursorHideDropsQueuedMove(t *testing.T) { | |
| 146 | - | c, plane := testCursor(t) | |
| 147 | - | vblank := make(chan struct{}) | |
| 148 | - | set := make(chan modeCursor2, 16) | |
| 149 | - | plane.EXPECT().WaitVblank().RunAndReturn(func() error { <-vblank; return nil }) | |
| 150 | - | plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error { set <- *v; return nil }) | |
| 151 | - | c.start() | |
| 152 | - | defer func() { close(vblank); c.close() }() | |
| 153 | - | c.Move(10, 10) | |
| 154 | - | c.Hide() | |
| 155 | - | if v := <-set; v.handle != 0 || v.flags != cursorBO { | |
| 156 | - | t.Fatalf("hide: %+v", v) | |
| 157 | - | } | |
| 158 | - | vblank <- struct{}{} | |
| 159 | - | select { | |
| 160 | - | case v := <-set: | |
| 161 | - | t.Fatalf("queued move applied after hide: %+v", v) | |
| 162 | - | case <-time.After(20 * time.Millisecond): | |
| 163 | - | } | |
| 164 | - | // The next move shows it again. | |
| 165 | - | c.Move(20, 20) | |
| 166 | - | vblank <- struct{}{} | |
| 167 | - | if v := <-set; v.flags != cursorBO|cursorMove || v.x != 20 { | |
| 168 | - | t.Fatalf("show: %+v", v) | |
| 169 | - | } | |
| 170 | - | } | |
| 171 | - | ||
| 172 | - | // An empty image takes the buffer off the plane; the next image shows it. | |
| 173 | - | func TestCursorEmptyImageHides(t *testing.T) { | |
| 174 | - | c, plane := testCursor(t) | |
| 175 | - | c.buf.mem = make([]byte, 64*64*4) | |
| 176 | - | c.buf.pitch = 64 * 4 | |
| 177 | - | var sets []modeCursor2 | |
| 178 | - | plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error { sets = append(sets, *v); return nil }) | |
| 179 | - | if err := c.SetImage(nil, 0, 0, 0, 0); err != nil { | |
| 180 | - | t.Fatal(err) | |
| 181 | - | } | |
| 182 | - | if len(sets) != 1 || sets[0].flags != cursorBO || sets[0].handle != 0 || c.shown { | |
| 183 | - | t.Fatalf("hide: %+v shown=%v", sets, c.shown) | |
| 184 | - | } | |
| 185 | - | // Still hidden: no second ioctl. | |
| 186 | - | if err := c.SetImage(nil, 0, 0, 0, 0); err != nil || len(sets) != 1 { | |
| 187 | - | t.Fatalf("second hide: %+v %v", sets, err) | |
| 188 | - | } | |
| 189 | - | if err := c.SetImage(make([]byte, 4), 1, 1, 0, 0); err != nil { | |
| 190 | - | t.Fatal(err) | |
| 191 | - | } | |
| 192 | - | if len(sets) != 2 || sets[1].flags != cursorBO|cursorMove || !c.shown { | |
| 193 | - | t.Fatalf("show: %+v", sets) | |
| 194 | - | } | |
| 195 | - | } | |
M internal/adapters/drm/kms.go +1 -109
| @@ -1,8 +1,7 @@ | |||
| 1 | - | // Package drm drives KMS outputs with raw ioctls and dumb buffers. | |
| 1 | + | // Package drm drives KMS outputs with raw ioctls and atomic commits. | |
| 2 | 2 | package drm | |
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | - | "encoding/binary" | |
| 6 | 5 | "fmt" | |
| 7 | 6 | "math" | |
| 8 | 7 | "unsafe" | |
| @@ -14,18 +13,10 @@ import ( | |||
| 14 | 13 | const ( | |
| 15 | 14 | ioctlGetResources = 0xC04064A0 | |
| 16 | 15 | ioctlGetCrtc = 0xC06864A1 | |
| 17 | - | ioctlSetCrtc = 0xC06864A2 | |
| 18 | 16 | ioctlGetEncoder = 0xC01464A6 | |
| 19 | 17 | ioctlGetConnector = 0xC05064A7 | |
| 20 | - | ioctlAddFB = 0xC01C64AE | |
| 21 | 18 | ioctlRmFB = 0xC00464AF | |
| 22 | - | ioctlPageFlip = 0xC01864B0 | |
| 23 | - | ioctlCreateDumb = 0xC02064B2 | |
| 24 | - | ioctlMapDumb = 0xC01064B3 | |
| 25 | - | ioctlDestroyDumb = 0xC00464B4 | |
| 26 | 19 | ||
| 27 | - | pageFlipEvent = 1 | |
| 28 | - | pageFlipAsync = 2 | |
| 29 | 20 | eventFlipDone = 2 | |
| 30 | 21 | modeTypePrefered = 1 << 3 | |
| 31 | 22 | connected = 1 | |
| @@ -72,24 +63,6 @@ type modeCrtc struct { | |||
| 72 | 63 | mode modeInfo | |
| 73 | 64 | } | |
| 74 | 65 | ||
| 75 | - | type pageFlip struct { | |
| 76 | - | crtcID, fbID, flags, reserved uint32 | |
| 77 | - | userData uint64 | |
| 78 | - | } | |
| 79 | - | ||
| 80 | - | type createDumb struct { | |
| 81 | - | height, width, bpp, flags uint32 | |
| 82 | - | handle, pitch uint32 | |
| 83 | - | size uint64 | |
| 84 | - | } | |
| 85 | - | ||
| 86 | - | type mapDumb struct { | |
| 87 | - | handle, pad uint32 | |
| 88 | - | offset uint64 | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | type fbCmd struct{ fbID, width, height, pitch, bpp, depth, handle uint32 } | |
| 92 | - | ||
| 93 | 66 | func ioctl(fd int, req uintptr, arg unsafe.Pointer) error { | |
| 94 | 67 | for { | |
| 95 | 68 | _, _, e := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg)) | |
| @@ -261,89 +234,8 @@ func pickCrtc(fd int, c connector, crtcs []uint32, used map[uint32]bool) (uint32 | |||
| 261 | 234 | return 0, fmt.Errorf("no free CRTC for %s", c.name) | |
| 262 | 235 | } | |
| 263 | 236 | ||
| 264 | - | type dumbBuffer struct { | |
| 265 | - | handle, fbID, pitch uint32 | |
| 266 | - | mem []byte | |
| 267 | - | } | |
| 268 | - | ||
| 269 | - | func newDumb(fd int, w, h int) (*dumbBuffer, error) { | |
| 270 | - | c := createDumb{width: uint32(w), height: uint32(h), bpp: 32} | |
| 271 | - | if err := ioctl(fd, ioctlCreateDumb, unsafe.Pointer(&c)); err != nil { | |
| 272 | - | return nil, fmt.Errorf("create dumb: %w", err) | |
| 273 | - | } | |
| 274 | - | b := &dumbBuffer{handle: c.handle, pitch: c.pitch} | |
| 275 | - | f := fbCmd{width: uint32(w), height: uint32(h), pitch: c.pitch, bpp: 32, depth: 24, handle: c.handle} | |
| 276 | - | if err := ioctl(fd, ioctlAddFB, unsafe.Pointer(&f)); err != nil { | |
| 277 | - | b.destroy(fd) | |
| 278 | - | return nil, fmt.Errorf("add fb: %w", err) | |
| 279 | - | } | |
| 280 | - | b.fbID = f.fbID | |
| 281 | - | m := mapDumb{handle: c.handle} | |
| 282 | - | if err := ioctl(fd, ioctlMapDumb, unsafe.Pointer(&m)); err != nil { | |
| 283 | - | b.destroy(fd) | |
| 284 | - | return nil, fmt.Errorf("map dumb: %w", err) | |
| 285 | - | } | |
| 286 | - | mem, err := unix.Mmap(fd, int64(m.offset), int(c.size), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED) | |
| 287 | - | if err != nil { | |
| 288 | - | b.destroy(fd) | |
| 289 | - | return nil, fmt.Errorf("mmap dumb: %w", err) | |
| 290 | - | } | |
| 291 | - | b.mem = mem | |
| 292 | - | return b, nil | |
| 293 | - | } | |
| 294 | - | ||
| 295 | - | func (b *dumbBuffer) destroy(fd int) { | |
| 296 | - | if b.mem != nil { | |
| 297 | - | _ = unix.Munmap(b.mem) | |
| 298 | - | b.mem = nil | |
| 299 | - | } | |
| 300 | - | if b.fbID != 0 { | |
| 301 | - | id := b.fbID | |
| 302 | - | _ = ioctl(fd, ioctlRmFB, unsafe.Pointer(&id)) | |
| 303 | - | b.fbID = 0 | |
| 304 | - | } | |
| 305 | - | h := b.handle | |
| 306 | - | _ = ioctl(fd, ioctlDestroyDumb, unsafe.Pointer(&h)) | |
| 307 | - | } | |
| 308 | - | ||
| 309 | - | func setCrtc(fd int, crtc, conn, fb uint32, mode *modeInfo) error { | |
| 310 | - | ids := []uint32{conn} | |
| 311 | - | c := modeCrtc{crtcID: crtc, fbID: fb, countConnectors: 1, setConnectors: unsafe.Pointer(&ids[0])} | |
| 312 | - | if mode != nil { | |
| 313 | - | c.mode, c.modeValid = *mode, 1 | |
| 314 | - | } | |
| 315 | - | return ioctl(fd, ioctlSetCrtc, unsafe.Pointer(&c)) | |
| 316 | - | } | |
| 317 | - | ||
| 318 | 237 | func getCrtc(fd int, crtc uint32) (modeCrtc, error) { | |
| 319 | 238 | c := modeCrtc{crtcID: crtc} | |
| 320 | 239 | err := ioctl(fd, ioctlGetCrtc, unsafe.Pointer(&c)) | |
| 321 | 240 | return c, err | |
| 322 | 241 | } | |
| 323 | - | ||
| 324 | - | func flip(fd int, crtc, fb uint32, async bool) error { | |
| 325 | - | p := pageFlip{crtcID: crtc, fbID: fb, flags: pageFlipEvent} | |
| 326 | - | if async { | |
| 327 | - | p.flags |= pageFlipAsync | |
| 328 | - | } | |
| 329 | - | return ioctl(fd, ioctlPageFlip, unsafe.Pointer(&p)) | |
| 330 | - | } | |
| 331 | - | ||
| 332 | - | // flipCrtcs parses DRM events read from the card fd and returns the CRTC of | |
| 333 | - | // each completed page flip. | |
| 334 | - | func flipCrtcs(buf []byte) []uint32 { | |
| 335 | - | var out []uint32 | |
| 336 | - | for len(buf) >= 8 { | |
| 337 | - | typ := binary.LittleEndian.Uint32(buf) | |
| 338 | - | length := int(binary.LittleEndian.Uint32(buf[4:])) | |
| 339 | - | if length < 8 || length > len(buf) { | |
| 340 | - | break | |
| 341 | - | } | |
| 342 | - | // struct drm_event_vblank: base(8) user_data(8) sec usec sequence crtc_id. | |
| 343 | - | if typ == eventFlipDone && length >= 32 { | |
| 344 | - | out = append(out, binary.LittleEndian.Uint32(buf[28:])) | |
| 345 | - | } | |
| 346 | - | buf = buf[length:] | |
| 347 | - | } | |
| 348 | - | return out | |
| 349 | - | } | |
A internal/adapters/drm/kms_mock_test.go +522 -0
| @@ -0,0 +1,522 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package drm | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "github.com/bnema/nefertty/internal/ports" | |
| 9 | + | mock "github.com/stretchr/testify/mock" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // newMockkms creates a new instance of mockkms. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 13 | + | // The first argument is typically a *testing.T value. | |
| 14 | + | func newMockkms(t interface { | |
| 15 | + | mock.TestingT | |
| 16 | + | Cleanup(func()) | |
| 17 | + | }) *mockkms { | |
| 18 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 19 | + | helper.Helper() | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | mock := &mockkms{} | |
| 23 | + | mock.Mock.Test(t) | |
| 24 | + | ||
| 25 | + | t.Cleanup(func() { | |
| 26 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 27 | + | helper.Helper() | |
| 28 | + | } | |
| 29 | + | mock.AssertExpectations(t) | |
| 30 | + | }) | |
| 31 | + | ||
| 32 | + | return mock | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // mockkms is an autogenerated mock type for the kms type | |
| 36 | + | type mockkms struct { | |
| 37 | + | mock.Mock | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | type mockkms_Expecter struct { | |
| 41 | + | mock *mock.Mock | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | func (_m *mockkms) EXPECT() *mockkms_Expecter { | |
| 45 | + | return &mockkms_Expecter{mock: &_m.Mock} | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // addFB provides a mock function for the type mockkms | |
| 49 | + | func (_mock *mockkms) addFB(b *ports.DMABuf, format uint32) (uint32, error) { | |
| 50 | + | ret := _mock.Called(b, format) | |
| 51 | + | ||
| 52 | + | if len(ret) == 0 { | |
| 53 | + | panic("no return value specified for addFB") | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | var r0 uint32 | |
| 57 | + | var r1 error | |
| 58 | + | if returnFunc, ok := ret.Get(0).(func(*ports.DMABuf, uint32) (uint32, error)); ok { | |
| 59 | + | return returnFunc(b, format) | |
| 60 | + | } | |
| 61 | + | if returnFunc, ok := ret.Get(0).(func(*ports.DMABuf, uint32) uint32); ok { | |
| 62 | + | r0 = returnFunc(b, format) | |
| 63 | + | } else { | |
| 64 | + | r0 = ret.Get(0).(uint32) | |
| 65 | + | } | |
| 66 | + | if returnFunc, ok := ret.Get(1).(func(*ports.DMABuf, uint32) error); ok { | |
| 67 | + | r1 = returnFunc(b, format) | |
| 68 | + | } else { | |
| 69 | + | r1 = ret.Error(1) | |
| 70 | + | } | |
| 71 | + | return r0, r1 | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // mockkms_addFB_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'addFB' | |
| 75 | + | type mockkms_addFB_Call struct { | |
| 76 | + | *mock.Call | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // addFB is a helper method to define mock.On call | |
| 80 | + | // - b *ports.DMABuf | |
| 81 | + | // - format uint32 | |
| 82 | + | func (_e *mockkms_Expecter) addFB(b any, format any) *mockkms_addFB_Call { | |
| 83 | + | return &mockkms_addFB_Call{Call: _e.mock.On("addFB", b, format)} | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | func (_c *mockkms_addFB_Call) Run(run func(b *ports.DMABuf, format uint32)) *mockkms_addFB_Call { | |
| 87 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 88 | + | var arg0 *ports.DMABuf | |
| 89 | + | if args[0] != nil { | |
| 90 | + | arg0 = args[0].(*ports.DMABuf) | |
| 91 | + | } | |
| 92 | + | var arg1 uint32 | |
| 93 | + | if args[1] != nil { | |
| 94 | + | arg1 = args[1].(uint32) | |
| 95 | + | } | |
| 96 | + | run( | |
| 97 | + | arg0, | |
| 98 | + | arg1, | |
| 99 | + | ) | |
| 100 | + | }) | |
| 101 | + | return _c | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | func (_c *mockkms_addFB_Call) Return(v uint32, err error) *mockkms_addFB_Call { | |
| 105 | + | _c.Call.Return(v, err) | |
| 106 | + | return _c | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | func (_c *mockkms_addFB_Call) RunAndReturn(run func(b *ports.DMABuf, format uint32) (uint32, error)) *mockkms_addFB_Call { | |
| 110 | + | _c.Call.Return(run) | |
| 111 | + | return _c | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // commit provides a mock function for the type mockkms | |
| 115 | + | func (_mock *mockkms) commit(req *atomicReq, flags uint32, userData uint64) error { | |
| 116 | + | ret := _mock.Called(req, flags, userData) | |
| 117 | + | ||
| 118 | + | if len(ret) == 0 { | |
| 119 | + | panic("no return value specified for commit") | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | var r0 error | |
| 123 | + | if returnFunc, ok := ret.Get(0).(func(*atomicReq, uint32, uint64) error); ok { | |
| 124 | + | r0 = returnFunc(req, flags, userData) | |
| 125 | + | } else { | |
| 126 | + | r0 = ret.Error(0) | |
| 127 | + | } | |
| 128 | + | return r0 | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // mockkms_commit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'commit' | |
| 132 | + | type mockkms_commit_Call struct { | |
| 133 | + | *mock.Call | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | // commit is a helper method to define mock.On call | |
| 137 | + | // - req *atomicReq | |
| 138 | + | // - flags uint32 | |
| 139 | + | // - userData uint64 | |
| 140 | + | func (_e *mockkms_Expecter) commit(req any, flags any, userData any) *mockkms_commit_Call { | |
| 141 | + | return &mockkms_commit_Call{Call: _e.mock.On("commit", req, flags, userData)} | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | func (_c *mockkms_commit_Call) Run(run func(req *atomicReq, flags uint32, userData uint64)) *mockkms_commit_Call { | |
| 145 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 146 | + | var arg0 *atomicReq | |
| 147 | + | if args[0] != nil { | |
| 148 | + | arg0 = args[0].(*atomicReq) | |
| 149 | + | } | |
| 150 | + | var arg1 uint32 | |
| 151 | + | if args[1] != nil { | |
| 152 | + | arg1 = args[1].(uint32) | |
| 153 | + | } | |
| 154 | + | var arg2 uint64 | |
| 155 | + | if args[2] != nil { | |
| 156 | + | arg2 = args[2].(uint64) | |
| 157 | + | } | |
| 158 | + | run( | |
| 159 | + | arg0, | |
| 160 | + | arg1, | |
| 161 | + | arg2, | |
| 162 | + | ) | |
| 163 | + | }) | |
| 164 | + | return _c | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | func (_c *mockkms_commit_Call) Return(err error) *mockkms_commit_Call { | |
| 168 | + | _c.Call.Return(err) | |
| 169 | + | return _c | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | func (_c *mockkms_commit_Call) RunAndReturn(run func(req *atomicReq, flags uint32, userData uint64) error) *mockkms_commit_Call { | |
| 173 | + | _c.Call.Return(run) | |
| 174 | + | return _c | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | // createBlob provides a mock function for the type mockkms | |
| 178 | + | func (_mock *mockkms) createBlob(data []byte) (uint32, error) { | |
| 179 | + | ret := _mock.Called(data) | |
| 180 | + | ||
| 181 | + | if len(ret) == 0 { | |
| 182 | + | panic("no return value specified for createBlob") | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | var r0 uint32 | |
| 186 | + | var r1 error | |
| 187 | + | if returnFunc, ok := ret.Get(0).(func([]byte) (uint32, error)); ok { | |
| 188 | + | return returnFunc(data) | |
| 189 | + | } | |
| 190 | + | if returnFunc, ok := ret.Get(0).(func([]byte) uint32); ok { | |
| 191 | + | r0 = returnFunc(data) | |
| 192 | + | } else { | |
| 193 | + | r0 = ret.Get(0).(uint32) | |
| 194 | + | } | |
| 195 | + | if returnFunc, ok := ret.Get(1).(func([]byte) error); ok { | |
| 196 | + | r1 = returnFunc(data) | |
| 197 | + | } else { | |
| 198 | + | r1 = ret.Error(1) | |
| 199 | + | } | |
| 200 | + | return r0, r1 | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // mockkms_createBlob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'createBlob' | |
| 204 | + | type mockkms_createBlob_Call struct { | |
| 205 | + | *mock.Call | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | // createBlob is a helper method to define mock.On call | |
| 209 | + | // - data []byte | |
| 210 | + | func (_e *mockkms_Expecter) createBlob(data any) *mockkms_createBlob_Call { | |
| 211 | + | return &mockkms_createBlob_Call{Call: _e.mock.On("createBlob", data)} | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | func (_c *mockkms_createBlob_Call) Run(run func(data []byte)) *mockkms_createBlob_Call { | |
| 215 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 216 | + | var arg0 []byte | |
| 217 | + | if args[0] != nil { | |
| 218 | + | arg0 = args[0].([]byte) | |
| 219 | + | } | |
| 220 | + | run( | |
| 221 | + | arg0, | |
| 222 | + | ) | |
| 223 | + | }) | |
| 224 | + | return _c | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | func (_c *mockkms_createBlob_Call) Return(v uint32, err error) *mockkms_createBlob_Call { | |
| 228 | + | _c.Call.Return(v, err) | |
| 229 | + | return _c | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | func (_c *mockkms_createBlob_Call) RunAndReturn(run func(data []byte) (uint32, error)) *mockkms_createBlob_Call { | |
| 233 | + | _c.Call.Return(run) | |
| 234 | + | return _c | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | // destroyBlob provides a mock function for the type mockkms | |
| 238 | + | func (_mock *mockkms) destroyBlob(id uint32) error { | |
| 239 | + | ret := _mock.Called(id) | |
| 240 | + | ||
| 241 | + | if len(ret) == 0 { | |
| 242 | + | panic("no return value specified for destroyBlob") | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | var r0 error | |
| 246 | + | if returnFunc, ok := ret.Get(0).(func(uint32) error); ok { | |
| 247 | + | r0 = returnFunc(id) | |
| 248 | + | } else { | |
| 249 | + | r0 = ret.Error(0) | |
| 250 | + | } | |
| 251 | + | return r0 | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | // mockkms_destroyBlob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'destroyBlob' | |
| 255 | + | type mockkms_destroyBlob_Call struct { | |
| 256 | + | *mock.Call | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | // destroyBlob is a helper method to define mock.On call | |
| 260 | + | // - id uint32 | |
| 261 | + | func (_e *mockkms_Expecter) destroyBlob(id any) *mockkms_destroyBlob_Call { | |
| 262 | + | return &mockkms_destroyBlob_Call{Call: _e.mock.On("destroyBlob", id)} | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | func (_c *mockkms_destroyBlob_Call) Run(run func(id uint32)) *mockkms_destroyBlob_Call { | |
| 266 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 267 | + | var arg0 uint32 | |
| 268 | + | if args[0] != nil { | |
| 269 | + | arg0 = args[0].(uint32) | |
| 270 | + | } | |
| 271 | + | run( | |
| 272 | + | arg0, | |
| 273 | + | ) | |
| 274 | + | }) | |
| 275 | + | return _c | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | func (_c *mockkms_destroyBlob_Call) Return(err error) *mockkms_destroyBlob_Call { | |
| 279 | + | _c.Call.Return(err) | |
| 280 | + | return _c | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | func (_c *mockkms_destroyBlob_Call) RunAndReturn(run func(id uint32) error) *mockkms_destroyBlob_Call { | |
| 284 | + | _c.Call.Return(run) | |
| 285 | + | return _c | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | // getBlob provides a mock function for the type mockkms | |
| 289 | + | func (_mock *mockkms) getBlob(id uint32) ([]byte, error) { | |
| 290 | + | ret := _mock.Called(id) | |
| 291 | + | ||
| 292 | + | if len(ret) == 0 { | |
| 293 | + | panic("no return value specified for getBlob") | |
| 294 | + | } | |
| 295 | + | ||
| 296 | + | var r0 []byte | |
| 297 | + | var r1 error | |
| 298 | + | if returnFunc, ok := ret.Get(0).(func(uint32) ([]byte, error)); ok { | |
| 299 | + | return returnFunc(id) | |
| 300 | + | } | |
| 301 | + | if returnFunc, ok := ret.Get(0).(func(uint32) []byte); ok { | |
| 302 | + | r0 = returnFunc(id) | |
| 303 | + | } else { | |
| 304 | + | if ret.Get(0) != nil { | |
| 305 | + | r0 = ret.Get(0).([]byte) | |
| 306 | + | } | |
| 307 | + | } | |
| 308 | + | if returnFunc, ok := ret.Get(1).(func(uint32) error); ok { | |
| 309 | + | r1 = returnFunc(id) | |
| 310 | + | } else { | |
| 311 | + | r1 = ret.Error(1) | |
| 312 | + | } | |
| 313 | + | return r0, r1 | |
| 314 | + | } | |
| 315 | + | ||
| 316 | + | // mockkms_getBlob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'getBlob' | |
| 317 | + | type mockkms_getBlob_Call struct { | |
| 318 | + | *mock.Call | |
| 319 | + | } | |
| 320 | + | ||
| 321 | + | // getBlob is a helper method to define mock.On call | |
| 322 | + | // - id uint32 | |
| 323 | + | func (_e *mockkms_Expecter) getBlob(id any) *mockkms_getBlob_Call { | |
| 324 | + | return &mockkms_getBlob_Call{Call: _e.mock.On("getBlob", id)} | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | func (_c *mockkms_getBlob_Call) Run(run func(id uint32)) *mockkms_getBlob_Call { | |
| 328 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 329 | + | var arg0 uint32 | |
| 330 | + | if args[0] != nil { | |
| 331 | + | arg0 = args[0].(uint32) | |
| 332 | + | } | |
| 333 | + | run( | |
| 334 | + | arg0, | |
| 335 | + | ) | |
| 336 | + | }) | |
| 337 | + | return _c | |
| 338 | + | } | |
| 339 | + | ||
| 340 | + | func (_c *mockkms_getBlob_Call) Return(bytes []byte, err error) *mockkms_getBlob_Call { | |
| 341 | + | _c.Call.Return(bytes, err) | |
| 342 | + | return _c | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | func (_c *mockkms_getBlob_Call) RunAndReturn(run func(id uint32) ([]byte, error)) *mockkms_getBlob_Call { | |
| 346 | + | _c.Call.Return(run) | |
| 347 | + | return _c | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | // objProps provides a mock function for the type mockkms | |
| 351 | + | func (_mock *mockkms) objProps(obj uint32, typ uint32) (map[string][2]uint64, error) { | |
| 352 | + | ret := _mock.Called(obj, typ) | |
| 353 | + | ||
| 354 | + | if len(ret) == 0 { | |
| 355 | + | panic("no return value specified for objProps") | |
| 356 | + | } | |
| 357 | + | ||
| 358 | + | var r0 map[string][2]uint64 | |
| 359 | + | var r1 error | |
| 360 | + | if returnFunc, ok := ret.Get(0).(func(uint32, uint32) (map[string][2]uint64, error)); ok { | |
| 361 | + | return returnFunc(obj, typ) | |
| 362 | + | } | |
| 363 | + | if returnFunc, ok := ret.Get(0).(func(uint32, uint32) map[string][2]uint64); ok { | |
| 364 | + | r0 = returnFunc(obj, typ) | |
| 365 | + | } else { | |
| 366 | + | if ret.Get(0) != nil { | |
| 367 | + | r0 = ret.Get(0).(map[string][2]uint64) | |
| 368 | + | } | |
| 369 | + | } | |
| 370 | + | if returnFunc, ok := ret.Get(1).(func(uint32, uint32) error); ok { | |
| 371 | + | r1 = returnFunc(obj, typ) | |
| 372 | + | } else { | |
| 373 | + | r1 = ret.Error(1) | |
| 374 | + | } | |
| 375 | + | return r0, r1 | |
| 376 | + | } | |
| 377 | + | ||
| 378 | + | // mockkms_objProps_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'objProps' | |
| 379 | + | type mockkms_objProps_Call struct { | |
| 380 | + | *mock.Call | |
| 381 | + | } | |
| 382 | + | ||
| 383 | + | // objProps is a helper method to define mock.On call | |
| 384 | + | // - obj uint32 | |
| 385 | + | // - typ uint32 | |
| 386 | + | func (_e *mockkms_Expecter) objProps(obj any, typ any) *mockkms_objProps_Call { | |
| 387 | + | return &mockkms_objProps_Call{Call: _e.mock.On("objProps", obj, typ)} | |
| 388 | + | } | |
| 389 | + | ||
| 390 | + | func (_c *mockkms_objProps_Call) Run(run func(obj uint32, typ uint32)) *mockkms_objProps_Call { | |
| 391 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 392 | + | var arg0 uint32 | |
| 393 | + | if args[0] != nil { | |
| 394 | + | arg0 = args[0].(uint32) | |
| 395 | + | } | |
| 396 | + | var arg1 uint32 | |
| 397 | + | if args[1] != nil { | |
| 398 | + | arg1 = args[1].(uint32) | |
| 399 | + | } | |
| 400 | + | run( | |
| 401 | + | arg0, | |
| 402 | + | arg1, | |
| 403 | + | ) | |
| 404 | + | }) | |
| 405 | + | return _c | |
| 406 | + | } | |
| 407 | + | ||
| 408 | + | func (_c *mockkms_objProps_Call) Return(stringToUint64s map[string][2]uint64, err error) *mockkms_objProps_Call { | |
| 409 | + | _c.Call.Return(stringToUint64s, err) | |
| 410 | + | return _c | |
| 411 | + | } | |
| 412 | + | ||
| 413 | + | func (_c *mockkms_objProps_Call) RunAndReturn(run func(obj uint32, typ uint32) (map[string][2]uint64, error)) *mockkms_objProps_Call { | |
| 414 | + | _c.Call.Return(run) | |
| 415 | + | return _c | |
| 416 | + | } | |
| 417 | + | ||
| 418 | + | // planes provides a mock function for the type mockkms | |
| 419 | + | func (_mock *mockkms) planes() ([]planeRes, error) { | |
| 420 | + | ret := _mock.Called() | |
| 421 | + | ||
| 422 | + | if len(ret) == 0 { | |
| 423 | + | panic("no return value specified for planes") | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | var r0 []planeRes | |
| 427 | + | var r1 error | |
| 428 | + | if returnFunc, ok := ret.Get(0).(func() ([]planeRes, error)); ok { | |
| 429 | + | return returnFunc() | |
| 430 | + | } | |
| 431 | + | if returnFunc, ok := ret.Get(0).(func() []planeRes); ok { | |
| 432 | + | r0 = returnFunc() | |
| 433 | + | } else { | |
| 434 | + | if ret.Get(0) != nil { | |
| 435 | + | r0 = ret.Get(0).([]planeRes) | |
| 436 | + | } | |
| 437 | + | } | |
| 438 | + | if returnFunc, ok := ret.Get(1).(func() error); ok { | |
| 439 | + | r1 = returnFunc() | |
| 440 | + | } else { | |
| 441 | + | r1 = ret.Error(1) | |
| 442 | + | } | |
| 443 | + | return r0, r1 | |
| 444 | + | } | |
| 445 | + | ||
| 446 | + | // mockkms_planes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'planes' | |
| 447 | + | type mockkms_planes_Call struct { | |
| 448 | + | *mock.Call | |
| 449 | + | } | |
| 450 | + | ||
| 451 | + | // planes is a helper method to define mock.On call | |
| 452 | + | func (_e *mockkms_Expecter) planes() *mockkms_planes_Call { | |
| 453 | + | return &mockkms_planes_Call{Call: _e.mock.On("planes")} | |
| 454 | + | } | |
| 455 | + | ||
| 456 | + | func (_c *mockkms_planes_Call) Run(run func()) *mockkms_planes_Call { | |
| 457 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 458 | + | run() | |
| 459 | + | }) | |
| 460 | + | return _c | |
| 461 | + | } | |
| 462 | + | ||
| 463 | + | func (_c *mockkms_planes_Call) Return(planeResMoqParams []planeRes, err error) *mockkms_planes_Call { | |
| 464 | + | _c.Call.Return(planeResMoqParams, err) | |
| 465 | + | return _c | |
| 466 | + | } | |
| 467 | + | ||
| 468 | + | func (_c *mockkms_planes_Call) RunAndReturn(run func() ([]planeRes, error)) *mockkms_planes_Call { | |
| 469 | + | _c.Call.Return(run) | |
| 470 | + | return _c | |
| 471 | + | } | |
| 472 | + | ||
| 473 | + | // rmFB provides a mock function for the type mockkms | |
| 474 | + | func (_mock *mockkms) rmFB(id uint32) error { | |
| 475 | + | ret := _mock.Called(id) | |
| 476 | + | ||
| 477 | + | if len(ret) == 0 { | |
| 478 | + | panic("no return value specified for rmFB") | |
| 479 | + | } | |
| 480 | + | ||
| 481 | + | var r0 error | |
| 482 | + | if returnFunc, ok := ret.Get(0).(func(uint32) error); ok { | |
| 483 | + | r0 = returnFunc(id) | |
| 484 | + | } else { | |
| 485 | + | r0 = ret.Error(0) | |
| 486 | + | } | |
| 487 | + | return r0 | |
| 488 | + | } | |
| 489 | + | ||
| 490 | + | // mockkms_rmFB_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'rmFB' | |
| 491 | + | type mockkms_rmFB_Call struct { | |
| 492 | + | *mock.Call | |
| 493 | + | } | |
| 494 | + | ||
| 495 | + | // rmFB is a helper method to define mock.On call | |
| 496 | + | // - id uint32 | |
| 497 | + | func (_e *mockkms_Expecter) rmFB(id any) *mockkms_rmFB_Call { | |
| 498 | + | return &mockkms_rmFB_Call{Call: _e.mock.On("rmFB", id)} | |
| 499 | + | } | |
| 500 | + | ||
| 501 | + | func (_c *mockkms_rmFB_Call) Run(run func(id uint32)) *mockkms_rmFB_Call { | |
| 502 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 503 | + | var arg0 uint32 | |
| 504 | + | if args[0] != nil { | |
| 505 | + | arg0 = args[0].(uint32) | |
| 506 | + | } | |
| 507 | + | run( | |
| 508 | + | arg0, | |
| 509 | + | ) | |
| 510 | + | }) | |
| 511 | + | return _c | |
| 512 | + | } | |
| 513 | + | ||
| 514 | + | func (_c *mockkms_rmFB_Call) Return(err error) *mockkms_rmFB_Call { | |
| 515 | + | _c.Call.Return(err) | |
| 516 | + | return _c | |
| 517 | + | } | |
| 518 | + | ||
| 519 | + | func (_c *mockkms_rmFB_Call) RunAndReturn(run func(id uint32) error) *mockkms_rmFB_Call { | |
| 520 | + | _c.Call.Return(run) | |
| 521 | + | return _c | |
| 522 | + | } | |
M internal/adapters/drm/kms_test.go +55 -15
| @@ -2,8 +2,12 @@ package drm | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "encoding/binary" | |
| 5 | + | "slices" | |
| 5 | 6 | "testing" | |
| 7 | + | "time" | |
| 6 | 8 | "unsafe" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/nefertty/internal/ports" | |
| 7 | 11 | ) | |
| 8 | 12 | ||
| 9 | 13 | func TestStructSizesMatchIoctls(t *testing.T) { | |
| @@ -13,19 +17,21 @@ func TestStructSizesMatchIoctls(t *testing.T) { | |||
| 13 | 17 | req uintptr | |
| 14 | 18 | }{ | |
| 15 | 19 | {"card_res", unsafe.Sizeof(cardRes{}), ioctlGetResources}, | |
| 16 | - | {"mode_crtc", unsafe.Sizeof(modeCrtc{}), ioctlSetCrtc}, | |
| 17 | 20 | {"get_encoder", unsafe.Sizeof(getEncoder{}), ioctlGetEncoder}, | |
| 18 | 21 | {"get_connector", unsafe.Sizeof(getConnector{}), ioctlGetConnector}, | |
| 19 | - | {"fb_cmd", unsafe.Sizeof(fbCmd{}), ioctlAddFB}, | |
| 20 | - | {"page_flip", unsafe.Sizeof(pageFlip{}), ioctlPageFlip}, | |
| 21 | - | {"create_dumb", unsafe.Sizeof(createDumb{}), ioctlCreateDumb}, | |
| 22 | - | {"map_dumb", unsafe.Sizeof(mapDumb{}), ioctlMapDumb}, | |
| 23 | 22 | {"fb_cmd2", unsafe.Sizeof(fbCmd2{}), ioctlAddFB2}, | |
| 24 | 23 | {"prime_handle", unsafe.Sizeof(primeHandle{}), ioctlPrimeFDToHandle}, | |
| 25 | 24 | {"gem_close", unsafe.Sizeof(gemClose{}), ioctlGemClose}, | |
| 26 | 25 | {"obj_get_props", unsafe.Sizeof(objGetProps{}), ioctlObjGetProps}, | |
| 27 | 26 | {"get_prop", unsafe.Sizeof(getProp{}), ioctlGetProp}, | |
| 28 | - | {"obj_set_prop", unsafe.Sizeof(objSetProp{}), ioctlObjSetProp}, | |
| 27 | + | {"mode_crtc", unsafe.Sizeof(modeCrtc{}), ioctlGetCrtc}, | |
| 28 | + | {"set_client_cap", unsafe.Sizeof(setClientCap{}), ioctlSetClientCap}, | |
| 29 | + | {"get_plane_res", unsafe.Sizeof(getPlaneRes{}), ioctlGetPlaneRes}, | |
| 30 | + | {"get_plane", unsafe.Sizeof(getPlane{}), ioctlGetPlane}, | |
| 31 | + | {"get_blob", unsafe.Sizeof(getBlob{}), ioctlGetPropBlob}, | |
| 32 | + | {"atomic", unsafe.Sizeof(modeAtomic{}), ioctlAtomic}, | |
| 33 | + | {"create_blob", unsafe.Sizeof(createBlob{}), ioctlCreateBlob}, | |
| 34 | + | {"get_cap", unsafe.Sizeof(getCap{}), ioctlGetCap}, | |
| 29 | 35 | } { | |
| 30 | 36 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | |
| 31 | 37 | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | |
| @@ -88,19 +94,53 @@ func TestParseEDID(t *testing.T) { | |||
| 88 | 94 | } | |
| 89 | 95 | } | |
| 90 | 96 | ||
| 91 | - | func TestFlipCrtcs(t *testing.T) { | |
| 92 | - | ev := func(typ, crtc uint32) []byte { | |
| 97 | + | func TestParseFlips(t *testing.T) { | |
| 98 | + | ev := func(typ, crtc uint32, user uint64, sec, usec, seq uint32) []byte { | |
| 93 | 99 | b := make([]byte, 32) | |
| 94 | - | binary.LittleEndian.PutUint32(b, typ) | |
| 95 | - | binary.LittleEndian.PutUint32(b[4:], 32) | |
| 96 | - | binary.LittleEndian.PutUint32(b[28:], crtc) | |
| 100 | + | le := binary.LittleEndian | |
| 101 | + | le.PutUint32(b, typ) | |
| 102 | + | le.PutUint32(b[4:], 32) | |
| 103 | + | le.PutUint64(b[8:], user) | |
| 104 | + | le.PutUint32(b[16:], sec) | |
| 105 | + | le.PutUint32(b[20:], usec) | |
| 106 | + | le.PutUint32(b[24:], seq) | |
| 107 | + | le.PutUint32(b[28:], crtc) | |
| 97 | 108 | return b | |
| 98 | 109 | } | |
| 99 | - | buf := append(append(ev(eventFlipDone, 41), ev(1, 7)...), ev(eventFlipDone, 42)...) | |
| 100 | - | if got := flipCrtcs(buf); len(got) != 2 || got[0] != 41 || got[1] != 42 { | |
| 101 | - | t.Fatalf("got %v", got) | |
| 110 | + | buf := append(append(ev(eventFlipDone, 41, userFrame, 3, 250, 7), ev(1, 7, 0, 0, 0, 0)...), ev(eventFlipDone, 42, userState, 0, 1, 8)...) | |
| 111 | + | got := parseFlips(buf) | |
| 112 | + | want := []flipEvent{{crtc: 41, user: userFrame, when: 3*time.Second + 250*time.Microsecond, seq: 7}, {crtc: 42, user: userState, when: time.Microsecond, seq: 8}} | |
| 113 | + | if !slices.Equal(got, want) { | |
| 114 | + | t.Fatalf("got %+v, want %+v", got, want) | |
| 102 | 115 | } | |
| 103 | - | if got := flipCrtcs(buf[:20]); len(got) != 0 { | |
| 116 | + | if got := parseFlips(buf[:20]); len(got) != 0 { | |
| 104 | 117 | t.Fatalf("truncated: got %v", got) | |
| 105 | 118 | } | |
| 106 | 119 | } | |
| 120 | + | ||
| 121 | + | func TestParseInFormats(t *testing.T) { | |
| 122 | + | le := binary.LittleEndian | |
| 123 | + | // 3 formats at 24, 2 modifiers at 40. | |
| 124 | + | b := make([]byte, 40+2*24) | |
| 125 | + | le.PutUint32(b, 1) | |
| 126 | + | le.PutUint32(b[8:], 3) | |
| 127 | + | le.PutUint32(b[12:], 24) | |
| 128 | + | le.PutUint32(b[16:], 2) | |
| 129 | + | le.PutUint32(b[20:], 40) | |
| 130 | + | for i, f := range []uint32{fourccXRGB, fourccARGB, 0x3432564e} { | |
| 131 | + | le.PutUint32(b[24+i*4:], f) | |
| 132 | + | } | |
| 133 | + | // linear on formats 0 and 2, a tiled modifier on format 1. | |
| 134 | + | le.PutUint64(b[40:], 0b101) | |
| 135 | + | le.PutUint64(b[56:], 0) | |
| 136 | + | le.PutUint64(b[64:], 0b010) | |
| 137 | + | le.PutUint64(b[80:], 0x0200000000000001) | |
| 138 | + | got := parseInFormats(b) | |
| 139 | + | want := []ports.DMABufFormat{{Format: fourccXRGB}, {Format: 0x3432564e}, {Format: fourccARGB, Modifier: 0x0200000000000001}} | |
| 140 | + | if !slices.Equal(got, want) { | |
| 141 | + | t.Fatalf("got %+v, want %+v", got, want) | |
| 142 | + | } | |
| 143 | + | if parseInFormats(b[:30]) != nil { | |
| 144 | + | t.Fatal("truncated blob parsed") | |
| 145 | + | } | |
| 146 | + | } | |
M internal/adapters/drm/output.go +570 -227
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | 7 | "maps" | |
| 8 | + | "os" | |
| 8 | 9 | "slices" | |
| 9 | 10 | "time" | |
| 10 | 11 | "unsafe" | |
| @@ -14,73 +15,179 @@ import ( | |||
| 14 | 15 | "golang.org/x/sys/unix" | |
| 15 | 16 | ) | |
| 16 | 17 | ||
| 17 | - | // Output is one KMS connector flipping between two images. One goroutine | |
| 18 | - | // runs it (Run) and closes it. | |
| 18 | + | // Output is one KMS connector on a CRTC, driven by atomic commits (ADR | |
| 19 | + | // 015). One goroutine runs it (Run) and closes it. Every commit that | |
| 20 | + | // changes the screen asks for an event and leaves the output pending until | |
| 21 | + | // it arrives: the next commit waits for it, so none fails with EBUSY. | |
| 19 | 22 | type Output struct { | |
| 20 | - | fd int | |
| 21 | - | flipped <-chan int // page flips of this CRTC, from Card.ReadEvents | |
| 23 | + | k kms | |
| 24 | + | flipped <-chan flipEvent // commit events of this CRTC, from Card.ReadEvents | |
| 22 | 25 | crtc uint32 | |
| 23 | 26 | conn connector | |
| 24 | 27 | mode modeInfo | |
| 25 | 28 | saved modeCrtc | |
| 26 | - | // fbs are the two images frames alternate between, back the one the | |
| 27 | - | // next frame draws into. They are renderer images exported as dmabufs | |
| 28 | - | // (ADR 014: zero copy), or dumb buffers filled by the CPU when the GPU | |
| 29 | - | // cannot export a scanout-capable image (logged fallback). | |
| 30 | - | fbs [2]uint32 | |
| 31 | - | bufs [2]*dumbBuffer // fallback only | |
| 32 | - | back int | |
| 33 | - | pending bool | |
| 34 | - | flipStart time.Time | |
| 35 | - | flips int | |
| 36 | - | monitor Monitor | |
| 37 | - | cursor *Cursor | |
| 38 | - | log zerowrap.Logger | |
| 39 | - | // Direct scanout: client framebuffers by DMABuf ID, GEM handle counts, | |
| 40 | - | // and the buffer on screen and queued (0: the composed image). | |
| 41 | - | card *Card | |
| 29 | + | monitor Monitor | |
| 30 | + | log zerowrap.Logger | |
| 31 | + | // Properties: CRTC and connector property IDs by name. | |
| 32 | + | crtcProps map[string]uint32 | |
| 33 | + | connCrtc uint32 // the connector's CRTC_ID property | |
| 34 | + | modeBlob uint32 | |
| 35 | + | // primary shows frames; cursor is the cursor plane (nil: none); | |
| 36 | + | // stray are other planes that can show on this CRTC, turned off at | |
| 37 | + | // every modeset when a previous master left them on it. | |
| 38 | + | primary *plane | |
| 39 | + | cursor *Cursor | |
| 40 | + | stray []*plane | |
| 41 | + | // fbs are the two renderer images frames alternate between, back the | |
| 42 | + | // one the next frame draws into (ADR 014: zero copy). | |
| 43 | + | fbs [2]uint32 | |
| 44 | + | back int | |
| 45 | + | // pending: a commit waits for its event; pendingFrame when it flips a | |
| 46 | + | // new frame (else it only changed the cursor or VRR). | |
| 47 | + | pending bool | |
| 48 | + | pendingFrame pendingFrame | |
| 49 | + | flipStart time.Time | |
| 50 | + | flips int | |
| 51 | + | // Direct scanout: client framebuffers by DMABuf ID and the buffer on | |
| 52 | + | // screen and queued (0: the composed image). | |
| 42 | 53 | scanout bool | |
| 43 | 54 | clientFBs map[uint64]*clientFB | |
| 44 | 55 | shown, queued uint64 | |
| 45 | 56 | reason string // why the last frame was composed ("" = scanout) | |
| 46 | - | unsent ports.OutputPresented | |
| 47 | - | // Tearing (ADR 006): async flips of scanned-out buffers that ask for | |
| 48 | - | // them, while the card accepts them. vrrProp is the CRTC's VRR_ENABLED | |
| 49 | - | // property (0: no VRR), turned on while a buffer is scanned out. | |
| 50 | - | tearing bool | |
| 51 | - | vrrProp uint32 | |
| 52 | - | vrrOn, async bool | |
| 53 | - | // composedSince is when composition took over from scanout. | |
| 57 | + | unsent []ports.OutputPresented | |
| 58 | + | // Tearing (ADR 006): async commits of scanned-out buffers that ask for | |
| 59 | + | // them, when the card has DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. asyncFence | |
| 60 | + | // is set when an async commit may carry IN_FENCE_FD (probed once). | |
| 61 | + | tearing bool | |
| 62 | + | asyncFence bool | |
| 63 | + | asyncProbed bool | |
| 64 | + | async bool | |
| 65 | + | // VRR: the CRTC's VRR_ENABLED property (0: none), on while a buffer is | |
| 66 | + | // scanned out, set inside frame commits. | |
| 67 | + | vrrProp uint32 | |
| 68 | + | vrrOn bool | |
| 54 | 69 | composedSince time.Time | |
| 55 | 70 | // kind is how the images were made; validated once a modeset took them. | |
| 56 | 71 | kind imageKind | |
| 57 | 72 | validated bool | |
| 58 | 73 | } | |
| 59 | 74 | ||
| 75 | + | // pendingFrame is what the pending frame commit shows. | |
| 76 | + | type pendingFrame struct { | |
| 77 | + | frame bool | |
| 78 | + | queued uint64 | |
| 79 | + | zeroCopy, async bool | |
| 80 | + | shows map[ports.WindowID]uint64 | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // Commit event userData. | |
| 84 | + | const ( | |
| 85 | + | userFrame = 1 | |
| 86 | + | userState = 2 | |
| 87 | + | ) | |
| 88 | + | ||
| 60 | 89 | // CursorLoader returns the image of a cursor at an output scale, at most | |
| 61 | 90 | // limit pixels on a side; an empty image hides the cursor. | |
| 62 | 91 | type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error) | |
| 63 | 92 | ||
| 64 | - | // newOutput prepares the cursor and saves the CRTC for a connector on crtc. | |
| 93 | + | // newOutput reads the CRTC's planes and properties for a connector on crtc. | |
| 65 | 94 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | |
| 66 | 95 | log := card.log | |
| 67 | - | o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), card: card, scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"} | |
| 96 | + | pipe := slices.Index(card.crtcs, crtc) | |
| 97 | + | o := &Output{k: card.k, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"} | |
| 68 | 98 | var err error | |
| 69 | 99 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | |
| 70 | 100 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | |
| 71 | 101 | } | |
| 102 | + | if err := o.readProps(pipe, card.taken, cursorSize(card.fd)); err != nil { | |
| 103 | + | return nil, err | |
| 104 | + | } | |
| 72 | 105 | o.tearing = card.async && !card.want.NoTearing | |
| 73 | 106 | if !card.want.NoVRR { | |
| 74 | - | o.vrrProp = vrrProperty(card.fd, c.id, crtc) | |
| 107 | + | o.vrrProp = vrrProperty(o.k, c.id, crtc) | |
| 75 | 108 | } | |
| 76 | - | if o.cursor, err = newCursor(card.fd, crtc, slices.Index(card.crtcs, crtc)); err != nil { | |
| 77 | - | log.Warn().Err(err).Str("connector", c.name).Msg("no hardware cursor") | |
| 78 | - | } | |
| 79 | - | log.Info().Str("card", card.path).Str("connector", c.name).Str("mode", mode.String()).Str("make", o.monitor.Make).Str("model", o.monitor.Model).Uint32("crtc", crtc).Bool("tearing", o.tearing).Bool("vrr", o.vrrProp != 0).Msg("output") | |
| 109 | + | log.Info().Str("card", card.path).Str("connector", c.name).Str("mode", mode.String()).Str("make", o.monitor.Make).Str("model", o.monitor.Model).Uint32("crtc", crtc).Uint32("primary", o.primary.id).Bool("cursor", o.cursor != nil).Bool("tearing", o.tearing).Bool("vrr", o.vrrProp != 0).Msg("output") | |
| 80 | 110 | return o, nil | |
| 81 | 111 | } | |
| 82 | 112 | ||
| 83 | - | // Cursor is the hardware cursor, or nil when the driver has none. | |
| 113 | + | // readProps finds the output's planes and property IDs. taken are planes | |
| 114 | + | // other outputs drive. | |
| 115 | + | func (o *Output) readProps(pipe int, taken map[uint32]bool, cursorSide int) error { | |
| 116 | + | cp, err := o.k.objProps(o.crtc, objCrtc) | |
| 117 | + | if err != nil { | |
| 118 | + | return fmt.Errorf("crtc properties: %w", err) | |
| 119 | + | } | |
| 120 | + | o.crtcProps = map[string]uint32{} | |
| 121 | + | for name, v := range cp { | |
| 122 | + | o.crtcProps[name] = uint32(v[0]) | |
| 123 | + | } | |
| 124 | + | np, err := o.k.objProps(o.conn.id, objConnector) | |
| 125 | + | if err != nil { | |
| 126 | + | return fmt.Errorf("connector properties: %w", err) | |
| 127 | + | } | |
| 128 | + | o.connCrtc = uint32(np["CRTC_ID"][0]) | |
| 129 | + | if o.crtcProps["MODE_ID"] == 0 || o.crtcProps["ACTIVE"] == 0 || o.connCrtc == 0 { | |
| 130 | + | return errors.New("crtc or connector lacks atomic properties") | |
| 131 | + | } | |
| 132 | + | planes, err := readPlanes(o.k, pipe) | |
| 133 | + | if err != nil { | |
| 134 | + | return fmt.Errorf("planes: %w", err) | |
| 135 | + | } | |
| 136 | + | pick := func(typ uint32) *plane { | |
| 137 | + | var best *plane | |
| 138 | + | for _, p := range planes { | |
| 139 | + | if p.typ != typ || taken[p.id] { | |
| 140 | + | continue | |
| 141 | + | } | |
| 142 | + | // Prefer the plane already on this CRTC. | |
| 143 | + | if best == nil || p.crtc == o.crtc && best.crtc != o.crtc { | |
| 144 | + | best = p | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | return best | |
| 148 | + | } | |
| 149 | + | if o.primary = pick(planePrimary); o.primary == nil { | |
| 150 | + | return errors.New("no free primary plane") | |
| 151 | + | } | |
| 152 | + | if p := pick(planeCursor); p != nil { | |
| 153 | + | o.cursor = newCursor(p, cursorSide) | |
| 154 | + | } | |
| 155 | + | for _, p := range planes { | |
| 156 | + | if p != o.primary && (o.cursor == nil || p != o.cursor.plane) && !taken[p.id] { | |
| 157 | + | o.stray = append(o.stray, p) | |
| 158 | + | } | |
| 159 | + | } | |
| 160 | + | return nil | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | // owned are the planes this output drives. | |
| 164 | + | func (o *Output) owned() []*plane { | |
| 165 | + | out := []*plane{o.primary} | |
| 166 | + | if o.cursor != nil { | |
| 167 | + | out = append(out, o.cursor.plane) | |
| 168 | + | } | |
| 169 | + | return out | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | // cursorSize is the cursor plane side: the smaller of the driver's width | |
| 173 | + | // and height caps, 64 when it does not say. | |
| 174 | + | func cursorSize(fd int) int { | |
| 175 | + | size := 0 | |
| 176 | + | for _, c := range []uint64{capCursorW, capCursorH} { | |
| 177 | + | v := getCap{capability: c} | |
| 178 | + | if ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value >= 16 && v.value <= 512 { | |
| 179 | + | if size == 0 || int(v.value) < size { | |
| 180 | + | size = int(v.value) | |
| 181 | + | } | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | if size == 0 { | |
| 185 | + | size = 64 | |
| 186 | + | } | |
| 187 | + | return size | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // Cursor is the hardware cursor, or nil when the CRTC has no cursor plane. | |
| 84 | 191 | func (o *Output) Cursor() *Cursor { return o.cursor } | |
| 85 | 192 | ||
| 86 | 193 | // Info describes the output for core and clients. Unknown EDID fields are empty. | |
| @@ -97,52 +204,215 @@ func (o *Output) Info() ports.OutputInfo { | |||
| 97 | 204 | func (o *Output) Width() int { return int(o.mode.HDisplay) } | |
| 98 | 205 | func (o *Output) Height() int { return int(o.mode.VDisplay) } | |
| 99 | 206 | ||
| 100 | - | // modeset shows the front buffer; needed at start and after every VT resume. | |
| 207 | + | // primaryProps shows fb full screen on the primary plane. | |
| 208 | + | func (o *Output) primaryProps(req *atomicReq, fb uint32) { | |
| 209 | + | p := o.primary | |
| 210 | + | w, h := uint64(o.Width()), uint64(o.Height()) | |
| 211 | + | req.set(p.id, p.prop("FB_ID"), uint64(fb)) | |
| 212 | + | req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc)) | |
| 213 | + | req.set(p.id, p.prop("SRC_X"), 0) | |
| 214 | + | req.set(p.id, p.prop("SRC_Y"), 0) | |
| 215 | + | req.set(p.id, p.prop("SRC_W"), w<<16) | |
| 216 | + | req.set(p.id, p.prop("SRC_H"), h<<16) | |
| 217 | + | req.set(p.id, p.prop("CRTC_X"), 0) | |
| 218 | + | req.set(p.id, p.prop("CRTC_Y"), 0) | |
| 219 | + | req.set(p.id, p.prop("CRTC_W"), w) | |
| 220 | + | req.set(p.id, p.prop("CRTC_H"), h) | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | // modeset shows the front image with the output's mode, turning off every | |
| 224 | + | // other plane on the CRTC; needed at start and after every VT resume. | |
| 225 | + | // It blocks until the kernel applied it. | |
| 101 | 226 | func (o *Output) modeset() error { | |
| 102 | 227 | o.pending = false | |
| 228 | + | o.pendingFrame = pendingFrame{} | |
| 103 | 229 | o.shown, o.queued = 0, 0 | |
| 104 | - | if err := setCrtc(o.fd, o.crtc, o.conn.id, o.fbs[1-o.back], &o.mode); err != nil { | |
| 105 | - | return fmt.Errorf("set crtc: %w", err) | |
| 230 | + | mode := o.mode | |
| 231 | + | blob, err := o.k.createBlob(unsafe.Slice((*byte)(unsafe.Pointer(&mode)), unsafe.Sizeof(mode))) | |
| 232 | + | if err != nil { | |
| 233 | + | return fmt.Errorf("mode blob: %w", err) | |
| 234 | + | } | |
| 235 | + | req := &atomicReq{} | |
| 236 | + | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob)) | |
| 237 | + | req.set(o.crtc, o.crtcProps["ACTIVE"], 1) | |
| 238 | + | req.set(o.crtc, o.vrrProp, 0) | |
| 239 | + | req.set(o.conn.id, o.connCrtc, uint64(o.crtc)) | |
| 240 | + | o.primaryProps(req, o.fbs[1-o.back]) | |
| 241 | + | if o.cursor != nil { | |
| 242 | + | o.cursor.props(req, o.crtc, cursorState{}) | |
| 243 | + | } | |
| 244 | + | for _, p := range o.stray { | |
| 245 | + | // Only planes a previous master left on this CRTC. | |
| 246 | + | props, err := o.k.objProps(p.id, objPlane) | |
| 247 | + | if err != nil || uint32(props["CRTC_ID"][1]) != o.crtc { | |
| 248 | + | continue | |
| 249 | + | } | |
| 250 | + | req.set(p.id, p.prop("FB_ID"), 0) | |
| 251 | + | req.set(p.id, p.prop("CRTC_ID"), 0) | |
| 252 | + | } | |
| 253 | + | if err := o.k.commit(req, atomicAllowModes, 0); err != nil { | |
| 254 | + | _ = o.k.destroyBlob(blob) | |
| 255 | + | return fmt.Errorf("modeset: %w", err) | |
| 256 | + | } | |
| 257 | + | if o.modeBlob != 0 { | |
| 258 | + | _ = o.k.destroyBlob(o.modeBlob) | |
| 259 | + | } | |
| 260 | + | o.modeBlob = blob | |
| 261 | + | o.vrrOn = false | |
| 262 | + | if o.cursor != nil { | |
| 263 | + | o.cursor.applied = cursorState{} | |
| 106 | 264 | } | |
| 107 | 265 | o.log.Info().Str("connector", o.conn.name).Msg("modeset") | |
| 108 | - | // Another master may have left VRR on: start from off. | |
| 109 | - | o.vrrOn = true | |
| 110 | - | o.setVRR(false) | |
| 111 | 266 | return nil | |
| 112 | 267 | } | |
| 113 | 268 | ||
| 114 | - | // present queues a page flip to the frame just drawn into the back image | |
| 115 | - | // (copied into it by the CPU on the dumb-buffer fallback). | |
| 116 | - | func (o *Output) present(r ports.Renderer) error { | |
| 117 | - | if b := o.bufs[o.back]; b != nil { | |
| 118 | - | r.CopyBGRX(b.mem, int(b.pitch)) | |
| 269 | + | // probeAsync checks once whether async commits may carry IN_FENCE_FD. | |
| 270 | + | func (o *Output) probeAsync(r ports.Renderer) { | |
| 271 | + | if o.asyncProbed || !o.tearing { | |
| 272 | + | return | |
| 273 | + | } | |
| 274 | + | o.asyncProbed = true | |
| 275 | + | r.UseTarget(o.back) | |
| 276 | + | done, err := r.Render(ports.Scene{Background: "#000000"}, nil) | |
| 277 | + | if err != nil { | |
| 278 | + | return | |
| 279 | + | } | |
| 280 | + | req := &atomicReq{} | |
| 281 | + | req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(o.fbs[o.back])) | |
| 282 | + | if done != nil { | |
| 283 | + | defer done.Close() | |
| 284 | + | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(done.Fd())) | |
| 285 | + | } | |
| 286 | + | err = o.k.commit(req, atomicTestOnly|flipAsyncFlag, 0) | |
| 287 | + | o.asyncFence = err == nil && done != nil | |
| 288 | + | o.log.Info().Err(err).Str("connector", o.conn.name).Bool("async_fence", o.asyncFence).Msg("tearing probe") | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | // wantVRR is the VRR state the next frame commit sets: on while a buffer | |
| 292 | + | // is scanned out, off after composing for vrrHold (each toggle may | |
| 293 | + | // flicker, so short compositions keep it). | |
| 294 | + | func (o *Output) wantVRR(direct bool) bool { | |
| 295 | + | if o.vrrProp == 0 { | |
| 296 | + | return false | |
| 297 | + | } | |
| 298 | + | if direct { | |
| 299 | + | o.composedSince = time.Time{} | |
| 300 | + | return true | |
| 119 | 301 | } | |
| 120 | - | // VRR goes off only after composing for vrrHold: each toggle is a | |
| 121 | - | // blocking commit and may flicker, so short compositions keep it. | |
| 122 | 302 | if o.composedSince.IsZero() { | |
| 123 | 303 | o.composedSince = time.Now() | |
| 124 | - | } else if time.Since(o.composedSince) > vrrHold { | |
| 125 | - | o.setVRR(false) | |
| 126 | 304 | } | |
| 127 | - | o.setAsync(false) | |
| 128 | - | if err := flip(o.fd, o.crtc, o.fbs[o.back], false); err != nil { | |
| 305 | + | return o.vrrOn && time.Since(o.composedSince) <= vrrHold | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | // commitFrame flips fb in one commit with the cursor and VRR. An async | |
| 309 | + | // commit carries only the primary plane (kernel rule): a frame that must | |
| 310 | + | // also move the cursor or change VRR flips at vblank. fence is the | |
| 311 | + | // frame's GPU fence (nil: none); the caller keeps and closes it. | |
| 312 | + | func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame) error { | |
| 313 | + | cur := cursorState{} | |
| 314 | + | if o.cursor != nil { | |
| 315 | + | cur = o.cursor.desired() | |
| 316 | + | } | |
| 317 | + | if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence) { | |
| 318 | + | async = false | |
| 319 | + | } | |
| 320 | + | req := &atomicReq{} | |
| 321 | + | flags := uint32(atomicNonblock | flipEventFlag) | |
| 322 | + | if async { | |
| 323 | + | flags |= flipAsyncFlag | |
| 324 | + | req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(fb)) | |
| 325 | + | } else { | |
| 326 | + | o.primaryProps(req, fb) | |
| 327 | + | req.set(o.crtc, o.vrrProp, boolValue(vrr)) | |
| 328 | + | if o.cursor != nil { | |
| 329 | + | o.cursor.props(req, o.crtc, cur) | |
| 330 | + | } | |
| 331 | + | } | |
| 332 | + | if fence != nil { | |
| 333 | + | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |
| 334 | + | } | |
| 335 | + | // The kernel takes its own reference on the fence. | |
| 336 | + | if err := o.k.commit(req, flags, userFrame); err != nil { | |
| 337 | + | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | |
| 338 | + | // Retry without turning VRR on: if that passes, the driver | |
| 339 | + | // refuses VRR on this output. | |
| 340 | + | if o.commitFrame(fb, fence, false, false, f) == nil { | |
| 341 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled") | |
| 342 | + | o.vrrProp = 0 | |
| 343 | + | return nil | |
| 344 | + | } | |
| 345 | + | } | |
| 346 | + | return err | |
| 347 | + | } | |
| 348 | + | if !async { | |
| 349 | + | if o.vrrOn != vrr { | |
| 350 | + | o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr") | |
| 351 | + | } | |
| 352 | + | o.vrrOn = vrr | |
| 353 | + | if o.cursor != nil { | |
| 354 | + | if cur != o.cursor.applied { | |
| 355 | + | o.cursor.mu.Lock() | |
| 356 | + | o.cursor.commits++ | |
| 357 | + | o.cursor.mu.Unlock() | |
| 358 | + | } | |
| 359 | + | o.cursor.applied = cur | |
| 360 | + | } | |
| 361 | + | } | |
| 362 | + | f.frame, f.async = true, async | |
| 363 | + | o.setAsync(async) | |
| 364 | + | o.pending, o.pendingFrame, o.flipStart = true, f, time.Now() | |
| 365 | + | return nil | |
| 366 | + | } | |
| 367 | + | ||
| 368 | + | // commitState commits a cursor or VRR change without a new frame. | |
| 369 | + | func (o *Output) commitState(vrr bool) error { | |
| 370 | + | req := &atomicReq{} | |
| 371 | + | cur := cursorState{} | |
| 372 | + | if o.cursor != nil { | |
| 373 | + | cur = o.cursor.desired() | |
| 374 | + | if cur != o.cursor.applied { | |
| 375 | + | o.cursor.props(req, o.crtc, cur) | |
| 376 | + | } | |
| 377 | + | } | |
| 378 | + | if vrr != o.vrrOn { | |
| 379 | + | req.set(o.crtc, o.vrrProp, boolValue(vrr)) | |
| 380 | + | } | |
| 381 | + | if len(req.objs) == 0 { | |
| 382 | + | return nil | |
| 383 | + | } | |
| 384 | + | if err := o.k.commit(req, atomicNonblock|flipEventFlag, userState); err != nil { | |
| 129 | 385 | return err | |
| 130 | 386 | } | |
| 131 | - | o.pending, o.flipStart = true, time.Now() | |
| 132 | - | o.queued = 0 | |
| 133 | - | o.back = 1 - o.back | |
| 387 | + | if o.cursor != nil && cur != o.cursor.applied { | |
| 388 | + | o.cursor.applied = cur | |
| 389 | + | o.cursor.mu.Lock() | |
| 390 | + | o.cursor.commits++ | |
| 391 | + | o.cursor.mu.Unlock() | |
| 392 | + | } | |
| 393 | + | if vrr != o.vrrOn { | |
| 394 | + | o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr") | |
| 395 | + | } | |
| 396 | + | o.vrrOn = vrr | |
| 397 | + | o.pending, o.pendingFrame, o.flipStart = true, pendingFrame{}, time.Now() | |
| 134 | 398 | return nil | |
| 135 | 399 | } | |
| 136 | 400 | ||
| 137 | - | // tryScanout flips a fullscreen client buffer directly. It reports false, | |
| 138 | - | // with the reason logged on change, when the frame must be composed. | |
| 139 | - | func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (bool, error) { | |
| 401 | + | func boolValue(b bool) uint64 { | |
| 402 | + | if b { | |
| 403 | + | return 1 | |
| 404 | + | } | |
| 405 | + | return 0 | |
| 406 | + | } | |
| 407 | + | ||
| 408 | + | // scanoutFrame picks a fullscreen client buffer to flip directly. It | |
| 409 | + | // returns fb 0, with the reason logged on change, when the frame must be | |
| 410 | + | // composed. | |
| 411 | + | func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (fb uint32, c ports.SurfaceContent) { | |
| 140 | 412 | reason := "disabled" | |
| 141 | - | var c ports.SurfaceContent | |
| 142 | 413 | if o.scanout { | |
| 143 | 414 | c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height()) | |
| 144 | 415 | } | |
| 145 | - | var fb uint32 | |
| 146 | 416 | if reason == "" { | |
| 147 | 417 | fb, reason = o.scanoutFB(c.DMABuf, time.Now()) | |
| 148 | 418 | } | |
| @@ -151,71 +421,44 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports | |||
| 151 | 421 | o.reason = reason | |
| 152 | 422 | } | |
| 153 | 423 | if reason != "" { | |
| 154 | - | return false, nil | |
| 155 | - | } | |
| 156 | - | if o.shown == c.DMABuf.ID && o.queued == 0 { | |
| 157 | - | // Already on screen: nothing to flip. | |
| 158 | - | return true, nil | |
| 424 | + | return 0, c | |
| 159 | 425 | } | |
| 426 | + | return fb, c | |
| 427 | + | } | |
| 428 | + | ||
| 429 | + | // commitScanout flips a client buffer. It reports false when KMS refused | |
| 430 | + | // the buffer: the frame must be composed. | |
| 431 | + | func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame) (bool, error) { | |
| 432 | + | cfb := o.clientFBs[c.DMABuf.ID] | |
| 160 | 433 | // Drivers refuse async flips that change the format or modifier: the | |
| 161 | 434 | // flip from the composed image into scanout waits for vblank. | |
| 162 | - | cfb := o.clientFBs[c.DMABuf.ID] | |
| 163 | 435 | async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync | |
| 164 | - | err := flip(o.fd, o.crtc, fb, async) | |
| 436 | + | vrr := o.wantVRR(true) | |
| 437 | + | f.queued, f.zeroCopy = c.DMABuf.ID, true | |
| 438 | + | err := o.commitFrame(fb, nil, async, vrr, f) | |
| 165 | 439 | if err != nil && async && errors.Is(err, unix.EINVAL) { | |
| 166 | - | // Refused (e.g. a layout change, or not a fast update): this | |
| 167 | - | // buffer flips at vblank from now on. | |
| 440 | + | // Refused (e.g. not a fast update): this buffer flips at vblank | |
| 441 | + | // from now on. | |
| 168 | 442 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | |
| 169 | - | cfb.noAsync, async = true, false | |
| 170 | - | err = flip(o.fd, o.crtc, fb, false) | |
| 443 | + | cfb.noAsync = true | |
| 444 | + | err = o.commitFrame(fb, nil, false, vrr, f) | |
| 445 | + | } | |
| 446 | + | if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) { | |
| 447 | + | // KMS refuses this buffer on the plane: compose it instead. | |
| 448 | + | o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused") | |
| 449 | + | cfb.failed, o.reason = "flip_refused", "flip_refused" | |
| 450 | + | o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout") | |
| 451 | + | return false, nil | |
| 171 | 452 | } | |
| 172 | 453 | if err == nil { | |
| 173 | - | o.setAsync(async) | |
| 174 | - | o.setVRR(true) | |
| 175 | - | o.composedSince = time.Time{} | |
| 454 | + | o.queued = c.DMABuf.ID | |
| 176 | 455 | } | |
| 177 | - | if err != nil { | |
| 178 | - | if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) { | |
| 179 | - | // KMS refuses this buffer on the plane: compose it instead. | |
| 180 | - | o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused") | |
| 181 | - | cfb.failed = "flip_refused" | |
| 182 | - | o.reason = "flip_refused" | |
| 183 | - | o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout") | |
| 184 | - | return false, nil | |
| 185 | - | } | |
| 186 | - | return true, err | |
| 187 | - | } | |
| 188 | - | o.pending, o.flipStart = true, time.Now() | |
| 189 | - | o.queued = c.DMABuf.ID | |
| 190 | - | return true, nil | |
| 456 | + | return true, err | |
| 191 | 457 | } | |
| 192 | 458 | ||
| 193 | 459 | // vrrHold is how long composition runs before VRR turns off. | |
| 194 | 460 | const vrrHold = 500 * time.Millisecond | |
| 195 | 461 | ||
| 196 | - | // setVRR turns variable refresh on or off, when the output has it. | |
| 197 | - | func (o *Output) setVRR(on bool) { | |
| 198 | - | if o.vrrProp == 0 || on == o.vrrOn { | |
| 199 | - | return | |
| 200 | - | } | |
| 201 | - | v := uint64(0) | |
| 202 | - | if on { | |
| 203 | - | v = 1 | |
| 204 | - | } | |
| 205 | - | // SETPROPERTY is a blocking atomic commit (about one refresh). | |
| 206 | - | start := time.Now() | |
| 207 | - | if err := setProp(o.fd, o.crtc, objCrtc, o.vrrProp, v); err != nil { | |
| 208 | - | if lostMaster(err) { | |
| 209 | - | return // switched away: retried on the next change | |
| 210 | - | } | |
| 211 | - | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr; disabled") | |
| 212 | - | o.vrrProp, o.vrrOn = 0, false | |
| 213 | - | return | |
| 214 | - | } | |
| 215 | - | o.vrrOn = on | |
| 216 | - | o.log.Info().Bool("vrr", on).Str("connector", o.conn.name).Dur("took", time.Since(start)).Msg("vrr") | |
| 217 | - | } | |
| 218 | - | ||
| 219 | 462 | // setAsync logs when the output starts or stops tearing. | |
| 220 | 463 | func (o *Output) setAsync(on bool) { | |
| 221 | 464 | if on != o.async { | |
| @@ -227,30 +470,62 @@ func (o *Output) setAsync(on bool) { | |||
| 227 | 470 | // Close restores the CRTC state found at startup, when it had a mode, and | |
| 228 | 471 | // frees buffers. | |
| 229 | 472 | func (o *Output) Close() { | |
| 473 | + | req := &atomicReq{} | |
| 230 | 474 | if o.cursor != nil { | |
| 231 | - | o.cursor.close() | |
| 232 | - | } | |
| 233 | - | o.setVRR(false) | |
| 234 | - | if o.saved.crtcID != 0 { | |
| 235 | - | ids := []uint32{o.conn.id} | |
| 236 | - | s := o.saved | |
| 237 | - | if s.modeValid != 0 { | |
| 238 | - | _ = setCrtc(o.fd, s.crtcID, ids[0], s.fbID, &s.mode) | |
| 475 | + | o.cursor.props(req, o.crtc, cursorState{}) | |
| 476 | + | } | |
| 477 | + | req.set(o.crtc, o.vrrProp, 0) | |
| 478 | + | s := o.saved | |
| 479 | + | var blob uint32 | |
| 480 | + | if s.crtcID != 0 && s.modeValid != 0 && s.fbID != 0 { | |
| 481 | + | mode := s.mode | |
| 482 | + | if b, err := o.k.createBlob(unsafe.Slice((*byte)(unsafe.Pointer(&mode)), unsafe.Sizeof(mode))); err == nil { | |
| 483 | + | blob = b | |
| 484 | + | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(b)) | |
| 485 | + | req.set(o.crtc, o.crtcProps["ACTIVE"], 1) | |
| 486 | + | p := o.primary | |
| 487 | + | req.set(p.id, p.prop("FB_ID"), uint64(s.fbID)) | |
| 488 | + | req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc)) | |
| 489 | + | req.set(p.id, p.prop("SRC_X"), 0) | |
| 490 | + | req.set(p.id, p.prop("SRC_Y"), 0) | |
| 491 | + | req.set(p.id, p.prop("SRC_W"), uint64(s.mode.HDisplay)<<16) | |
| 492 | + | req.set(p.id, p.prop("SRC_H"), uint64(s.mode.VDisplay)<<16) | |
| 493 | + | req.set(p.id, p.prop("CRTC_X"), 0) | |
| 494 | + | req.set(p.id, p.prop("CRTC_Y"), 0) | |
| 495 | + | req.set(p.id, p.prop("CRTC_W"), uint64(s.mode.HDisplay)) | |
| 496 | + | req.set(p.id, p.prop("CRTC_H"), uint64(s.mode.VDisplay)) | |
| 239 | 497 | } | |
| 240 | 498 | } | |
| 499 | + | // Best effort: at exit the card may already belong to another session. | |
| 500 | + | _ = o.k.commit(req, atomicAllowModes, 0) | |
| 501 | + | if blob != 0 { | |
| 502 | + | _ = o.k.destroyBlob(blob) | |
| 503 | + | } | |
| 504 | + | if o.modeBlob != 0 { | |
| 505 | + | _ = o.k.destroyBlob(o.modeBlob) | |
| 506 | + | o.modeBlob = 0 | |
| 507 | + | } | |
| 508 | + | if o.cursor != nil { | |
| 509 | + | o.cursor.free(o.k) | |
| 510 | + | } | |
| 241 | 511 | o.freeImages() | |
| 242 | 512 | o.shown, o.queued = 0, 0 | |
| 243 | 513 | o.dropClientFBs(time.Now(), true) | |
| 244 | 514 | } | |
| 245 | 515 | ||
| 246 | - | // Run renders scenes and flips until ctx ends. active reports seat enable/disable. | |
| 247 | - | // Each completed page flip is reported on presented (dropped when full). | |
| 516 | + | // Run renders scenes and commits them until ctx ends. active reports seat | |
| 517 | + | // enable/disable. What the output shows and read is reported on presented. | |
| 248 | 518 | func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, presented chan<- ports.OutputPresented) error { | |
| 249 | 519 | r, err := newRenderer(o.Width(), o.Height()) | |
| 250 | 520 | if err != nil { | |
| 251 | 521 | return fmt.Errorf("create renderer: %w", err) | |
| 252 | 522 | } | |
| 253 | 523 | defer r.Close() | |
| 524 | + | if o.cursor != nil { | |
| 525 | + | if err := o.cursor.setup(o.k, r); err != nil { | |
| 526 | + | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images") | |
| 527 | + | } | |
| 528 | + | } | |
| 254 | 529 | // The seat sends its state first; while switched away (e.g. a monitor | |
| 255 | 530 | // plugged in on another VT) the modeset waits for the enable. | |
| 256 | 531 | enabled := true | |
| @@ -258,8 +533,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 258 | 533 | case enabled = <-active: | |
| 259 | 534 | default: | |
| 260 | 535 | } | |
| 261 | - | // Output images, from the best to the fallback (ADR 014). While | |
| 262 | - | // switched away they are validated by the first modeset on enable. | |
| 536 | + | // Output images, from the best to the linear fallback (ADR 014). | |
| 537 | + | // While switched away they are validated by the first modeset on enable. | |
| 263 | 538 | if enabled { | |
| 264 | 539 | err = o.showImages(r, imagesDriver, nil) | |
| 265 | 540 | } else { | |
| @@ -268,6 +543,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 268 | 543 | if err != nil { | |
| 269 | 544 | return err | |
| 270 | 545 | } | |
| 546 | + | if enabled { | |
| 547 | + | o.probeAsync(r) | |
| 548 | + | } | |
| 271 | 549 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | |
| 272 | 550 | var scene ports.Scene | |
| 273 | 551 | haveScene, dirty := false, false | |
| @@ -276,36 +554,39 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 276 | 554 | frame := 0 | |
| 277 | 555 | stats := time.NewTicker(10 * time.Second) | |
| 278 | 556 | defer stats.Stop() | |
| 279 | - | // seen is the latest content Seq per window, reported to wayland with | |
| 280 | - | // what the output scans out, after each frame decision. | |
| 557 | + | // seen is the latest content Seq per window. It is reported only while | |
| 558 | + | // no frame is in flight: then the GPU has finished every frame that | |
| 559 | + | // read older buffers (the kernel waited for their fence to flip). | |
| 281 | 560 | seen := map[ports.WindowID]uint64{} | |
| 282 | 561 | reportDirty := false | |
| 283 | - | flipped := false | |
| 562 | + | var cursorWake <-chan struct{} | |
| 563 | + | if o.cursor != nil { | |
| 564 | + | cursorWake = o.cursor.wake | |
| 565 | + | } | |
| 284 | 566 | for { | |
| 285 | - | if flipped || reportDirty { | |
| 286 | - | o.report(presented, flipped, seen) | |
| 287 | - | flipped, reportDirty = false, false | |
| 288 | - | } else { | |
| 289 | - | o.flushReport(presented) | |
| 567 | + | if reportDirty && !o.pending { | |
| 568 | + | o.report(nil, seen) | |
| 569 | + | reportDirty = false | |
| 290 | 570 | } | |
| 571 | + | o.flushReport(presented) | |
| 291 | 572 | // An unsent report is retried soon, not only on the next event. | |
| 292 | 573 | var retry <-chan time.Time | |
| 293 | - | if o.unsent.Output != "" { | |
| 574 | + | if len(o.unsent) > 0 { | |
| 294 | 575 | retry = time.After(time.Millisecond) | |
| 295 | 576 | } | |
| 296 | 577 | // A composed screen that stopped changing still drops VRR on time. | |
| 297 | 578 | var vrrOff <-chan time.Time | |
| 298 | - | if enabled && o.vrrOn && !o.composedSince.IsZero() { | |
| 579 | + | if enabled && o.vrrOn && !o.pending && !o.composedSince.IsZero() { | |
| 299 | 580 | vrrOff = time.After(max(0, vrrHold-time.Since(o.composedSince))) | |
| 300 | 581 | } | |
| 582 | + | stateDirty := false | |
| 301 | 583 | select { | |
| 302 | 584 | case <-retry: | |
| 303 | 585 | continue | |
| 304 | 586 | case <-vrrOff: | |
| 305 | - | if o.setVRR(false); o.vrrOn { | |
| 306 | - | o.composedSince = time.Now() // failed: retry later, not in a loop | |
| 307 | - | } | |
| 308 | - | continue | |
| 587 | + | stateDirty = true | |
| 588 | + | case <-cursorWake: | |
| 589 | + | stateDirty = true | |
| 309 | 590 | case <-ctx.Done(): | |
| 310 | 591 | return nil | |
| 311 | 592 | case on := <-active: | |
| @@ -317,7 +598,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 317 | 598 | o.log.Warn().Err(err).Msg("resume") | |
| 318 | 599 | continue | |
| 319 | 600 | } | |
| 320 | - | if refused(err) && !o.validated && o.kind < imagesDumb { | |
| 601 | + | if refused(err) && !o.validated && o.kind < imagesLinear { | |
| 321 | 602 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(o.kind)).Msg("modeset refused the output images") | |
| 322 | 603 | o.freeImages() | |
| 323 | 604 | err = o.showImages(r, o.kind+1, err) | |
| @@ -327,40 +608,33 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 327 | 608 | return err | |
| 328 | 609 | } | |
| 329 | 610 | o.validated = true | |
| 611 | + | o.probeAsync(r) | |
| 330 | 612 | dirty = haveScene | |
| 331 | - | if o.cursor != nil { | |
| 332 | - | if err := o.cursor.Reapply(); err != nil { | |
| 333 | - | o.log.Warn().Err(err).Msg("cursor") | |
| 334 | - | } | |
| 335 | - | } | |
| 613 | + | stateDirty = true | |
| 336 | 614 | } | |
| 337 | 615 | enabled = on | |
| 338 | 616 | o.log.Info().Bool("enabled", on).Msg("output") | |
| 339 | - | case n := <-o.flipped: | |
| 617 | + | case ev := <-o.flipped: | |
| 340 | 618 | if !o.pending { | |
| 341 | 619 | continue // stale event from before a modeset | |
| 342 | 620 | } | |
| 343 | - | o.pending = false | |
| 344 | - | o.flips += n | |
| 345 | - | o.shown = o.queued | |
| 346 | - | o.queued = 0 | |
| 347 | - | flipped = true | |
| 348 | - | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | |
| 349 | - | o.log.Info().Dur("flip_ms", d).Msg("slow flip") | |
| 350 | - | } | |
| 621 | + | o.completed(ev, seen) | |
| 622 | + | reportDirty = false | |
| 623 | + | stateDirty = true | |
| 351 | 624 | case s := <-scenes: | |
| 352 | 625 | if o.cursor != nil && loadCursor != nil && s.Scale != cursorScale { | |
| 353 | 626 | cursorScale = s.Scale | |
| 354 | - | o.setCursor(loadCursor, want, s.Scale) | |
| 627 | + | o.setCursor(r, loadCursor, want, s.Scale) | |
| 628 | + | stateDirty = true | |
| 355 | 629 | } | |
| 356 | 630 | scene, haveScene, dirty = s, true, true | |
| 357 | 631 | case c := <-cursor: | |
| 358 | 632 | want = c | |
| 359 | 633 | // Before the first scene the scale is unknown: loaded then. | |
| 360 | 634 | if o.cursor != nil && loadCursor != nil && cursorScale > 0 { | |
| 361 | - | o.setCursor(loadCursor, want, cursorScale) | |
| 635 | + | o.setCursor(r, loadCursor, want, cursorScale) | |
| 636 | + | stateDirty = true | |
| 362 | 637 | } | |
| 363 | - | continue | |
| 364 | 638 | case c := <-contents: | |
| 365 | 639 | if c.Empty() { | |
| 366 | 640 | delete(surfaces, c.ID) | |
| @@ -378,50 +652,100 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 378 | 652 | ev := o.log.Info().Int("frames", frame).Int("flips", o.flips) | |
| 379 | 653 | if o.cursor != nil { | |
| 380 | 654 | cs := o.cursor.TakeStats() | |
| 381 | - | ev = ev.Int("cursor_moves", cs.Moves).Int("cursor_ioctls", cs.Ioctls).Dur("cursor_max_ioctl_ms", cs.MaxIoctl) | |
| 655 | + | ev = ev.Int("cursor_moves", cs.Moves).Int("cursor_commits", cs.Commits) | |
| 382 | 656 | } | |
| 383 | 657 | ev.Msg("stats") | |
| 384 | 658 | } | |
| 385 | - | if !dirty || !haveScene || !enabled || o.pending { | |
| 659 | + | if !enabled || o.pending { | |
| 660 | + | continue | |
| 661 | + | } | |
| 662 | + | if !dirty || !haveScene { | |
| 663 | + | if stateDirty { | |
| 664 | + | if err := o.commitState(o.wantVRR(o.shown != 0)); err != nil && !o.commitFailed(err, &enabled) { | |
| 665 | + | return fmt.Errorf("commit: %w", err) | |
| 666 | + | } | |
| 667 | + | } | |
| 386 | 668 | continue | |
| 387 | 669 | } | |
| 388 | 670 | start := time.Now() | |
| 389 | - | direct, err := o.tryScanout(scene, surfaces) | |
| 671 | + | f := pendingFrame{shows: maps.Clone(seen)} | |
| 672 | + | fb, c := o.scanoutFrame(scene, surfaces) | |
| 673 | + | direct := false | |
| 674 | + | if fb != 0 { | |
| 675 | + | direct, err = o.commitScanout(fb, c, f) | |
| 676 | + | } | |
| 390 | 677 | if !direct { | |
| 391 | 678 | r.UseTarget(o.back) | |
| 392 | - | if err := r.Render(scene, surfaces); err != nil { | |
| 393 | - | return fmt.Errorf("render frame: %w", err) | |
| 679 | + | done, rerr := r.Render(scene, surfaces) | |
| 680 | + | if rerr != nil { | |
| 681 | + | return fmt.Errorf("render frame: %w", rerr) | |
| 682 | + | } | |
| 683 | + | err = o.commitFrame(o.fbs[o.back], done, false, o.wantVRR(false), f) | |
| 684 | + | if done != nil { | |
| 685 | + | done.Close() | |
| 686 | + | } | |
| 687 | + | if err == nil { | |
| 688 | + | o.queued = 0 | |
| 689 | + | o.back = 1 - o.back | |
| 394 | 690 | } | |
| 395 | - | err = o.present(r) | |
| 396 | 691 | } | |
| 397 | - | copyStart := time.Now() | |
| 398 | 692 | if err != nil { | |
| 399 | - | if errors.Is(err, unix.EBUSY) { | |
| 400 | - | // A flip is still in flight; retry after its event. | |
| 401 | - | o.pending, o.flipStart = true, time.Now() | |
| 402 | - | continue // dirty stays true | |
| 693 | + | if !o.commitFailed(err, &enabled) { | |
| 694 | + | return fmt.Errorf("page flip: %w", err) | |
| 403 | 695 | } | |
| 404 | - | if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) { | |
| 405 | - | // Lost DRM master mid-switch; the seat disable follows. | |
| 406 | - | o.log.Warn().Err(err).Msg("flip refused") | |
| 407 | - | enabled = false | |
| 408 | - | continue | |
| 409 | - | } | |
| 410 | - | o.log.Error().Err(err).Msg("page flip") | |
| 411 | - | return fmt.Errorf("page flip: %w", err) | |
| 696 | + | continue // dirty stays true | |
| 412 | 697 | } | |
| 413 | 698 | dirty = false | |
| 414 | - | reportDirty = true | |
| 415 | 699 | frame++ | |
| 416 | - | o.log.Debug().Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Dur("render", time.Since(start)).Dur("copy", time.Since(copyStart)).Msg("frame") | |
| 700 | + | o.log.Debug().Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Bool("direct", direct).Dur("took", time.Since(start)).Msg("frame") | |
| 701 | + | } | |
| 702 | + | } | |
| 703 | + | ||
| 704 | + | // commitFailed handles a commit error that does not stop the output: a | |
| 705 | + | // commit still in flight (EBUSY, retried after its event) or DRM master | |
| 706 | + | // lost mid-switch (the seat disable follows). | |
| 707 | + | func (o *Output) commitFailed(err error, enabled *bool) bool { | |
| 708 | + | switch { | |
| 709 | + | case errors.Is(err, unix.EBUSY): | |
| 710 | + | o.pending, o.pendingFrame, o.flipStart = true, pendingFrame{}, time.Now() | |
| 711 | + | return true | |
| 712 | + | case lostMaster(err): | |
| 713 | + | o.log.Warn().Err(err).Msg("commit refused") | |
| 714 | + | *enabled = false | |
| 715 | + | return true | |
| 716 | + | } | |
| 717 | + | o.log.Error().Err(err).Msg("commit") | |
| 718 | + | return false | |
| 719 | + | } | |
| 720 | + | ||
| 721 | + | // completed handles the event of the pending commit: a flipped frame is | |
| 722 | + | // reported with its kernel timestamp. | |
| 723 | + | func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) { | |
| 724 | + | f := o.pendingFrame | |
| 725 | + | o.pending, o.pendingFrame = false, pendingFrame{} | |
| 726 | + | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | |
| 727 | + | o.log.Info().Dur("flip_ms", d).Msg("slow flip") | |
| 728 | + | } | |
| 729 | + | if !f.frame { | |
| 730 | + | return | |
| 731 | + | } | |
| 732 | + | o.flips++ | |
| 733 | + | o.shown = f.queued | |
| 734 | + | if o.queued == f.queued { | |
| 735 | + | o.queued = 0 | |
| 736 | + | } | |
| 737 | + | refresh := time.Duration(0) | |
| 738 | + | if !o.vrrOn { | |
| 739 | + | refresh = time.Duration(int64(time.Second) * 1000 / int64(max(1, o.mode.refreshMilli()))) | |
| 417 | 740 | } | |
| 741 | + | o.report(&ports.FlipInfo{When: ev.when, Seq: uint64(ev.seq), Refresh: refresh, ZeroCopy: f.zeroCopy, Async: f.async, HardwareClock: true, Shows: f.shows}, seen) | |
| 418 | 742 | } | |
| 419 | 743 | ||
| 420 | - | // setCursor loads a cursor for scale onto the cursor plane. | |
| 421 | - | func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float64) { | |
| 744 | + | // setCursor loads a cursor for scale into the cursor images. | |
| 745 | + | func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64) { | |
| 422 | 746 | img, err := load(c, scale, o.cursor.Limit()) | |
| 423 | 747 | if err == nil { | |
| 424 | - | err = o.cursor.SetImage(img.Pixels, img.W, img.H, img.HotX, img.HotY) | |
| 748 | + | err = o.cursor.setImage(r, img.Pixels, img.W, img.H, img.HotX, img.HotY) | |
| 425 | 749 | } | |
| 426 | 750 | if err != nil { | |
| 427 | 751 | o.log.Warn().Err(err).Msg("cursor") | |
| @@ -430,26 +754,47 @@ func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float6 | |||
| 430 | 754 | o.log.Debug().Float64("scale", scale).Str("shape", c.Shape).Bool("client", c.Image != nil).Int("w", img.W).Int("h", img.H).Msg("cursor image") | |
| 431 | 755 | } | |
| 432 | 756 | ||
| 433 | - | // report tells wayland what the output shows and has read, so replaced | |
| 434 | - | // client buffers can be released. It must follow the frame decision: a | |
| 435 | - | // buffer is safe once a later content is seen and it is neither shown nor | |
| 436 | - | // queued. A report the channel cannot take waits in unsent, replaced by | |
| 437 | - | // newer ones (Flip kept), and is retried on the next loop. | |
| 438 | - | func (o *Output) report(presented chan<- ports.OutputPresented, flip bool, seen map[ports.WindowID]uint64) { | |
| 439 | - | r := ports.OutputPresented{Output: o.conn.name, Flip: flip || o.unsent.Flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)} | |
| 440 | - | o.unsent = r | |
| 441 | - | o.flushReport(presented) | |
| 442 | - | } | |
| 757 | + | // maxUnsent bounds reports waiting for wayland; past it the oldest flip is | |
| 758 | + | // folded into the next one. | |
| 759 | + | const maxUnsent = 16 | |
| 443 | 760 | ||
| 444 | - | // flushReport sends the unsent report, if any, without blocking. | |
| 445 | - | func (o *Output) flushReport(presented chan<- ports.OutputPresented) { | |
| 446 | - | if o.unsent.Output == "" { | |
| 761 | + | // report queues what the output shows and has read, so replaced client | |
| 762 | + | // buffers can be released. A flip-less report merges into the newest | |
| 763 | + | // queued one; flips are never merged unless maxUnsent is reached. | |
| 764 | + | func (o *Output) report(flip *ports.FlipInfo, seen map[ports.WindowID]uint64) { | |
| 765 | + | r := ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)} | |
| 766 | + | if n := len(o.unsent); n > 0 && (flip == nil || o.unsent[n-1].Flip == nil) { | |
| 767 | + | if flip == nil { | |
| 768 | + | r.Flip = o.unsent[n-1].Flip | |
| 769 | + | } | |
| 770 | + | o.unsent[n-1] = r | |
| 447 | 771 | return | |
| 448 | 772 | } | |
| 449 | - | select { | |
| 450 | - | case presented <- o.unsent: | |
| 451 | - | o.unsent = ports.OutputPresented{} | |
| 452 | - | default: | |
| 773 | + | o.unsent = append(o.unsent, r) | |
| 774 | + | if len(o.unsent) > maxUnsent { | |
| 775 | + | old, next := o.unsent[0].Flip, o.unsent[1].Flip | |
| 776 | + | if next != nil && old != nil { | |
| 777 | + | next.Merged += 1 + old.Merged | |
| 778 | + | for id, seq := range old.Shows { | |
| 779 | + | if _, ok := next.Shows[id]; !ok { | |
| 780 | + | next.Shows[id] = seq | |
| 781 | + | } | |
| 782 | + | } | |
| 783 | + | } | |
| 784 | + | o.unsent = slices.Delete(o.unsent, 0, 1) | |
| 785 | + | o.log.Warn().Str("connector", o.conn.name).Msg("wayland is not reading output reports; flips merged") | |
| 786 | + | } | |
| 787 | + | } | |
| 788 | + | ||
| 789 | + | // flushReport sends queued reports without blocking. | |
| 790 | + | func (o *Output) flushReport(presented chan<- ports.OutputPresented) { | |
| 791 | + | for len(o.unsent) > 0 { | |
| 792 | + | select { | |
| 793 | + | case presented <- o.unsent[0]: | |
| 794 | + | o.unsent = slices.Delete(o.unsent, 0, 1) | |
| 795 | + | default: | |
| 796 | + | return | |
| 797 | + | } | |
| 453 | 798 | } | |
| 454 | 799 | } | |
| 455 | 800 | ||
| @@ -459,7 +804,6 @@ type imageKind int | |||
| 459 | 804 | const ( | |
| 460 | 805 | imagesDriver imageKind = iota // exported, modifier chosen by the driver | |
| 461 | 806 | imagesLinear // exported, linear | |
| 462 | - | imagesDumb // dumb buffers filled by CPU copies | |
| 463 | 807 | ) | |
| 464 | 808 | ||
| 465 | 809 | // showImages sets up images from kind on and modesets them. KMS may refuse | |
| @@ -476,14 +820,11 @@ func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error | |||
| 476 | 820 | o.validated = true | |
| 477 | 821 | return nil | |
| 478 | 822 | } | |
| 479 | - | if !refused(err) { | |
| 823 | + | if !refused(err) || got == imagesLinear { | |
| 480 | 824 | return err | |
| 481 | 825 | } | |
| 482 | 826 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images") | |
| 483 | 827 | o.freeImages() | |
| 484 | - | if got == imagesDumb { | |
| 485 | - | return err | |
| 486 | - | } | |
| 487 | 828 | kind, cause = got+1, err | |
| 488 | 829 | } | |
| 489 | 830 | } | |
| @@ -493,17 +834,17 @@ func refused(err error) bool { | |||
| 493 | 834 | return errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) | |
| 494 | 835 | } | |
| 495 | 836 | ||
| 496 | - | // lostMaster reports a modeset error meaning the seat is switched away. | |
| 837 | + | // lostMaster reports a commit error meaning the seat is switched away. | |
| 497 | 838 | func lostMaster(err error) bool { | |
| 498 | 839 | return errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) | |
| 499 | 840 | } | |
| 500 | 841 | ||
| 501 | - | // setupImages gives the output the two images it flips between: renderer | |
| 502 | - | // targets exported as dmabufs, or dumb buffers as a logged fallback. A | |
| 503 | - | // kind that fails falls through to the next one. | |
| 842 | + | // setupImages gives the output the two renderer images it flips between, | |
| 843 | + | // exported as dmabufs. There is no CPU fallback (ADR 014): a GPU that | |
| 844 | + | // cannot export a scanout-capable image cannot drive the output. | |
| 504 | 845 | func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) { | |
| 505 | 846 | err := cause | |
| 506 | - | for ; kind < imagesDumb; kind++ { | |
| 847 | + | for ; kind <= imagesLinear; kind++ { | |
| 507 | 848 | mods := []uint64(nil) | |
| 508 | 849 | if kind == imagesLinear { | |
| 509 | 850 | mods = []uint64{0} | |
| @@ -513,17 +854,7 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima | |||
| 513 | 854 | } | |
| 514 | 855 | o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export") | |
| 515 | 856 | } | |
| 516 | - | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("output falls back to CPU copies (ADR 014)") | |
| 517 | - | // The renderer draws into its own image again, read back by CopyBGRX. | |
| 518 | - | _, _ = r.ExportTargets(0, nil) | |
| 519 | - | for i := range o.bufs { | |
| 520 | - | if o.bufs[i], err = newDumb(o.fd, o.Width(), o.Height()); err != nil { | |
| 521 | - | o.freeImages() | |
| 522 | - | return imagesDumb, err | |
| 523 | - | } | |
| 524 | - | o.fbs[i] = o.bufs[i].fbID | |
| 525 | - | } | |
| 526 | - | return imagesDumb, nil | |
| 857 | + | return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err) | |
| 527 | 858 | } | |
| 528 | 859 | ||
| 529 | 860 | // exportImages makes the renderer's exported targets the output images. | |
| @@ -534,7 +865,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error { | |||
| 534 | 865 | } | |
| 535 | 866 | for i := range bufs { | |
| 536 | 867 | if err == nil { | |
| 537 | - | o.fbs[i], err = o.card.addFB(&bufs[i]) | |
| 868 | + | o.fbs[i], err = o.k.addFB(&bufs[i], fourccXRGB) | |
| 538 | 869 | } | |
| 539 | 870 | bufs[i].Planes[0].File.Close() | |
| 540 | 871 | } | |
| @@ -545,7 +876,12 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error { | |||
| 545 | 876 | break | |
| 546 | 877 | } | |
| 547 | 878 | r.UseTarget(i) | |
| 548 | - | err = r.Render(ports.Scene{Background: "#000000"}, nil) | |
| 879 | + | var done *os.File | |
| 880 | + | if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil { | |
| 881 | + | // The modeset is a blocking commit: wait for the clear. | |
| 882 | + | err = errors.Join(err, waitFence(done)) | |
| 883 | + | done.Close() | |
| 884 | + | } | |
| 549 | 885 | } | |
| 550 | 886 | if err != nil { | |
| 551 | 887 | o.freeImages() | |
| @@ -556,15 +892,22 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error { | |||
| 556 | 892 | return nil | |
| 557 | 893 | } | |
| 558 | 894 | ||
| 559 | - | // freeImages removes the output images; dumb buffers own their fb. | |
| 895 | + | // waitFence blocks until a sync file signals (setup only, never per frame). | |
| 896 | + | func waitFence(f *os.File) error { | |
| 897 | + | fds := []unix.PollFd{{Fd: int32(f.Fd()), Events: unix.POLLIN}} | |
| 898 | + | for { | |
| 899 | + | _, err := unix.Poll(fds, -1) | |
| 900 | + | if !errors.Is(err, unix.EINTR) { | |
| 901 | + | return err | |
| 902 | + | } | |
| 903 | + | } | |
| 904 | + | } | |
| 905 | + | ||
| 906 | + | // freeImages removes the output images' framebuffers. | |
| 560 | 907 | func (o *Output) freeImages() { | |
| 561 | 908 | for i, fb := range o.fbs { | |
| 562 | - | if b := o.bufs[i]; b != nil { | |
| 563 | - | b.destroy(o.fd) | |
| 564 | - | o.bufs[i] = nil | |
| 565 | - | } else if fb != 0 { | |
| 566 | - | v := fb | |
| 567 | - | _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v)) | |
| 909 | + | if fb != 0 { | |
| 910 | + | _ = o.k.rmFB(fb) | |
| 568 | 911 | } | |
| 569 | 912 | o.fbs[i] = 0 | |
| 570 | 913 | } | |
A internal/adapters/drm/output_test.go +436 -0
| @@ -0,0 +1,436 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "os" | |
| 6 | + | "sync" | |
| 7 | + | "testing" | |
| 8 | + | "time" | |
| 9 | + | ||
| 10 | + | portsmocks "github.com/bnema/nefertty/internal/mocks/ports" | |
| 11 | + | "github.com/bnema/nefertty/internal/ports" | |
| 12 | + | "github.com/bnema/zerowrap" | |
| 13 | + | "github.com/stretchr/testify/mock" | |
| 14 | + | "golang.org/x/sys/unix" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // Object and property IDs of the test output. | |
| 18 | + | const ( | |
| 19 | + | tCrtc, tConn, tPrimary, tCursor, tOverlay = 30, 40, 50, 51, 52 | |
| 20 | + | pFB, pCrtcID, pFence, pMode, pActive = 1, 2, 3, 4, 5 | |
| 21 | + | pVRR, pConnCrtc = 6, 7 | |
| 22 | + | ) | |
| 23 | + | ||
| 24 | + | var planeProps = map[string]uint32{"FB_ID": pFB, "CRTC_ID": pCrtcID, "IN_FENCE_FD": pFence, "SRC_X": 10, "SRC_Y": 11, "SRC_W": 12, "SRC_H": 13, "CRTC_X": 14, "CRTC_Y": 15, "CRTC_W": 16, "CRTC_H": 17} | |
| 25 | + | ||
| 26 | + | // commitMu guards recorded commits (Run commits on its goroutine). | |
| 27 | + | var commitMu sync.Mutex | |
| 28 | + | ||
| 29 | + | // commitRec is one recorded atomic commit. | |
| 30 | + | type commitRec struct { | |
| 31 | + | req atomicReq | |
| 32 | + | flags uint32 | |
| 33 | + | user uint64 | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // testOutput returns an output on a mocked kms that records commits; | |
| 37 | + | // commit returns the next error of errs (nil when empty). | |
| 38 | + | func testOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) { | |
| 39 | + | k := newMockkms(t) | |
| 40 | + | var commits []commitRec | |
| 41 | + | k.EXPECT().commit(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(r *atomicReq, flags uint32, user uint64) error { | |
| 42 | + | commitMu.Lock() | |
| 43 | + | defer commitMu.Unlock() | |
| 44 | + | c := commitRec{flags: flags, user: user} | |
| 45 | + | c.req.objs = append([]uint32(nil), r.objs...) | |
| 46 | + | for _, ps := range r.props { | |
| 47 | + | c.req.props = append(c.req.props, append([]propValue(nil), ps...)) | |
| 48 | + | } | |
| 49 | + | commits = append(commits, c) | |
| 50 | + | if len(errs) > 0 { | |
| 51 | + | err := errs[0] | |
| 52 | + | errs = errs[1:] | |
| 53 | + | return err | |
| 54 | + | } | |
| 55 | + | return nil | |
| 56 | + | }).Maybe() | |
| 57 | + | primary := &plane{id: tPrimary, typ: planePrimary, props: planeProps} | |
| 58 | + | cur := newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, 64) | |
| 59 | + | cur.fbs = [2]uint32{90, 91} | |
| 60 | + | o := &Output{k: k, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(), | |
| 61 | + | crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR, | |
| 62 | + | primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true} | |
| 63 | + | return o, k, &commits | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func objCount(c commitRec) int { return len(c.req.objs) } | |
| 67 | + | ||
| 68 | + | func TestFrameCommitCarriesPrimaryFenceCursorAndVRR(t *testing.T) { | |
| 69 | + | o, _, commits := testOutput(t) | |
| 70 | + | o.cursor.image = true | |
| 71 | + | o.cursor.Move(12, 34) | |
| 72 | + | fence, w, err := os.Pipe() | |
| 73 | + | if err != nil { | |
| 74 | + | t.Fatal(err) | |
| 75 | + | } | |
| 76 | + | defer fence.Close() | |
| 77 | + | defer w.Close() | |
| 78 | + | if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil { | |
| 79 | + | t.Fatal(err) | |
| 80 | + | } | |
| 81 | + | c := (*commits)[0] | |
| 82 | + | if c.flags != atomicNonblock|flipEventFlag || c.user != userFrame { | |
| 83 | + | t.Fatalf("flags %#x user %d", c.flags, c.user) | |
| 84 | + | } | |
| 85 | + | if v, _ := c.req.value(tPrimary, pFB); v != 70 { | |
| 86 | + | t.Fatalf("primary fb %d", v) | |
| 87 | + | } | |
| 88 | + | if v, ok := c.req.value(tPrimary, pFence); !ok || v != uint64(fence.Fd()) { | |
| 89 | + | t.Fatalf("fence %d %v", v, ok) | |
| 90 | + | } | |
| 91 | + | if v, _ := c.req.value(tCursor, 14); v != 12 { | |
| 92 | + | t.Fatalf("cursor x %d", v) | |
| 93 | + | } | |
| 94 | + | if v, _ := c.req.value(tCrtc, pVRR); v != 1 { | |
| 95 | + | t.Fatalf("vrr %d", v) | |
| 96 | + | } | |
| 97 | + | if !o.pending || !o.vrrOn || o.cursor.applied.x != 12 { | |
| 98 | + | t.Fatalf("state pending=%v vrr=%v cursor=%+v", o.pending, o.vrrOn, o.cursor.applied) | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func TestAsyncCommitHasOnlyPrimaryFBAndFence(t *testing.T) { | |
| 103 | + | o, _, commits := testOutput(t) | |
| 104 | + | fence, w, _ := os.Pipe() | |
| 105 | + | defer fence.Close() | |
| 106 | + | defer w.Close() | |
| 107 | + | if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil { | |
| 108 | + | t.Fatal(err) | |
| 109 | + | } | |
| 110 | + | c := (*commits)[0] | |
| 111 | + | if c.flags&flipAsyncFlag == 0 { | |
| 112 | + | t.Fatal("not async") | |
| 113 | + | } | |
| 114 | + | if objCount(c) != 1 || c.req.objs[0] != tPrimary || len(c.req.props[0]) != 2 { | |
| 115 | + | t.Fatalf("async commit %+v", c.req) | |
| 116 | + | } | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | func TestAsyncFallsBackToSyncWhenCursorMoves(t *testing.T) { | |
| 120 | + | o, _, commits := testOutput(t) | |
| 121 | + | o.cursor.image = true | |
| 122 | + | o.cursor.Move(5, 5) | |
| 123 | + | if err := o.commitFrame(70, nil, true, false, pendingFrame{}); err != nil { | |
| 124 | + | t.Fatal(err) | |
| 125 | + | } | |
| 126 | + | if (*commits)[0].flags&flipAsyncFlag != 0 { | |
| 127 | + | t.Fatal("async commit moved the cursor") | |
| 128 | + | } | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | func TestAsyncWithoutFenceSupportStaysSync(t *testing.T) { | |
| 132 | + | o, _, commits := testOutput(t) | |
| 133 | + | o.asyncFence = false | |
| 134 | + | fence, w, _ := os.Pipe() | |
| 135 | + | defer fence.Close() | |
| 136 | + | defer w.Close() | |
| 137 | + | if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil { | |
| 138 | + | t.Fatal(err) | |
| 139 | + | } | |
| 140 | + | if (*commits)[0].flags&flipAsyncFlag != 0 { | |
| 141 | + | t.Fatal("async commit with a fence the driver refuses") | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | func TestScanoutAsyncRefusedRetriesSync(t *testing.T) { | |
| 146 | + | o, _, commits := testOutput(t, unix.EINVAL) | |
| 147 | + | o.shown, o.vrrOn = 1, true | |
| 148 | + | buf := &ports.DMABuf{ID: 9} | |
| 149 | + | o.clientFBs[9] = &clientFB{fbID: 80} | |
| 150 | + | ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: buf, Async: true}, pendingFrame{}) | |
| 151 | + | if !ok || err != nil { | |
| 152 | + | t.Fatalf("ok=%v err=%v", ok, err) | |
| 153 | + | } | |
| 154 | + | if len(*commits) != 2 || (*commits)[0].flags&flipAsyncFlag == 0 || (*commits)[1].flags&flipAsyncFlag != 0 { | |
| 155 | + | t.Fatalf("commits %+v", *commits) | |
| 156 | + | } | |
| 157 | + | if !o.clientFBs[9].noAsync || o.queued != 9 { | |
| 158 | + | t.Fatalf("noAsync=%v queued=%d", o.clientFBs[9].noAsync, o.queued) | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | func TestCursorCommitWaitsForEvent(t *testing.T) { | |
| 163 | + | o, _, commits := testOutput(t) | |
| 164 | + | o.cursor.image = true | |
| 165 | + | o.cursor.Move(1, 1) | |
| 166 | + | if err := o.commitState(false); err != nil { | |
| 167 | + | t.Fatal(err) | |
| 168 | + | } | |
| 169 | + | c := (*commits)[0] | |
| 170 | + | if c.flags != atomicNonblock|flipEventFlag || c.user != userState || !o.pending { | |
| 171 | + | t.Fatalf("cursor commit flags %#x user %d pending %v", c.flags, c.user, o.pending) | |
| 172 | + | } | |
| 173 | + | // Its event is not a frame: nothing is reported. | |
| 174 | + | seen := map[ports.WindowID]uint64{} | |
| 175 | + | o.completed(flipEvent{user: userState}, seen) | |
| 176 | + | if o.pending || len(o.unsent) != 0 || o.flips != 0 { | |
| 177 | + | t.Fatalf("pending=%v unsent=%d flips=%d", o.pending, len(o.unsent), o.flips) | |
| 178 | + | } | |
| 179 | + | // Nothing changed: no commit. | |
| 180 | + | if err := o.commitState(false); err != nil || len(*commits) != 1 { | |
| 181 | + | t.Fatalf("idle commit: %v, %d commits", err, len(*commits)) | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | func TestBusyCommitStaysPending(t *testing.T) { | |
| 186 | + | o, _, _ := testOutput(t, unix.EBUSY) | |
| 187 | + | err := o.commitFrame(70, nil, false, false, pendingFrame{}) | |
| 188 | + | enabled := true | |
| 189 | + | if !o.commitFailed(err, &enabled) || !o.pending || !enabled { | |
| 190 | + | t.Fatalf("EBUSY: pending=%v enabled=%v", o.pending, enabled) | |
| 191 | + | } | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | func TestFlipReportsTimestampAndShownFrame(t *testing.T) { | |
| 195 | + | o, _, _ := testOutput(t) | |
| 196 | + | o.vrrOn = false | |
| 197 | + | o.mode.Clock, o.mode.HTotal, o.mode.VTotal = 0, 0, 0 | |
| 198 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{shows: map[ports.WindowID]uint64{1: 4}}); err != nil { | |
| 199 | + | t.Fatal(err) | |
| 200 | + | } | |
| 201 | + | o.completed(flipEvent{user: userFrame, when: 5 * time.Second, seq: 11}, map[ports.WindowID]uint64{1: 4}) | |
| 202 | + | if len(o.unsent) != 1 { | |
| 203 | + | t.Fatalf("reports %d", len(o.unsent)) | |
| 204 | + | } | |
| 205 | + | f := o.unsent[0].Flip | |
| 206 | + | if f == nil || f.When != 5*time.Second || f.Seq != 11 || f.Refresh != time.Second/60 || f.Shows[1] != 4 || !f.HardwareClock { | |
| 207 | + | t.Fatalf("flip %+v", f) | |
| 208 | + | } | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | func TestReportsKeepEveryFlip(t *testing.T) { | |
| 212 | + | o, _, _ := testOutput(t) | |
| 213 | + | seen := map[ports.WindowID]uint64{1: 1} | |
| 214 | + | o.report(&ports.FlipInfo{Seq: 1}, seen) | |
| 215 | + | o.report(nil, seen) | |
| 216 | + | o.report(&ports.FlipInfo{Seq: 2}, seen) | |
| 217 | + | o.report(nil, map[ports.WindowID]uint64{1: 3}) | |
| 218 | + | if len(o.unsent) != 2 || o.unsent[0].Flip.Seq != 1 || o.unsent[1].Flip.Seq != 2 || o.unsent[1].Seen[1] != 3 { | |
| 219 | + | t.Fatalf("unsent %+v", o.unsent) | |
| 220 | + | } | |
| 221 | + | for i := range maxUnsent + 3 { | |
| 222 | + | o.report(&ports.FlipInfo{Seq: uint64(10 + i), Shows: map[ports.WindowID]uint64{}}, seen) | |
| 223 | + | } | |
| 224 | + | if len(o.unsent) != maxUnsent || o.unsent[0].Flip.Merged == 0 { | |
| 225 | + | t.Fatalf("cap: %d reports, first merged %d", len(o.unsent), o.unsent[0].Flip.Merged) | |
| 226 | + | } | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | func TestModesetDisablesCursorAndStrayPlanes(t *testing.T) { | |
| 230 | + | o, k, commits := testOutput(t) | |
| 231 | + | stray := &plane{id: tOverlay, typ: planeOverlay, props: planeProps} | |
| 232 | + | o.stray = []*plane{stray} | |
| 233 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once() | |
| 234 | + | k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil).Once() | |
| 235 | + | if err := o.modeset(); err != nil { | |
| 236 | + | t.Fatal(err) | |
| 237 | + | } | |
| 238 | + | c := (*commits)[0] | |
| 239 | + | if c.flags != atomicAllowModes { | |
| 240 | + | t.Fatalf("flags %#x", c.flags) | |
| 241 | + | } | |
| 242 | + | if v, _ := c.req.value(tCrtc, pMode); v != 99 { | |
| 243 | + | t.Fatalf("mode blob %d", v) | |
| 244 | + | } | |
| 245 | + | for _, p := range []uint32{tCursor, tOverlay} { | |
| 246 | + | if v, ok := c.req.value(p, pFB); !ok || v != 0 { | |
| 247 | + | t.Fatalf("plane %d fb %d %v", p, v, ok) | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc { | |
| 251 | + | t.Fatalf("connector crtc %d", v) | |
| 252 | + | } | |
| 253 | + | // The next modeset replaces the blob. | |
| 254 | + | k.EXPECT().createBlob(mock.Anything).Return(100, nil).Once() | |
| 255 | + | k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Once() | |
| 256 | + | k.EXPECT().destroyBlob(uint32(99)).Return(nil).Once() | |
| 257 | + | if err := o.modeset(); err != nil { | |
| 258 | + | t.Fatal(err) | |
| 259 | + | } | |
| 260 | + | if _, ok := (*commits)[1].req.value(tOverlay, pFB); ok { | |
| 261 | + | t.Fatal("plane on another CRTC turned off") | |
| 262 | + | } | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | func TestAsyncProbeAfterModeset(t *testing.T) { | |
| 266 | + | o, _, commits := testOutput(t, unix.EINVAL) | |
| 267 | + | o.asyncFence = true | |
| 268 | + | r := portsmocks.NewMockRenderer(t) | |
| 269 | + | fence, w, _ := os.Pipe() | |
| 270 | + | defer w.Close() | |
| 271 | + | r.EXPECT().UseTarget(mock.Anything).Return().Once() | |
| 272 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(fence, nil).Once() | |
| 273 | + | o.probeAsync(r) | |
| 274 | + | c := (*commits)[0] | |
| 275 | + | if c.flags != atomicTestOnly|flipAsyncFlag { | |
| 276 | + | t.Fatalf("flags %#x", c.flags) | |
| 277 | + | } | |
| 278 | + | if _, ok := c.req.value(tPrimary, pFence); !ok { | |
| 279 | + | t.Fatal("probe without a fence") | |
| 280 | + | } | |
| 281 | + | if o.asyncFence { | |
| 282 | + | t.Fatal("refused probe kept asyncFence") | |
| 283 | + | } | |
| 284 | + | if _, err := fence.Stat(); err == nil { | |
| 285 | + | t.Fatal("probe fence not closed") | |
| 286 | + | } | |
| 287 | + | o.probeAsync(r) // once only | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | func TestVRRToggleOnlyInsideFrames(t *testing.T) { | |
| 291 | + | o, _, commits := testOutput(t) | |
| 292 | + | o.vrrOn = true | |
| 293 | + | o.composedSince = time.Now().Add(-time.Second) | |
| 294 | + | if o.wantVRR(false) { | |
| 295 | + | t.Fatal("VRR kept after vrrHold of composition") | |
| 296 | + | } | |
| 297 | + | o.composedSince = time.Now() | |
| 298 | + | if !o.wantVRR(false) { | |
| 299 | + | t.Fatal("VRR dropped before vrrHold") | |
| 300 | + | } | |
| 301 | + | if err := o.commitState(false); err != nil { | |
| 302 | + | t.Fatal(err) | |
| 303 | + | } | |
| 304 | + | if v, ok := (*commits)[0].req.value(tCrtc, pVRR); !ok || v != 0 || (*commits)[0].flags&flipEventFlag == 0 { | |
| 305 | + | t.Fatalf("vrr off commit %+v", (*commits)[0]) | |
| 306 | + | } | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | func TestReadPlanes(t *testing.T) { | |
| 310 | + | k := newMockkms(t) | |
| 311 | + | k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1}, {id: 2, possible: 2}, {id: 3, possible: 3}}, nil) | |
| 312 | + | k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}, "FB_ID": {21, 0}, "IN_FORMATS": {22, 5}}, nil) | |
| 313 | + | k.EXPECT().objProps(uint32(3), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planeCursor}}, nil) | |
| 314 | + | k.EXPECT().getBlob(uint32(5)).Return(nil, nil) | |
| 315 | + | ps, err := readPlanes(k, 0) | |
| 316 | + | if err != nil || len(ps) != 2 || ps[0].typ != planePrimary || ps[0].prop("FB_ID") != 21 || ps[1].typ != planeCursor { | |
| 317 | + | t.Fatalf("planes %+v %v", ps, err) | |
| 318 | + | } | |
| 319 | + | } | |
| 320 | + | ||
| 321 | + | func TestVRRRefusedIsDisabled(t *testing.T) { | |
| 322 | + | o, _, commits := testOutput(t, unix.EINVAL) | |
| 323 | + | if err := o.commitFrame(70, nil, false, true, pendingFrame{}); err != nil { | |
| 324 | + | t.Fatal(err) | |
| 325 | + | } | |
| 326 | + | if len(*commits) != 2 || o.vrrProp != 0 || !o.pending { | |
| 327 | + | t.Fatalf("%d commits, vrrProp %d", len(*commits), o.vrrProp) | |
| 328 | + | } | |
| 329 | + | if v, _ := (*commits)[1].req.value(tCrtc, pVRR); v != 0 { | |
| 330 | + | t.Fatal("retry kept VRR on") | |
| 331 | + | } | |
| 332 | + | } | |
| 333 | + | ||
| 334 | + | func TestBufferRefusedIsNotBlamedOnVRR(t *testing.T) { | |
| 335 | + | o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL) | |
| 336 | + | o.clientFBs[9] = &clientFB{fbID: 80} | |
| 337 | + | ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{}) | |
| 338 | + | if ok || err != nil || o.vrrProp == 0 || o.clientFBs[9].failed != "flip_refused" { | |
| 339 | + | t.Fatalf("ok=%v err=%v vrrProp=%d failed=%q", ok, err, o.vrrProp, o.clientFBs[9].failed) | |
| 340 | + | } | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | // Run: while a frame is in flight its buffers may still be read by the | |
| 344 | + | // GPU, so a newer content is reported only after the flip event; every | |
| 345 | + | // frame fence is closed. | |
| 346 | + | func TestRunReportsSeenAfterFlip(t *testing.T) { | |
| 347 | + | o, k, commits := testOutput(t) | |
| 348 | + | o.cursor, o.tearing = nil, false | |
| 349 | + | flips := make(chan flipEvent, 1) | |
| 350 | + | o.flipped = flips | |
| 351 | + | r := portsmocks.NewMockRenderer(t) | |
| 352 | + | var mu sync.Mutex | |
| 353 | + | var fences []*os.File | |
| 354 | + | pipeBuf := func() ports.DMABuf { | |
| 355 | + | f, w, _ := os.Pipe() | |
| 356 | + | w.Close() | |
| 357 | + | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |
| 358 | + | } | |
| 359 | + | r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once() | |
| 360 | + | r.EXPECT().UseTarget(mock.Anything).Return() | |
| 361 | + | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |
| 362 | + | f, w, _ := os.Pipe() | |
| 363 | + | w.Close() // readable: signalled | |
| 364 | + | mu.Lock() | |
| 365 | + | fences = append(fences, f) | |
| 366 | + | mu.Unlock() | |
| 367 | + | return f, nil | |
| 368 | + | }) | |
| 369 | + | r.EXPECT().Close().Return().Once() | |
| 370 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |
| 371 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |
| 372 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe() | |
| 373 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |
| 374 | + | frameCommits := func() int { | |
| 375 | + | commitMu.Lock() | |
| 376 | + | defer commitMu.Unlock() | |
| 377 | + | n := 0 | |
| 378 | + | for _, c := range *commits { | |
| 379 | + | if c.user == userFrame { | |
| 380 | + | n++ | |
| 381 | + | } | |
| 382 | + | } | |
| 383 | + | return n | |
| 384 | + | } | |
| 385 | + | scenes := make(chan ports.Scene, 1) | |
| 386 | + | contents := make(chan ports.SurfaceContent, 1) | |
| 387 | + | presented := make(chan ports.OutputPresented, 8) | |
| 388 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 389 | + | done := make(chan error, 1) | |
| 390 | + | go func() { | |
| 391 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, presented) | |
| 392 | + | }() | |
| 393 | + | scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}} | |
| 394 | + | waitFor(t, func() bool { return frameCommits() == 1 }) | |
| 395 | + | // Frame 1 is in flight: content 3 replaces what it may be reading. | |
| 396 | + | contents <- ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}} | |
| 397 | + | select { | |
| 398 | + | case p := <-presented: | |
| 399 | + | t.Fatalf("report while a frame is in flight: %+v", p) | |
| 400 | + | case <-time.After(30 * time.Millisecond): | |
| 401 | + | } | |
| 402 | + | flips <- flipEvent{crtc: tCrtc, user: userFrame, when: time.Second, seq: 1} | |
| 403 | + | select { | |
| 404 | + | case p := <-presented: | |
| 405 | + | if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 { | |
| 406 | + | t.Fatalf("report %+v", p) | |
| 407 | + | } | |
| 408 | + | case <-time.After(2 * time.Second): | |
| 409 | + | t.Fatal("no report after the flip") | |
| 410 | + | } | |
| 411 | + | cancel() | |
| 412 | + | if err := <-done; err != nil { | |
| 413 | + | t.Fatal(err) | |
| 414 | + | } | |
| 415 | + | mu.Lock() | |
| 416 | + | defer mu.Unlock() | |
| 417 | + | if len(fences) < 3 { | |
| 418 | + | t.Fatalf("%d renders", len(fences)) | |
| 419 | + | } | |
| 420 | + | for _, f := range fences { | |
| 421 | + | if _, err := f.Stat(); err == nil { | |
| 422 | + | t.Fatal("frame fence left open") | |
| 423 | + | } | |
| 424 | + | } | |
| 425 | + | } | |
| 426 | + | ||
| 427 | + | func waitFor(t *testing.T, cond func() bool) { | |
| 428 | + | t.Helper() | |
| 429 | + | deadline := time.Now().Add(2 * time.Second) | |
| 430 | + | for !cond() { | |
| 431 | + | if time.Now().After(deadline) { | |
| 432 | + | t.Fatal("timed out") | |
| 433 | + | } | |
| 434 | + | time.Sleep(2 * time.Millisecond) | |
| 435 | + | } | |
| 436 | + | } | |
M internal/adapters/drm/props.go +4 -16
| @@ -6,12 +6,11 @@ import ( | |||
| 6 | 6 | "golang.org/x/sys/unix" | |
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | - | // KMS object properties, read and set with the legacy property ioctls. | |
| 9 | + | // KMS object properties, read by name; atomic commits set them. | |
| 10 | 10 | ||
| 11 | 11 | const ( | |
| 12 | 12 | ioctlObjGetProps = 0xC02064B9 // DRM_IOWR('d', 0xB9, struct drm_mode_obj_get_properties) | |
| 13 | 13 | ioctlGetProp = 0xC04064AA // DRM_IOWR('d', 0xAA, struct drm_mode_get_property) | |
| 14 | - | ioctlObjSetProp = 0xC01864BA // DRM_IOWR('d', 0xBA, struct drm_mode_obj_set_property) | |
| 15 | 14 | ||
| 16 | 15 | objCrtc = 0xcccccccc | |
| 17 | 16 | objConnector = 0xc0c0c0c0 | |
| @@ -30,12 +29,6 @@ type getProp struct { | |||
| 30 | 29 | countValues, countEn uint32 | |
| 31 | 30 | } | |
| 32 | 31 | ||
| 33 | - | type objSetProp struct { | |
| 34 | - | value uint64 | |
| 35 | - | propID, objID, objType uint32 | |
| 36 | - | _ uint32 | |
| 37 | - | } | |
| 38 | - | ||
| 39 | 32 | // objProps returns an object's property IDs and values by name. | |
| 40 | 33 | func objProps(fd int, obj, typ uint32) (map[string][2]uint64, error) { | |
| 41 | 34 | q := objGetProps{objID: obj, objType: typ} | |
| @@ -64,19 +57,14 @@ func objProps(fd int, obj, typ uint32) (map[string][2]uint64, error) { | |||
| 64 | 57 | ||
| 65 | 58 | // vrrProperty returns the CRTC's VRR_ENABLED property when the connector | |
| 66 | 59 | // is VRR capable, else 0. | |
| 67 | - | func vrrProperty(fd int, conn, crtc uint32) uint32 { | |
| 68 | - | cp, err := objProps(fd, conn, objConnector) | |
| 60 | + | func vrrProperty(k kms, conn, crtc uint32) uint32 { | |
| 61 | + | cp, err := k.objProps(conn, objConnector) | |
| 69 | 62 | if err != nil || cp["vrr_capable"][1] != 1 { | |
| 70 | 63 | return 0 | |
| 71 | 64 | } | |
| 72 | - | rp, err := objProps(fd, crtc, objCrtc) | |
| 65 | + | rp, err := k.objProps(crtc, objCrtc) | |
| 73 | 66 | if err != nil { | |
| 74 | 67 | return 0 | |
| 75 | 68 | } | |
| 76 | 69 | return uint32(rp["VRR_ENABLED"][0]) | |
| 77 | 70 | } | |
| 78 | - | ||
| 79 | - | func setProp(fd int, obj, typ, prop uint32, v uint64) error { | |
| 80 | - | s := objSetProp{value: v, propID: prop, objID: obj, objType: typ} | |
| 81 | - | return ioctl(fd, ioctlObjSetProp, unsafe.Pointer(&s)) | |
| 82 | - | } | |
M internal/adapters/drm/scanout.go +19 -21
| @@ -23,7 +23,6 @@ const ( | |||
| 23 | 23 | ||
| 24 | 24 | fbModifiers = 1 << 1 | |
| 25 | 25 | capAddFB2Modifiers = 0x10 | |
| 26 | - | capAsyncPageFlip = 0x7 | |
| 27 | 26 | modInvalid = 0x00ffffffffffffff | |
| 28 | 27 | scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused | |
| 29 | 28 | ) | |
| @@ -103,7 +102,12 @@ func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) { | |||
| 103 | 102 | } | |
| 104 | 103 | fb := &clientFB{last: now} | |
| 105 | 104 | o.clientFBs[b.ID] = fb | |
| 106 | - | id, err := o.card.addFB(b) | |
| 105 | + | format, ok := scanoutFormat(b.Format) | |
| 106 | + | if !ok { | |
| 107 | + | fb.failed = "format" | |
| 108 | + | return 0, fb.failed | |
| 109 | + | } | |
| 110 | + | id, err := o.k.addFB(b, format) | |
| 107 | 111 | if err != nil { | |
| 108 | 112 | o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed") | |
| 109 | 113 | fb.failed = "import_failed" | |
| @@ -114,9 +118,8 @@ func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) { | |||
| 114 | 118 | } | |
| 115 | 119 | ||
| 116 | 120 | // scanoutFormat is the framebuffer format for a client format. The primary | |
| 117 | - | // plane shows no alpha, and a legacy page flip keeps the format of the | |
| 118 | - | // composed image (XRGB8888): alpha formats are scanned out as their X | |
| 119 | - | // variant, others are refused. | |
| 121 | + | // plane shows no alpha: alpha formats are scanned out as their X variant, | |
| 122 | + | // others are refused. | |
| 120 | 123 | func scanoutFormat(f uint32) (uint32, bool) { | |
| 121 | 124 | switch f { | |
| 122 | 125 | case fourccXRGB, fourccARGB: | |
| @@ -130,27 +133,23 @@ const ( | |||
| 130 | 133 | fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24 | |
| 131 | 134 | ) | |
| 132 | 135 | ||
| 133 | - | // addFB imports a dmabuf as a framebuffer: client buffers and the | |
| 134 | - | // renderer's exported output images. The GEM handle belongs to the card fd | |
| 136 | + | // addFB imports a dmabuf as a framebuffer of the given fourcc: client buffers, the renderer's | |
| 137 | + | // output images and cursor images. The GEM handle belongs to the card fd | |
| 135 | 138 | // and is shared by every import of the same buffer, so import, ADDFB2 and | |
| 136 | - | // close run under the card lock and the handle is closed right away: the | |
| 139 | + | // close run under the lock and the handle is closed right away: the | |
| 137 | 140 | // framebuffer keeps its own reference to the buffer. | |
| 138 | - | func (c *Card) addFB(b *ports.DMABuf) (uint32, error) { | |
| 139 | - | format, ok := scanoutFormat(b.Format) | |
| 140 | - | if !ok { | |
| 141 | - | return 0, errors.New("format not scanned out") | |
| 142 | - | } | |
| 141 | + | func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) { | |
| 143 | 142 | raw, err := b.Planes[0].File.SyscallConn() | |
| 144 | 143 | if err != nil { | |
| 145 | 144 | return 0, err | |
| 146 | 145 | } | |
| 147 | - | c.gemMu.Lock() | |
| 148 | - | defer c.gemMu.Unlock() | |
| 146 | + | k.gemMu.Lock() | |
| 147 | + | defer k.gemMu.Unlock() | |
| 149 | 148 | var p primeHandle | |
| 150 | 149 | var ioErr error | |
| 151 | 150 | if err := raw.Control(func(fd uintptr) { | |
| 152 | 151 | p.fd = int32(fd) | |
| 153 | - | ioErr = ioctl(c.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) | |
| 152 | + | ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) | |
| 154 | 153 | }); err != nil { | |
| 155 | 154 | return 0, err | |
| 156 | 155 | } | |
| @@ -159,14 +158,14 @@ func (c *Card) addFB(b *ports.DMABuf) (uint32, error) { | |||
| 159 | 158 | } | |
| 160 | 159 | defer func() { | |
| 161 | 160 | g := gemClose{handle: p.handle} | |
| 162 | - | _ = ioctl(c.fd, ioctlGemClose, unsafe.Pointer(&g)) | |
| 161 | + | _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g)) | |
| 163 | 162 | }() | |
| 164 | 163 | cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format} | |
| 165 | 164 | cmd.handles[0], cmd.pitches[0], cmd.offsets[0] = p.handle, b.Planes[0].Stride, b.Planes[0].Offset | |
| 166 | - | if b.Modifier != modInvalid && (b.Modifier != 0 || c.modifiers) { | |
| 165 | + | if b.Modifier != modInvalid && (b.Modifier != 0 || k.modifiers) { | |
| 167 | 166 | cmd.flags, cmd.modifiers[0] = fbModifiers, b.Modifier | |
| 168 | 167 | } | |
| 169 | - | if err := ioctl(c.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil { | |
| 168 | + | if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil { | |
| 170 | 169 | return 0, errors.Join(errors.New("add fb2"), err) | |
| 171 | 170 | } | |
| 172 | 171 | return cmd.fbID, nil | |
| @@ -183,8 +182,7 @@ func (o *Output) dropClientFBs(now time.Time, all bool) { | |||
| 183 | 182 | continue | |
| 184 | 183 | } | |
| 185 | 184 | if fb.fbID != 0 { | |
| 186 | - | v := fb.fbID | |
| 187 | - | _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v)) | |
| 185 | + | _ = o.k.rmFB(fb.fbID) | |
| 188 | 186 | } | |
| 189 | 187 | delete(o.clientFBs, id) | |
| 190 | 188 | } | |
M internal/adapters/headless/output.go +24 -1
| @@ -2,6 +2,7 @@ package headless | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "errors" | |
| 5 | 6 | "fmt" | |
| 6 | 7 | "image" | |
| 7 | 8 | "image/png" | |
| @@ -12,6 +13,7 @@ import ( | |||
| 12 | 13 | ||
| 13 | 14 | "github.com/bnema/nefertty/internal/ports" | |
| 14 | 15 | "github.com/bnema/zerowrap" | |
| 16 | + | "golang.org/x/sys/unix" | |
| 15 | 17 | ) | |
| 16 | 18 | ||
| 17 | 19 | type Options struct { | |
| @@ -118,9 +120,19 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 118 | 120 | } | |
| 119 | 121 | } | |
| 120 | 122 | start := time.Now() | |
| 121 | - | if err := r.Render(scene, surfaces); err != nil { | |
| 123 | + | done, err := r.Render(scene, surfaces) | |
| 124 | + | if err != nil { | |
| 122 | 125 | return fmt.Errorf("render frame: %w", err) | |
| 123 | 126 | } | |
| 127 | + | if done != nil { | |
| 128 | + | // No screen paces headless frames: wait for the GPU before | |
| 129 | + | // reporting that its buffers were read. | |
| 130 | + | err = waitFence(done) | |
| 131 | + | done.Close() | |
| 132 | + | if err != nil { | |
| 133 | + | return fmt.Errorf("frame fence: %w", err) | |
| 134 | + | } | |
| 135 | + | } | |
| 124 | 136 | frame++ | |
| 125 | 137 | pending = opts.report(pending, seen) | |
| 126 | 138 | if opts.ScreenshotDir != "" { | |
| @@ -174,3 +186,14 @@ func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | |||
| 174 | 186 | return r | |
| 175 | 187 | } | |
| 176 | 188 | } | |
| 189 | + | ||
| 190 | + | // waitFence blocks until a sync file signals. | |
| 191 | + | func waitFence(f *os.File) error { | |
| 192 | + | fds := []unix.PollFd{{Fd: int32(f.Fd()), Events: unix.POLLIN}} | |
| 193 | + | for { | |
| 194 | + | _, err := unix.Poll(fds, -1) | |
| 195 | + | if !errors.Is(err, unix.EINTR) { | |
| 196 | + | return err | |
| 197 | + | } | |
| 198 | + | } | |
| 199 | + | } | |
M internal/adapters/headless/output_test.go +2 -2
| @@ -35,12 +35,12 @@ func (f *frames) snapshot() ([]ports.Scene, []map[ports.WindowID]ports.SurfaceCo | |||
| 35 | 35 | func recordingRenderer(t *testing.T, err error) (*portsmocks.MockRenderer, *frames) { | |
| 36 | 36 | r := portsmocks.NewMockRenderer(t) | |
| 37 | 37 | f := &frames{} | |
| 38 | - | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, c map[ports.WindowID]ports.SurfaceContent) error { | |
| 38 | + | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, c map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |
| 39 | 39 | f.mu.Lock() | |
| 40 | 40 | defer f.mu.Unlock() | |
| 41 | 41 | f.scenes = append(f.scenes, s) | |
| 42 | 42 | f.contents = append(f.contents, maps.Clone(c)) | |
| 43 | - | return err | |
| 43 | + | return nil, err | |
| 44 | 44 | }).Maybe() | |
| 45 | 45 | r.EXPECT().Pixels().Return(image.NewRGBA(image.Rect(0, 0, 2, 2))).Maybe() | |
| 46 | 46 | r.EXPECT().Close().Return().Once() | |
A internal/adapters/vulkan/cursor.go +141 -0
| @@ -0,0 +1,141 @@ | |||
| 1 | + | package vulkan | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "fmt" | |
| 6 | + | "os" | |
| 7 | + | "unsafe" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/nefertty/internal/ports" | |
| 10 | + | vk "github.com/bnema/purego-vulkan/vulkan" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // Cursor images for the display's cursor plane: two linear B8G8R8A8 | |
| 14 | + | // images in host-visible memory, exported as dmabufs. The CPU writes a | |
| 15 | + | // cursor image only when the cursor changes, never per frame. | |
| 16 | + | ||
| 17 | + | type cursorImage struct { | |
| 18 | + | image vk.Image | |
| 19 | + | memory vk.DeviceMemory | |
| 20 | + | mapped unsafe.Pointer | |
| 21 | + | pitch int | |
| 22 | + | size int | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // fourccARGB is DRM_FORMAT_ARGB8888: B8G8R8A8 in memory. | |
| 26 | + | const fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24 | |
| 27 | + | ||
| 28 | + | // CursorBuffers allocates the two cursor images, replacing earlier ones. | |
| 29 | + | func (r *Renderer) CursorBuffers(size int) ([2]ports.DMABuf, error) { | |
| 30 | + | var out [2]ports.DMABuf | |
| 31 | + | r.dropCursors() | |
| 32 | + | if r.dd.GetMemoryFdKHR == nil || size <= 0 || size > 512 { | |
| 33 | + | return out, errors.New("device cannot export cursor images") | |
| 34 | + | } | |
| 35 | + | for i := range r.cursors { | |
| 36 | + | c, buf, err := r.cursorImage(size) | |
| 37 | + | if err != nil { | |
| 38 | + | r.dropCursors() | |
| 39 | + | for _, b := range out[:i] { | |
| 40 | + | b.Planes[0].File.Close() | |
| 41 | + | } | |
| 42 | + | return [2]ports.DMABuf{}, err | |
| 43 | + | } | |
| 44 | + | r.cursors[i], out[i] = c, buf | |
| 45 | + | } | |
| 46 | + | return out, nil | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | func (r *Renderer) cursorImage(size int) (*cursorImage, ports.DMABuf, error) { | |
| 50 | + | d := r.dd | |
| 51 | + | c := &cursorImage{size: size} | |
| 52 | + | ok := false | |
| 53 | + | defer func() { | |
| 54 | + | if !ok { | |
| 55 | + | r.freeCursor(c) | |
| 56 | + | } | |
| 57 | + | }() | |
| 58 | + | external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | |
| 59 | + | ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: uint32(size), Height: uint32(size), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingLinear, Usage: vk.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutPreinitialized} | |
| 60 | + | if err := checked("vkCreateImage(cursor)", d.CreateImage(r.device, &ii, nil, &c.image)); err != nil { | |
| 61 | + | return nil, ports.DMABuf{}, err | |
| 62 | + | } | |
| 63 | + | var req vk.MemoryRequirements | |
| 64 | + | d.GetImageMemoryRequirements(r.device, c.image, &req) | |
| 65 | + | kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit) | |
| 66 | + | if err != nil { | |
| 67 | + | return nil, ports.DMABuf{}, err | |
| 68 | + | } | |
| 69 | + | dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: c.image} | |
| 70 | + | export := vk.ExportMemoryAllocateInfo{SType: vk.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&dedicated), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | |
| 71 | + | alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&export), AllocationSize: req.Size, MemoryTypeIndex: kind} | |
| 72 | + | if err := checked("vkAllocateMemory(cursor)", d.AllocateMemory(r.device, &alloc, nil, &c.memory)); err != nil { | |
| 73 | + | return nil, ports.DMABuf{}, err | |
| 74 | + | } | |
| 75 | + | if err := checked("vkBindImageMemory(cursor)", d.BindImageMemory(r.device, c.image, c.memory, 0)); err != nil { | |
| 76 | + | return nil, ports.DMABuf{}, err | |
| 77 | + | } | |
| 78 | + | var layout vk.SubresourceLayout | |
| 79 | + | sub := vk.ImageSubresource{AspectMask: vk.ImageAspectColorBit} | |
| 80 | + | d.GetImageSubresourceLayout(r.device, c.image, &sub, &layout) | |
| 81 | + | c.pitch = int(layout.RowPitch) | |
| 82 | + | if err := checked("vkMapMemory(cursor)", d.MapMemory(r.device, c.memory, 0, vk.DeviceSize(req.Size), 0, &c.mapped)); err != nil { | |
| 83 | + | return nil, ports.DMABuf{}, err | |
| 84 | + | } | |
| 85 | + | c.mapped = unsafe.Add(c.mapped, layout.Offset) | |
| 86 | + | clear(unsafe.Slice((*byte)(c.mapped), c.pitch*size)) | |
| 87 | + | var fd int32 | |
| 88 | + | get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: c.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | |
| 89 | + | if err := checked("vkGetMemoryFdKHR(cursor)", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil { | |
| 90 | + | return nil, ports.DMABuf{}, err | |
| 91 | + | } | |
| 92 | + | f := os.NewFile(uintptr(fd), "nefertty-cursor") | |
| 93 | + | if f == nil { | |
| 94 | + | return nil, ports.DMABuf{}, fmt.Errorf("invalid exported fd %d", fd) | |
| 95 | + | } | |
| 96 | + | ok = true | |
| 97 | + | return c, ports.DMABuf{Width: size, Height: size, Format: fourccARGB, Modifier: 0, Planes: []ports.DMABufPlane{{File: f, Offset: uint32(layout.Offset), Stride: uint32(layout.RowPitch)}}}, nil | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | // WriteCursor copies premultiplied ARGB8888 pixels (w×h, w*4 per row) | |
| 101 | + | // into cursor image i at its row pitch, cropped and cleared around. | |
| 102 | + | func (r *Renderer) WriteCursor(i int, pixels []byte, w, h int) error { | |
| 103 | + | if i < 0 || i >= len(r.cursors) || r.cursors[i] == nil { | |
| 104 | + | return errors.New("no cursor image") | |
| 105 | + | } | |
| 106 | + | if w < 0 || h < 0 || len(pixels) < w*h*4 { | |
| 107 | + | return errors.New("short cursor image") | |
| 108 | + | } | |
| 109 | + | c := r.cursors[i] | |
| 110 | + | mem := unsafe.Slice((*byte)(c.mapped), c.pitch*c.size) | |
| 111 | + | clear(mem) | |
| 112 | + | cw := min(w, c.size) * 4 | |
| 113 | + | for y := range min(h, c.size) { | |
| 114 | + | copy(mem[y*c.pitch:y*c.pitch+cw], pixels[y*w*4:y*w*4+cw]) | |
| 115 | + | } | |
| 116 | + | return nil | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | func (r *Renderer) freeCursor(c *cursorImage) { | |
| 120 | + | if c.mapped != nil { | |
| 121 | + | r.dd.UnmapMemory(r.device, c.memory) | |
| 122 | + | c.mapped = nil | |
| 123 | + | } | |
| 124 | + | if c.image != 0 { | |
| 125 | + | r.dd.DestroyImage(r.device, c.image, nil) | |
| 126 | + | c.image = 0 | |
| 127 | + | } | |
| 128 | + | if c.memory != 0 { | |
| 129 | + | r.dd.FreeMemory(r.device, c.memory, nil) | |
| 130 | + | c.memory = 0 | |
| 131 | + | } | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | func (r *Renderer) dropCursors() { | |
| 135 | + | for i, c := range r.cursors { | |
| 136 | + | if c != nil { | |
| 137 | + | r.freeCursor(c) | |
| 138 | + | r.cursors[i] = nil | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | } | |
M internal/adapters/vulkan/dmabuf_test.go +2 -2
| @@ -81,7 +81,7 @@ func TestRendererImportsDMABuf(t *testing.T) { | |||
| 81 | 81 | buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}} | |
| 82 | 82 | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}} | |
| 83 | 83 | contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, DMABuf: buf}} | |
| 84 | - | if err := r.Render(scene, contents); err != nil { | |
| 84 | + | if _, err := r.Render(scene, contents); err != nil { | |
| 85 | 85 | t.Fatal(err) | |
| 86 | 86 | } | |
| 87 | 87 | px := r.Pixels() | |
| @@ -98,7 +98,7 @@ func TestRendererImportsDMABuf(t *testing.T) { | |||
| 98 | 98 | } | |
| 99 | 99 | // Frames without the buffer: its import is released after importTTL. | |
| 100 | 100 | for range importTTL + 1 { | |
| 101 | - | if err := r.Render(scene, nil); err != nil { | |
| 101 | + | if _, err := r.Render(scene, nil); err != nil { | |
| 102 | 102 | t.Fatal(err) | |
| 103 | 103 | } | |
| 104 | 104 | } | |
M internal/adapters/vulkan/renderer.go +11 -16
| @@ -30,7 +30,7 @@ type Renderer struct { | |||
| 30 | 30 | current int | |
| 31 | 31 | renderMods []uint64 | |
| 32 | 32 | // last is the target of the last frame; readback copies it into | |
| 33 | - | // buffer only when Pixels or CopyBGRX asks (stale until then). | |
| 33 | + | // buffer only when Pixels asks (stale until then). | |
| 34 | 34 | last *target | |
| 35 | 35 | readBack bool | |
| 36 | 36 | buffer vk.Buffer | |
| @@ -55,6 +55,8 @@ type Renderer struct { | |||
| 55 | 55 | frame uint64 | |
| 56 | 56 | // fillRegions is reused by Render for the bands of solid fills. | |
| 57 | 57 | fillRegions []vk.BufferImageCopy | |
| 58 | + | // cursors are the exported cursor images (CursorBuffers). | |
| 59 | + | cursors [2]*cursorImage | |
| 58 | 60 | // blend draws translucent client pixels (blend.go); align is the | |
| 59 | 61 | // device's storage buffer offset alignment. | |
| 60 | 62 | blend blendPipeline | |
| @@ -370,7 +372,7 @@ func (r *Renderer) ensureStaging(size int) error { | |||
| 370 | 372 | } | |
| 371 | 373 | ||
| 372 | 374 | func (r *Renderer) Clear(rgb [3]uint8) error { | |
| 373 | - | return r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil) | |
| 375 | + | return r.render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil) | |
| 374 | 376 | } | |
| 375 | 377 | ||
| 376 | 378 | // upload is one piece of a frame: a solid fill, client pixels staged from | |
| @@ -427,7 +429,12 @@ func (u *upload) stage(dst []byte) { | |||
| 427 | 429 | } | |
| 428 | 430 | } | |
| 429 | 431 | ||
| 430 | - | func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error { | |
| 432 | + | // Render draws a frame. It still waits for the GPU; done is nil. | |
| 433 | + | func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) (done *os.File, err error) { | |
| 434 | + | return nil, r.render(s, contents) | |
| 435 | + | } | |
| 436 | + | ||
| 437 | + | func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error { | |
| 431 | 438 | // Scene rects are logical; everything below works in physical pixels. | |
| 432 | 439 | scale := s.Scale | |
| 433 | 440 | if scale <= 0 { | |
| @@ -810,19 +817,6 @@ func (r *Renderer) Pixels() *image.RGBA { | |||
| 810 | 817 | return out | |
| 811 | 818 | } | |
| 812 | 819 | ||
| 813 | - | // CopyBGRX writes the last frame into an XRGB8888 buffer with the given pitch. | |
| 814 | - | // The image is B8G8R8A8, which is already XRGB8888 in memory. | |
| 815 | - | func (r *Renderer) CopyBGRX(dst []byte, pitch int) { | |
| 816 | - | if r.mapped == nil || r.readback() != nil { | |
| 817 | - | return | |
| 818 | - | } | |
| 819 | - | row := r.width * 4 | |
| 820 | - | src := unsafe.Slice((*byte)(r.mapped), row*r.height) | |
| 821 | - | for y := 0; y < r.height; y++ { | |
| 822 | - | copy(dst[y*pitch:y*pitch+row], src[y*row:(y+1)*row]) | |
| 823 | - | } | |
| 824 | - | } | |
| 825 | - | ||
| 826 | 820 | // Missing from the bindings: VK_QUEUE_FAMILY_IGNORED, VK_WHOLE_SIZE. | |
| 827 | 821 | const ( | |
| 828 | 822 | queueFamilyIgnored = ^uint32(0) | |
| @@ -887,6 +881,7 @@ func (r *Renderer) Close() { | |||
| 887 | 881 | _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device)) | |
| 888 | 882 | } | |
| 889 | 883 | r.dropTargets() | |
| 884 | + | r.dropCursors() | |
| 890 | 885 | for id, im := range r.imports { | |
| 891 | 886 | r.release(im) | |
| 892 | 887 | delete(r.imports, id) | |
M internal/adapters/vulkan/renderer_test.go +12 -12
| @@ -48,7 +48,7 @@ func TestRendererRender(t *testing.T) { | |||
| 48 | 48 | bg := color.RGBA{16, 32, 48, 255} | |
| 49 | 49 | check := func(s ports.Scene, expected map[image.Point]color.RGBA) { | |
| 50 | 50 | t.Helper() | |
| 51 | - | if err := r.Render(s, nil); err != nil { | |
| 51 | + | if _, err := r.Render(s, nil); err != nil { | |
| 52 | 52 | t.Fatal(err) | |
| 53 | 53 | } | |
| 54 | 54 | for p, want := range expected { | |
| @@ -92,7 +92,7 @@ func TestRendererContents(t *testing.T) { | |||
| 92 | 92 | if c != nil { | |
| 93 | 93 | contents = map[ports.WindowID]ports.SurfaceContent{1: *c} | |
| 94 | 94 | } | |
| 95 | - | if err := r.Render(scene, contents); err != nil { | |
| 95 | + | if _, err := r.Render(scene, contents); err != nil { | |
| 96 | 96 | t.Fatal(err) | |
| 97 | 97 | } | |
| 98 | 98 | } | |
| @@ -127,7 +127,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) { | |||
| 127 | 127 | {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}}, | |
| 128 | 128 | {ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}}, | |
| 129 | 129 | }} | |
| 130 | - | if err := r.Render(scene, nil); err != nil { | |
| 130 | + | if _, err := r.Render(scene, nil); err != nil { | |
| 131 | 131 | t.Fatal(err) | |
| 132 | 132 | } | |
| 133 | 133 | c := windowColor(2) | |
| @@ -143,7 +143,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) { | |||
| 143 | 143 | opaque := shmContent(t, 16, 16, 64, px) | |
| 144 | 144 | opaque.Opaque = true | |
| 145 | 145 | contents := map[ports.WindowID]ports.SurfaceContent{1: *opaque} | |
| 146 | - | if err := r.Render(scene, contents); err != nil { | |
| 146 | + | if _, err := r.Render(scene, contents); err != nil { | |
| 147 | 147 | t.Fatal(err) | |
| 148 | 148 | } | |
| 149 | 149 | for _, tc := range []struct { | |
| @@ -174,7 +174,7 @@ func TestRendererLayers(t *testing.T) { | |||
| 174 | 174 | window := color.RGBA{wc[0], wc[1], wc[2], 255} | |
| 175 | 175 | check := func(want color.RGBA) { | |
| 176 | 176 | t.Helper() | |
| 177 | - | if err := r.Render(scene, contents); err != nil { | |
| 177 | + | if _, err := r.Render(scene, contents); err != nil { | |
| 178 | 178 | t.Fatal(err) | |
| 179 | 179 | } | |
| 180 | 180 | if got := r.Pixels().RGBAAt(5, 2); got != want { | |
| @@ -218,7 +218,7 @@ func TestRendererScale(t *testing.T) { | |||
| 218 | 218 | scaled := shmContent(t, 20, 20, 80, solid(20, 20, red)) | |
| 219 | 219 | scaled.ID, scaled.LogicalW, scaled.LogicalH = 1, 10, 10 | |
| 220 | 220 | content := map[ports.WindowID]ports.SurfaceContent{1: *scaled} | |
| 221 | - | if err := r.Render(s, content); err != nil { | |
| 221 | + | if _, err := r.Render(s, content); err != nil { | |
| 222 | 222 | t.Fatal(err) | |
| 223 | 223 | } | |
| 224 | 224 | px := r.Pixels() | |
| @@ -236,7 +236,7 @@ func TestRendererScale(t *testing.T) { | |||
| 236 | 236 | // Scale 1.5 with an integer-scale client (buffer scale 2): the 20x20 | |
| 237 | 237 | // buffer is 10x10 logical and downscaled to 15x15 physical at (6,6). | |
| 238 | 238 | s.Scale = 1.5 | |
| 239 | - | if err := r.Render(s, content); err != nil { | |
| 239 | + | if _, err := r.Render(s, content); err != nil { | |
| 240 | 240 | t.Fatal(err) | |
| 241 | 241 | } | |
| 242 | 242 | px = r.Pixels() | |
| @@ -252,7 +252,7 @@ func TestRendererScale(t *testing.T) { | |||
| 252 | 252 | odd := shmContent(t, 16, 15, 64, solid(16, 15, red)) | |
| 253 | 253 | odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10 | |
| 254 | 254 | content[1] = *odd | |
| 255 | - | if err := r.Render(s, content); err != nil { | |
| 255 | + | if _, err := r.Render(s, content); err != nil { | |
| 256 | 256 | t.Fatal(err) | |
| 257 | 257 | } | |
| 258 | 258 | if got := r.Pixels().At(21, 10); got != (color.RGBA{0, 0, 0, 255}) { | |
| @@ -309,7 +309,7 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) { | |||
| 309 | 309 | {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(t, 30, 20, blue)}, | |
| 310 | 310 | {X: 5, Y: 5, SurfaceContent: solidContent(t, 4, 4, green)}, | |
| 311 | 311 | } | |
| 312 | - | if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil { | |
| 312 | + | if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil { | |
| 313 | 313 | t.Fatal(err) | |
| 314 | 314 | } | |
| 315 | 315 | px := r.Pixels() | |
| @@ -327,7 +327,7 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) { | |||
| 327 | 327 | } | |
| 328 | 328 | // A root with no buffer of its own still shows its children. | |
| 329 | 329 | only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(t, 20, 10, blue)}}} | |
| 330 | - | if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil { | |
| 330 | + | if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil { | |
| 331 | 331 | t.Fatal(err) | |
| 332 | 332 | } | |
| 333 | 333 | if got := r.Pixels().At(20, 15); got != blue { | |
| @@ -345,7 +345,7 @@ func TestRendererTallFill(t *testing.T) { | |||
| 345 | 345 | } | |
| 346 | 346 | defer r.Close() | |
| 347 | 347 | scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}} | |
| 348 | - | if err := r.Render(scene, nil); err != nil { | |
| 348 | + | if _, err := r.Render(scene, nil); err != nil { | |
| 349 | 349 | t.Fatal(err) | |
| 350 | 350 | } | |
| 351 | 351 | c := windowColor(1) | |
| @@ -403,7 +403,7 @@ func TestRendererAlphaBlend(t *testing.T) { | |||
| 403 | 403 | {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}}, | |
| 404 | 404 | {ID: 4, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 8, W: 16, H: 8}}, | |
| 405 | 405 | }} | |
| 406 | - | if err := r.Render(scene, contents); err != nil { | |
| 406 | + | if _, err := r.Render(scene, contents); err != nil { | |
| 407 | 407 | t.Fatal(err) | |
| 408 | 408 | } | |
| 409 | 409 | px := r.Pixels() | |
M internal/adapters/vulkan/targets.go +2 -2
| @@ -17,7 +17,7 @@ import ( | |||
| 17 | 17 | // turns into KMS framebuffers. Render then composes straight into the | |
| 18 | 18 | // selected target and the output flips to it; nothing is copied by the | |
| 19 | 19 | // CPU. The internal image is the target until then (headless, tests). | |
| 20 | - | // Readback into host memory happens only for Pixels and CopyBGRX. | |
| 20 | + | // Readback into host memory happens only for Pixels (screenshots). | |
| 21 | 21 | ||
| 22 | 22 | // target is an image frames are drawn into. | |
| 23 | 23 | type target struct { | |
| @@ -107,7 +107,7 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) { | |||
| 107 | 107 | r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp) | |
| 108 | 108 | need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit) | |
| 109 | 109 | // Targets are drawn on too (blend.go): a transfer-only modifier is | |
| 110 | - | // not a target, and without any the display falls back to readback. | |
| 110 | + | // not a target, and without any the output cannot start. | |
| 111 | 111 | for _, m := range mods[:list.DrmFormatModifierCount] { | |
| 112 | 112 | if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) { | |
| 113 | 113 | r.renderMods = append(r.renderMods, m.DrmFormatModifier) | |
M internal/adapters/vulkan/targets_test.go +2 -2
| @@ -31,7 +31,7 @@ func TestExportTargets(t *testing.T) { | |||
| 31 | 31 | colors := []string{"#ff0000", "#0000ff"} | |
| 32 | 32 | for i, c := range colors { | |
| 33 | 33 | r.UseTarget(i) | |
| 34 | - | if err := r.Render(ports.Scene{Background: c}, nil); err != nil { | |
| 34 | + | if _, err := r.Render(ports.Scene{Background: c}, nil); err != nil { | |
| 35 | 35 | t.Fatal(err) | |
| 36 | 36 | } | |
| 37 | 37 | } | |
| @@ -55,7 +55,7 @@ func TestExportTargets(t *testing.T) { | |||
| 55 | 55 | } | |
| 56 | 56 | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 32}}}} | |
| 57 | 57 | contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 32, Opaque: true, DMABuf: &b}} | |
| 58 | - | if err := viewer.Render(scene, contents); err != nil { | |
| 58 | + | if _, err := viewer.Render(scene, contents); err != nil { | |
| 59 | 59 | t.Fatal(err) | |
| 60 | 60 | } | |
| 61 | 61 | if got := viewer.Pixels().At(10, 10); got != (color.RGBA{255, 0, 0, 255}) { | |
M internal/adapters/wayland/frames.go +2 -2
| @@ -34,7 +34,7 @@ func (s *Server) pace(ctx context.Context) { | |||
| 34 | 34 | case <-s.display.Stopped(): | |
| 35 | 35 | return | |
| 36 | 36 | case p := <-presented: | |
| 37 | - | flipped[p.Output] = flipped[p.Output] || p.Flip | |
| 37 | + | flipped[p.Output] = flipped[p.Output] || p.Flip != nil | |
| 38 | 38 | reports = append(reports, p) | |
| 39 | 39 | case <-s.frameReady: | |
| 40 | 40 | case <-timer.C: | |
| @@ -45,7 +45,7 @@ func (s *Server) pace(ctx context.Context) { | |||
| 45 | 45 | for { | |
| 46 | 46 | select { | |
| 47 | 47 | case p := <-presented: | |
| 48 | - | flipped[p.Output] = flipped[p.Output] || p.Flip | |
| 48 | + | flipped[p.Output] = flipped[p.Output] || p.Flip != nil | |
| 49 | 49 | reports = append(reports, p) | |
| 50 | 50 | default: | |
| 51 | 51 | break drain | |
M internal/mocks/ports/devices_mock.go +128 -31
| @@ -6,6 +6,7 @@ package portsmocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "image" | |
| 9 | + | "os" | |
| 9 | 10 | ||
| 10 | 11 | "github.com/bnema/nefertty/internal/ports" | |
| 11 | 12 | mock "github.com/stretchr/testify/mock" | |
| @@ -80,49 +81,65 @@ func (_c *MockRenderer_Close_Call) RunAndReturn(run func()) *MockRenderer_Close_ | |||
| 80 | 81 | return _c | |
| 81 | 82 | } | |
| 82 | 83 | ||
| 83 | - | // CopyBGRX provides a mock function for the type MockRenderer | |
| 84 | - | func (_mock *MockRenderer) CopyBGRX(dst []byte, pitch int) { | |
| 85 | - | _mock.Called(dst, pitch) | |
| 86 | - | return | |
| 84 | + | // CursorBuffers provides a mock function for the type MockRenderer | |
| 85 | + | func (_mock *MockRenderer) CursorBuffers(size int) ([2]ports.DMABuf, error) { | |
| 86 | + | ret := _mock.Called(size) | |
| 87 | + | ||
| 88 | + | if len(ret) == 0 { | |
| 89 | + | panic("no return value specified for CursorBuffers") | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | var r0 [2]ports.DMABuf | |
| 93 | + | var r1 error | |
| 94 | + | if returnFunc, ok := ret.Get(0).(func(int) ([2]ports.DMABuf, error)); ok { | |
| 95 | + | return returnFunc(size) | |
| 96 | + | } | |
| 97 | + | if returnFunc, ok := ret.Get(0).(func(int) [2]ports.DMABuf); ok { | |
| 98 | + | r0 = returnFunc(size) | |
| 99 | + | } else { | |
| 100 | + | if ret.Get(0) != nil { | |
| 101 | + | r0 = ret.Get(0).([2]ports.DMABuf) | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | if returnFunc, ok := ret.Get(1).(func(int) error); ok { | |
| 105 | + | r1 = returnFunc(size) | |
| 106 | + | } else { | |
| 107 | + | r1 = ret.Error(1) | |
| 108 | + | } | |
| 109 | + | return r0, r1 | |
| 87 | 110 | } | |
| 88 | 111 | ||
| 89 | - | // MockRenderer_CopyBGRX_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CopyBGRX' | |
| 90 | - | type MockRenderer_CopyBGRX_Call struct { | |
| 112 | + | // MockRenderer_CursorBuffers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CursorBuffers' | |
| 113 | + | type MockRenderer_CursorBuffers_Call struct { | |
| 91 | 114 | *mock.Call | |
| 92 | 115 | } | |
| 93 | 116 | ||
| 94 | - | // CopyBGRX is a helper method to define mock.On call | |
| 95 | - | // - dst []byte | |
| 96 | - | // - pitch int | |
| 97 | - | func (_e *MockRenderer_Expecter) CopyBGRX(dst any, pitch any) *MockRenderer_CopyBGRX_Call { | |
| 98 | - | return &MockRenderer_CopyBGRX_Call{Call: _e.mock.On("CopyBGRX", dst, pitch)} | |
| 117 | + | // CursorBuffers is a helper method to define mock.On call | |
| 118 | + | // - size int | |
| 119 | + | func (_e *MockRenderer_Expecter) CursorBuffers(size any) *MockRenderer_CursorBuffers_Call { | |
| 120 | + | return &MockRenderer_CursorBuffers_Call{Call: _e.mock.On("CursorBuffers", size)} | |
| 99 | 121 | } | |
| 100 | 122 | ||
| 101 | - | func (_c *MockRenderer_CopyBGRX_Call) Run(run func(dst []byte, pitch int)) *MockRenderer_CopyBGRX_Call { | |
| 123 | + | func (_c *MockRenderer_CursorBuffers_Call) Run(run func(size int)) *MockRenderer_CursorBuffers_Call { | |
| 102 | 124 | _c.Call.Run(func(args mock.Arguments) { | |
| 103 | - | var arg0 []byte | |
| 125 | + | var arg0 int | |
| 104 | 126 | if args[0] != nil { | |
| 105 | - | arg0 = args[0].([]byte) | |
| 106 | - | } | |
| 107 | - | var arg1 int | |
| 108 | - | if args[1] != nil { | |
| 109 | - | arg1 = args[1].(int) | |
| 127 | + | arg0 = args[0].(int) | |
| 110 | 128 | } | |
| 111 | 129 | run( | |
| 112 | 130 | arg0, | |
| 113 | - | arg1, | |
| 114 | 131 | ) | |
| 115 | 132 | }) | |
| 116 | 133 | return _c | |
| 117 | 134 | } | |
| 118 | 135 | ||
| 119 | - | func (_c *MockRenderer_CopyBGRX_Call) Return() *MockRenderer_CopyBGRX_Call { | |
| 120 | - | _c.Call.Return() | |
| 136 | + | func (_c *MockRenderer_CursorBuffers_Call) Return(dMABufs [2]ports.DMABuf, err error) *MockRenderer_CursorBuffers_Call { | |
| 137 | + | _c.Call.Return(dMABufs, err) | |
| 121 | 138 | return _c | |
| 122 | 139 | } | |
| 123 | 140 | ||
| 124 | - | func (_c *MockRenderer_CopyBGRX_Call) RunAndReturn(run func(dst []byte, pitch int)) *MockRenderer_CopyBGRX_Call { | |
| 125 | - | _c.Run(run) | |
| 141 | + | func (_c *MockRenderer_CursorBuffers_Call) RunAndReturn(run func(size int) ([2]ports.DMABuf, error)) *MockRenderer_CursorBuffers_Call { | |
| 142 | + | _c.Call.Return(run) | |
| 126 | 143 | return _c | |
| 127 | 144 | } | |
| 128 | 145 | ||
| @@ -241,20 +258,31 @@ func (_c *MockRenderer_Pixels_Call) RunAndReturn(run func() *image.RGBA) *MockRe | |||
| 241 | 258 | } | |
| 242 | 259 | ||
| 243 | 260 | // Render provides a mock function for the type MockRenderer | |
| 244 | - | func (_mock *MockRenderer) Render(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent) error { | |
| 261 | + | func (_mock *MockRenderer) Render(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |
| 245 | 262 | ret := _mock.Called(scene, windowIDToSurfaceContent) | |
| 246 | 263 | ||
| 247 | 264 | if len(ret) == 0 { | |
| 248 | 265 | panic("no return value specified for Render") | |
| 249 | 266 | } | |
| 250 | 267 | ||
| 251 | - | var r0 error | |
| 252 | - | if returnFunc, ok := ret.Get(0).(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error); ok { | |
| 268 | + | var r0 *os.File | |
| 269 | + | var r1 error | |
| 270 | + | if returnFunc, ok := ret.Get(0).(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error)); ok { | |
| 271 | + | return returnFunc(scene, windowIDToSurfaceContent) | |
| 272 | + | } | |
| 273 | + | if returnFunc, ok := ret.Get(0).(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) *os.File); ok { | |
| 253 | 274 | r0 = returnFunc(scene, windowIDToSurfaceContent) | |
| 254 | 275 | } else { | |
| 255 | - | r0 = ret.Error(0) | |
| 276 | + | if ret.Get(0) != nil { | |
| 277 | + | r0 = ret.Get(0).(*os.File) | |
| 278 | + | } | |
| 256 | 279 | } | |
| 257 | - | return r0 | |
| 280 | + | if returnFunc, ok := ret.Get(1).(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error); ok { | |
| 281 | + | r1 = returnFunc(scene, windowIDToSurfaceContent) | |
| 282 | + | } else { | |
| 283 | + | r1 = ret.Error(1) | |
| 284 | + | } | |
| 285 | + | return r0, r1 | |
| 258 | 286 | } | |
| 259 | 287 | ||
| 260 | 288 | // MockRenderer_Render_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Render' | |
| @@ -287,12 +315,12 @@ func (_c *MockRenderer_Render_Call) Run(run func(scene ports.Scene, windowIDToSu | |||
| 287 | 315 | return _c | |
| 288 | 316 | } | |
| 289 | 317 | ||
| 290 | - | func (_c *MockRenderer_Render_Call) Return(err error) *MockRenderer_Render_Call { | |
| 291 | - | _c.Call.Return(err) | |
| 318 | + | func (_c *MockRenderer_Render_Call) Return(done *os.File, err error) *MockRenderer_Render_Call { | |
| 319 | + | _c.Call.Return(done, err) | |
| 292 | 320 | return _c | |
| 293 | 321 | } | |
| 294 | 322 | ||
| 295 | - | func (_c *MockRenderer_Render_Call) RunAndReturn(run func(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent) error) *MockRenderer_Render_Call { | |
| 323 | + | func (_c *MockRenderer_Render_Call) RunAndReturn(run func(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent) (*os.File, error)) *MockRenderer_Render_Call { | |
| 296 | 324 | _c.Call.Return(run) | |
| 297 | 325 | return _c | |
| 298 | 326 | } | |
| @@ -337,6 +365,75 @@ func (_c *MockRenderer_UseTarget_Call) RunAndReturn(run func(i int)) *MockRender | |||
| 337 | 365 | return _c | |
| 338 | 366 | } | |
| 339 | 367 | ||
| 368 | + | // WriteCursor provides a mock function for the type MockRenderer | |
| 369 | + | func (_mock *MockRenderer) WriteCursor(i int, pixels []byte, w int, h int) error { | |
| 370 | + | ret := _mock.Called(i, pixels, w, h) | |
| 371 | + | ||
| 372 | + | if len(ret) == 0 { | |
| 373 | + | panic("no return value specified for WriteCursor") | |
| 374 | + | } | |
| 375 | + | ||
| 376 | + | var r0 error | |
| 377 | + | if returnFunc, ok := ret.Get(0).(func(int, []byte, int, int) error); ok { | |
| 378 | + | r0 = returnFunc(i, pixels, w, h) | |
| 379 | + | } else { | |
| 380 | + | r0 = ret.Error(0) | |
| 381 | + | } | |
| 382 | + | return r0 | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | // MockRenderer_WriteCursor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteCursor' | |
| 386 | + | type MockRenderer_WriteCursor_Call struct { | |
| 387 | + | *mock.Call | |
| 388 | + | } | |
| 389 | + | ||
| 390 | + | // WriteCursor is a helper method to define mock.On call | |
| 391 | + | // - i int | |
| 392 | + | // - pixels []byte | |
| 393 | + | // - w int | |
| 394 | + | // - h int | |
| 395 | + | func (_e *MockRenderer_Expecter) WriteCursor(i any, pixels any, w any, h any) *MockRenderer_WriteCursor_Call { | |
| 396 | + | return &MockRenderer_WriteCursor_Call{Call: _e.mock.On("WriteCursor", i, pixels, w, h)} | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | func (_c *MockRenderer_WriteCursor_Call) Run(run func(i int, pixels []byte, w int, h int)) *MockRenderer_WriteCursor_Call { | |
| 400 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 401 | + | var arg0 int | |
| 402 | + | if args[0] != nil { | |
| 403 | + | arg0 = args[0].(int) | |
| 404 | + | } | |
| 405 | + | var arg1 []byte | |
| 406 | + | if args[1] != nil { | |
| 407 | + | arg1 = args[1].([]byte) | |
| 408 | + | } | |
| 409 | + | var arg2 int | |
| 410 | + | if args[2] != nil { | |
| 411 | + | arg2 = args[2].(int) | |
| 412 | + | } | |
| 413 | + | var arg3 int | |
| 414 | + | if args[3] != nil { | |
| 415 | + | arg3 = args[3].(int) | |
| 416 | + | } | |
| 417 | + | run( | |
| 418 | + | arg0, | |
| 419 | + | arg1, | |
| 420 | + | arg2, | |
| 421 | + | arg3, | |
| 422 | + | ) | |
| 423 | + | }) | |
| 424 | + | return _c | |
| 425 | + | } | |
| 426 | + | ||
| 427 | + | func (_c *MockRenderer_WriteCursor_Call) Return(err error) *MockRenderer_WriteCursor_Call { | |
| 428 | + | _c.Call.Return(err) | |
| 429 | + | return _c | |
| 430 | + | } | |
| 431 | + | ||
| 432 | + | func (_c *MockRenderer_WriteCursor_Call) RunAndReturn(run func(i int, pixels []byte, w int, h int) error) *MockRenderer_WriteCursor_Call { | |
| 433 | + | _c.Call.Return(run) | |
| 434 | + | return _c | |
| 435 | + | } | |
| 436 | + | ||
| 340 | 437 | // NewMockSeat creates a new instance of MockSeat. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 341 | 438 | // The first argument is typically a *testing.T value. | |
| 342 | 439 | func NewMockSeat(t interface { | |
M internal/ports/devices.go +14 -6
| @@ -1,12 +1,17 @@ | |||
| 1 | 1 | package ports | |
| 2 | 2 | ||
| 3 | - | import "image" | |
| 3 | + | import ( | |
| 4 | + | "image" | |
| 5 | + | "os" | |
| 6 | + | ) | |
| 4 | 7 | ||
| 5 | 8 | // Renderer draws scenes for an output. One goroutine owns it. Frames go | |
| 6 | 9 | // to its own image until ExportTargets gives it scanout images; then each | |
| 7 | - | // Render draws into the target chosen by UseTarget (ADR 014: no CPU copy). | |
| 10 | + | // Render draws into the target chosen by UseTarget (ADR 014: no CPU pass). | |
| 8 | 11 | type Renderer interface { | |
| 9 | - | Render(Scene, map[WindowID]SurfaceContent) error | |
| 12 | + | // Render draws the scene. done is a sync file signalled when the GPU | |
| 13 | + | // finished the frame (nil: already finished); the caller closes it. | |
| 14 | + | Render(Scene, map[WindowID]SurfaceContent) (done *os.File, err error) | |
| 10 | 15 | // ExportTargets allocates n images of the renderer's size that the | |
| 11 | 16 | // display can scan out, with one of the given XRGB8888 modifiers | |
| 12 | 17 | // (none: any the device exports), and returns them as dmabufs. | |
| @@ -14,11 +19,14 @@ type Renderer interface { | |||
| 14 | 19 | ExportTargets(n int, modifiers []uint64) ([]DMABuf, error) | |
| 15 | 20 | // UseTarget selects the exported image the next Render draws into. | |
| 16 | 21 | UseTarget(i int) | |
| 22 | + | // CursorBuffers allocates two linear ARGB8888 images of size×size the | |
| 23 | + | // display can show on its cursor plane, and returns them as dmabufs. | |
| 24 | + | CursorBuffers(size int) ([2]DMABuf, error) | |
| 25 | + | // WriteCursor fills cursor image i with premultiplied ARGB8888 pixels | |
| 26 | + | // (w×h, w*4 per row), cropped to the image and cleared around it. | |
| 27 | + | WriteCursor(i int, pixels []byte, w, h int) error | |
| 17 | 28 | // Pixels reads the last frame back (headless screenshots, tests). | |
| 18 | 29 | Pixels() *image.RGBA | |
| 19 | - | // CopyBGRX reads the last frame back as XRGB8888 rows of the given | |
| 20 | - | // pitch: the logged fallback when no image can be exported. | |
| 21 | - | CopyBGRX(dst []byte, pitch int) | |
| 22 | 30 | Close() | |
| 23 | 31 | } | |
| 24 | 32 | ||
M internal/ports/messages.go +27 -6
| @@ -1,6 +1,9 @@ | |||
| 1 | 1 | package ports | |
| 2 | 2 | ||
| 3 | - | import "os" | |
| 3 | + | import ( | |
| 4 | + | "os" | |
| 5 | + | "time" | |
| 6 | + | ) | |
| 4 | 7 | ||
| 5 | 8 | // ClientEvent carries wayland → core notifications. | |
| 6 | 9 | type ClientEvent interface{ clientEvent() } | |
| @@ -315,17 +318,35 @@ func (OutputAdded) outputEvent() {} | |||
| 315 | 318 | // OutputPresented carries output → wayland what an output shows and reads. | |
| 316 | 319 | // Flip is set when a page flip completed: frame callbacks of the surfaces | |
| 317 | 320 | // on it are due. Shown and Queued are the DMABuf IDs scanned out directly | |
| 318 | - | // (0: a composed image); Seen is the latest content Seq the output got per | |
| 319 | - | // window. A replaced client buffer is released once every output that | |
| 320 | - | // reports has seen a later content of its window and neither shows nor | |
| 321 | - | // queues it. | |
| 321 | + | // (0: a composed image); Seen is the latest content Seq per window the | |
| 322 | + | // output finished reading (the GPU is done with it). A replaced client | |
| 323 | + | // buffer is released once every output that reports has seen a later | |
| 324 | + | // content of its window and neither shows nor queues it. | |
| 322 | 325 | type OutputPresented struct { | |
| 323 | 326 | Output string | |
| 324 | - | Flip bool | |
| 327 | + | Flip *FlipInfo | |
| 325 | 328 | Shown, Queued uint64 | |
| 326 | 329 | Seen map[WindowID]uint64 | |
| 327 | 330 | } | |
| 328 | 331 | ||
| 332 | + | // FlipInfo is one completed page flip. When is its CLOCK_MONOTONIC time | |
| 333 | + | // (hardware clock on DRM), Seq the output's vblank counter, Refresh the | |
| 334 | + | // refresh period (0 while variable refresh is on). ZeroCopy is set when a | |
| 335 | + | // client buffer was shown without composition, Async for a tearing flip. | |
| 336 | + | // Shows is the content Seq per window the flipped frame shows. Merged | |
| 337 | + | // counts earlier flips folded into this one when the reader fell behind: | |
| 338 | + | // their presentation feedback is discarded. | |
| 339 | + | type FlipInfo struct { | |
| 340 | + | When time.Duration | |
| 341 | + | Seq uint64 | |
| 342 | + | Refresh time.Duration | |
| 343 | + | ZeroCopy bool | |
| 344 | + | Async bool | |
| 345 | + | HardwareClock bool | |
| 346 | + | Merged int | |
| 347 | + | Shows map[WindowID]uint64 | |
| 348 | + | } | |
| 349 | + | ||
| 329 | 350 | // OutputRemoved carries output → core an unplugged display. | |
| 330 | 351 | type OutputRemoved struct{ Name string } | |
| 331 | 352 | ||