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 | # Unexported adapter seams: mocks live next to the code, in test files. | 18 | # Unexported adapter seams: mocks live next to the code, in test files. | ||
| 19 | github.com/bnema/nefertty/internal/adapters/drm: | 19 | github.com/bnema/nefertty/internal/adapters/drm: | ||
| 20 | interfaces: | 20 | interfaces: | ||
| 21 | - | cursorPlane: | 21 | + | kms: |
| 22 | config: | 22 | config: | ||
| 23 | dir: internal/adapters/drm | 23 | dir: internal/adapters/drm | ||
| 24 | pkgname: drm | 24 | pkgname: drm | ||
| 25 | - | filename: cursor_plane_mock_test.go | 25 | + | filename: kms_mock_test.go |
| 26 | github.com/bnema/nefertty/internal/adapters/wayland: | 26 | github.com/bnema/nefertty/internal/adapters/wayland: | ||
| 27 | interfaces: | 27 | interfaces: | ||
| 28 | procEnv: | 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 | "fmt" | 6 | "fmt" | ||
| 7 | "slices" | 7 | "slices" | ||
| 8 | "sync" | 8 | "sync" | ||
| 9 | - | "unsafe" | |||
| 10 | 9 | ||||
| 11 | "github.com/bnema/zerowrap" | 10 | "github.com/bnema/zerowrap" | ||
| 12 | "golang.org/x/sys/unix" | 11 | "golang.org/x/sys/unix" | ||
| 13 | ) | 12 | ) | ||
| 14 | 13 | ||||
| 15 | // Card is one DRM device and the outputs it drives. Scan finds connected | 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 | 15 | + | // connectors; each gets its own Output (CRTC, planes, cursor) run by one |
| 17 | - | // goroutine. One reader goroutine reads page-flip events for all of them | 16 | + | // goroutine. One reader goroutine reads commit events for all of them |
| 18 | // and hands each to its output by CRTC. | 17 | // and hands each to its output by CRTC. | ||
| 19 | type Card struct { | 18 | type Card struct { | ||
| 20 | fd int | 19 | fd int | ||
| @@ -24,30 +23,30 @@ type Card struct { | |||||
| 24 | crtcs []uint32 | 23 | crtcs []uint32 | ||
| 25 | // outputs by connector name; owned by the goroutine that calls Scan. | 24 | // outputs by connector name; owned by the goroutine that calls Scan. | ||
| 26 | outputs map[string]*Output | 25 | outputs map[string]*Output | ||
| 27 | - | flips map[uint32]chan int // by CRTC; set before the reader starts | 26 | + | flips map[uint32]chan flipEvent // by CRTC; set before the reader starts |
| 28 | - | // gemMu serialises GEM handle import and close on fd (outputs run on | 27 | + | k kmsDevice |
| 29 | - | // their own goroutines); modifiers is DRM_CAP_ADDFB2_MODIFIERS. | 28 | + | // taken are planes driven by an output, by plane ID. |
| 30 | - | gemMu sync.Mutex | 29 | + | taken map[uint32]bool |
| 31 | - | modifiers bool | 30 | + | // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. |
| 32 | - | // async is DRM_CAP_ASYNC_PAGE_FLIP. | |||
| 33 | async bool | 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 | func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error) { | 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 | crtcs, _, err := resources(fd) | 40 | crtcs, _, err := resources(fd) | ||
| 39 | if err != nil { | 41 | if err != nil { | ||
| 40 | return nil, fmt.Errorf("%s: %w", path, err) | 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 | for _, c := range crtcs { | 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} | 48 | + | k := kmsDevice{fd: fd, gemMu: &sync.Mutex{}, modifiers: hasCap(fd, capAddFB2Modifiers)} |
| 47 | - | mods := ioctl(fd, ioctlGetCap, unsafe.Pointer(&cp)) == nil && cp.value == 1 | 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 |
| 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 | |||
| 51 | } | 50 | } | ||
| 52 | 51 | ||||
| 53 | // Path is the device path, e.g. /dev/dri/card1. | 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 | return added, removed, replaced, nil | 102 | return added, removed, replaced, nil | ||
| 104 | } | 103 | } | ||
| 105 | 104 | ||||
| 106 | - | // Release forgets an output closed by its goroutine, freeing its CRTC for the | 105 | + | // Release forgets an output closed by its goroutine, freeing its CRTC and |
| 107 | - | // next Scan. | 106 | + | // planes for the next Scan. |
| 108 | - | func (c *Card) Release(name string) { delete(c.outputs, name) } | 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 | // Outputs returns the driven outputs. | 116 | // Outputs returns the driven outputs. | ||
| 111 | func (c *Card) Outputs() []*Output { | 117 | func (c *Card) Outputs() []*Output { | ||
| @@ -131,11 +137,19 @@ func (c *Card) open(conn connector, mode modeInfo) (*Output, error) { | |||||
| 131 | if err != nil { | 137 | if err != nil { | ||
| 132 | return nil, err | 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 | 150 | + | // ReadEvents reads commit events until ctx ends and routes them to the |
| 138 | - | // output of their CRTC. Run it once per card. | 151 | + | // output of their CRTC. Run it once per card. An event is never dropped: |
| 152 | + | // the output owes it a pending commit. | |||
| 139 | func (c *Card) ReadEvents(ctx context.Context) error { | 153 | func (c *Card) ReadEvents(ctx context.Context) error { | ||
| 140 | buf := make([]byte, 1024) | 154 | buf := make([]byte, 1024) | ||
| 141 | for ctx.Err() == nil { | 155 | for ctx.Err() == nil { | ||
| @@ -154,14 +168,15 @@ func (c *Card) ReadEvents(ctx context.Context) error { | |||||
| 154 | } | 168 | } | ||
| 155 | return fmt.Errorf("drm read: %w", err) | 169 | return fmt.Errorf("drm read: %w", err) | ||
| 156 | } | 170 | } | ||
| 157 | - | for _, crtc := range flipCrtcs(buf[:m]) { | 171 | + | for _, ev := range parseFlips(buf[:m]) { |
| 158 | - | ch := c.flips[crtc] | 172 | + | ch := c.flips[ev.crtc] |
| 159 | if ch == nil { | 173 | if ch == nil { | ||
| 160 | continue | 174 | continue | ||
| 161 | } | 175 | } | ||
| 162 | select { | 176 | select { | ||
| 163 | - | case ch <- 1: | 177 | + | case ch <- ev: |
| 164 | - | default: // the output is busy; the flip is counted on its next event | 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 | package drm | 1 | package drm | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "fmt" | 4 | + | "errors" |
| 5 | "sync" | 5 | "sync" | ||
| 6 | - | "time" | 6 | + | |
| 7 | - | "unsafe" | 7 | + | "github.com/bnema/nefertty/internal/ports" |
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| 10 | - | // Hardware cursor through the legacy cursor ioctls: the GPU overlays a small | 10 | + | // Hardware cursor on the CRTC's cursor plane. Move and Hide come from the |
| 11 | - | // buffer at a position, so moving it never re-renders the frame. | 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 | const ( | 16 | const ( | ||
| 14 | - | ioctlGetCap = 0xC010640C // DRM_IOWR('d', 0x0C, struct drm_get_cap) | 17 | + | ioctlGetCap = 0xC010640C // DRM_IOWR('d', 0x0C, struct drm_get_cap) |
| 15 | - | ioctlCursor2 = 0xC02464BB // DRM_IOWR('d', 0xBB, struct drm_mode_cursor2) | 18 | + | capCursorW = 0x8 |
| 16 | - | ioctlVblank = 0xC018643A // DRM_IOWR('d', 0x3A, union drm_wait_vblank) | 19 | + | capCursorH = 0x9 |
| 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 | |||
| 24 | ) | 20 | ) | ||
| 25 | 21 | ||||
| 26 | type getCap struct{ capability, value uint64 } | 22 | type getCap struct{ capability, value uint64 } | ||
| 27 | 23 | ||||
| 28 | - | // waitVblank is union drm_wait_vblank (request and reply share 24 bytes). | 24 | + | // Cursor is the hardware cursor of one output. |
| 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. | |||
| 69 | type Cursor struct { | 25 | type Cursor struct { | ||
| 70 | - | io sync.Mutex // serialises cursor ioctls and guards the fields below | 26 | + | mu sync.Mutex // guards x, y, away and moves |
| 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 | |||
| 81 | x, y int // physical position of the hotspot | 27 | x, y int // physical position of the hotspot | ||
| 82 | away bool // Hide was called and no Move since | 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{} | 43 | + | // cursorState is the cursor plane as committed: off, or fb at x, y. |
| 86 | - | stop chan struct{} | 44 | + | type cursorState struct { |
| 87 | - | done chan struct{} | 45 | + | on bool |
| 46 | + | fb uint32 | |||
| 47 | + | x, y int | |||
| 88 | } | 48 | } | ||
| 89 | 49 | ||||
| 90 | // CursorStats counts cursor work since the last TakeStats. | 50 | // CursorStats counts cursor work since the last TakeStats. | ||
| 91 | type CursorStats struct { | 51 | type CursorStats struct { | ||
| 92 | - | Moves int // Move calls | 52 | + | Moves int // Move calls |
| 93 | - | Ioctls int // move ioctls issued by the worker | 53 | + | Commits int // commits that moved or changed the cursor |
| 94 | - | MaxIoctl time.Duration // slowest move ioctl | 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 | func (c *Cursor) TakeStats() CursorStats { | 61 | func (c *Cursor) TakeStats() CursorStats { | ||
| 99 | c.mu.Lock() | 62 | c.mu.Lock() | ||
| 100 | - | defer c.mu.Unlock() | 63 | + | s := CursorStats{Moves: c.moves, Commits: c.commits} |
| 101 | - | s := c.stats | 64 | + | c.moves, c.commits = 0, 0 |
| 102 | - | c.stats = CursorStats{} | 65 | + | c.mu.Unlock() |
| 103 | return s | 66 | return s | ||
| 104 | } | 67 | } | ||
| 105 | 68 | ||||
| 106 | - | // newCursor allocates a cursor buffer at the driver's preferred size (64 when | 69 | + | // Limit is the largest image side the cursor plane accepts. |
| 107 | - | // the driver does not say). | 70 | + | func (c *Cursor) Limit() int { return c.size } |
| 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 | - | } | |||
| 135 | 71 | ||||
| 136 | - | // start runs the worker that applies coalesced moves. | 72 | + | // Move places the hotspot at physical (x, y). It never blocks. |
| 137 | - | func (c *Cursor) start() { | 73 | + | func (c *Cursor) Move(x, y float64) { |
| 138 | - | c.wake = make(chan struct{}, 1) | 74 | + | c.mu.Lock() |
| 139 | - | c.stop = make(chan struct{}) | 75 | + | c.x, c.y = int(x), int(y) |
| 140 | - | c.done = make(chan struct{}) | 76 | + | c.away = false |
| 141 | - | go func() { | 77 | + | c.moves++ |
| 142 | - | defer close(c.done) | 78 | + | c.mu.Unlock() |
| 143 | - | for { | 79 | + | c.poke() |
| 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 | - | }() | |||
| 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 | c.mu.Lock() | 85 | c.mu.Lock() | ||
| 182 | - | defer c.mu.Unlock() | 86 | + | c.away = true |
| 183 | - | return c.x, c.y | 87 | + | c.mu.Unlock() |
| 88 | + | c.poke() | |||
| 184 | } | 89 | } | ||
| 185 | 90 | ||||
| 186 | - | // Limit is the largest image side the cursor plane accepts. | 91 | + | func (c *Cursor) poke() { |
| 187 | - | func (c *Cursor) Limit() int { return c.size } | 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 | 98 | + | // setup allocates the two cursor images and their framebuffers. |
| 190 | - | // click point at (hotX, hotY). Larger images are cropped to the plane; an | 99 | + | func (c *Cursor) setup(k kms, r ports.Renderer) error { |
| 191 | - | // empty image hides the cursor (a client asked for none). | 100 | + | bufs, err := r.CursorBuffers(c.size) |
| 192 | - | func (c *Cursor) SetImage(pixels []byte, w, h, hotX, hotY int) error { | 101 | + | if err != nil { |
| 193 | - | c.io.Lock() | 102 | + | return err |
| 194 | - | defer c.io.Unlock() | |||
| 195 | - | if c.buf == nil { | |||
| 196 | - | return nil | |||
| 197 | } | 103 | } | ||
| 198 | - | if w <= 0 || h <= 0 || len(pixels) < w*h*4 { | 104 | + | for i := range bufs { |
| 199 | - | wasShown := c.shown | 105 | + | if err == nil { |
| 200 | - | c.shown = false | 106 | + | c.fbs[i], err = k.addFB(&bufs[i], fourccARGB) |
| 201 | - | if !wasShown || c.hidden { | 107 | + | } |
| 202 | - | return nil | 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) | 112 | + | if err != nil { |
| 208 | - | clear(c.buf.mem) | 113 | + | c.free(k) |
| 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]) | |||
| 212 | } | 114 | } | ||
| 213 | - | c.hotX, c.hotY, c.shown = hotX, hotY, true | 115 | + | return err |
| 214 | - | return c.apply() | |||
| 215 | } | 116 | } | ||
| 216 | 117 | ||||
| 217 | - | // apply (re)attaches the buffer at the current position. Callers hold c.io. | 118 | + | // setImage loads premultiplied ARGB8888 pixels into the image not on |
| 218 | - | func (c *Cursor) apply() error { | 119 | + | // screen; an empty image hides the cursor (a client asked for none). |
| 219 | - | if !c.shown || c.hidden || c.buf == nil { | 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 | return nil | 123 | return nil | ||
| 221 | } | 124 | } | ||
| 222 | - | x, y := c.position() | 125 | + | if c.fbs[0] == 0 { |
| 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)} | 126 | + | return errors.New("no cursor images") |
| 224 | - | return c.plane.Set(&v) | 127 | + | } |
| 225 | - | } | 128 | + | next := 1 - c.cur |
| 226 | - | 129 | + | if err := r.WriteCursor(next, pixels, w, h); err != nil { | |
| 227 | - | // Move places the hotspot at physical (x, y). It never blocks on the GPU: | 130 | + | return err |
| 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 | |||
| 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 | 136 | + | // desired is what the cursor plane should show now. |
| 242 | - | // is on another output). | 137 | + | func (c *Cursor) desired() cursorState { |
| 243 | - | func (c *Cursor) Hide() { | |||
| 244 | - | c.io.Lock() | |||
| 245 | - | defer c.io.Unlock() | |||
| 246 | c.mu.Lock() | 138 | c.mu.Lock() | ||
| 247 | - | c.away = true | 139 | + | x, y, away := c.x, c.y, c.away |
| 248 | c.mu.Unlock() | 140 | c.mu.Unlock() | ||
| 249 | - | if c.buf == nil || c.hidden { | 141 | + | if !c.image || away || c.fbs[c.cur] == 0 { |
| 250 | - | return | 142 | + | return cursorState{} |
| 251 | } | 143 | } | ||
| 252 | - | c.hidden = true | 144 | + | return cursorState{on: true, fb: c.fbs[c.cur], x: x - c.hotX, y: y - c.hotY} |
| 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() | |||
| 262 | } | 145 | } | ||
| 263 | 146 | ||||
| 264 | - | // close stops the worker, hides the cursor and frees its buffer. | 147 | + | // props adds the cursor plane in state s to req. |
| 265 | - | func (c *Cursor) close() { | 148 | + | func (c *Cursor) props(req *atomicReq, crtc uint32, s cursorState) { |
| 266 | - | if c.stop != nil { | 149 | + | p := c.plane |
| 267 | - | close(c.stop) | 150 | + | if !s.on { |
| 268 | - | <-c.done | 151 | + | req.set(p.id, p.prop("FB_ID"), 0) |
| 269 | - | c.stop = nil | 152 | + | req.set(p.id, p.prop("CRTC_ID"), 0) |
| 270 | - | } | |||
| 271 | - | c.io.Lock() | |||
| 272 | - | defer c.io.Unlock() | |||
| 273 | - | if c.buf == nil { | |||
| 274 | return | 153 | return | ||
| 275 | } | 154 | } | ||
| 276 | - | // Best effort: at exit the card may already belong to another session. | 155 | + | size := uint64(c.size) |
| 277 | - | v := modeCursor2{flags: cursorBO, crtcID: c.crtc} | 156 | + | req.set(p.id, p.prop("FB_ID"), uint64(s.fb)) |
| 278 | - | _ = c.plane.Set(&v) | 157 | + | req.set(p.id, p.prop("CRTC_ID"), uint64(crtc)) |
| 279 | - | c.buf.destroy(c.fd) | 158 | + | req.set(p.id, p.prop("SRC_X"), 0) |
| 280 | - | c.buf, c.shown = nil, false | 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 | package drm | 2 | package drm | ||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | - | "encoding/binary" | |||
| 6 | "fmt" | 5 | "fmt" | ||
| 7 | "math" | 6 | "math" | ||
| 8 | "unsafe" | 7 | "unsafe" | ||
| @@ -14,18 +13,10 @@ import ( | |||||
| 14 | const ( | 13 | const ( | ||
| 15 | ioctlGetResources = 0xC04064A0 | 14 | ioctlGetResources = 0xC04064A0 | ||
| 16 | ioctlGetCrtc = 0xC06864A1 | 15 | ioctlGetCrtc = 0xC06864A1 | ||
| 17 | - | ioctlSetCrtc = 0xC06864A2 | |||
| 18 | ioctlGetEncoder = 0xC01464A6 | 16 | ioctlGetEncoder = 0xC01464A6 | ||
| 19 | ioctlGetConnector = 0xC05064A7 | 17 | ioctlGetConnector = 0xC05064A7 | ||
| 20 | - | ioctlAddFB = 0xC01C64AE | |||
| 21 | ioctlRmFB = 0xC00464AF | 18 | ioctlRmFB = 0xC00464AF | ||
| 22 | - | ioctlPageFlip = 0xC01864B0 | |||
| 23 | - | ioctlCreateDumb = 0xC02064B2 | |||
| 24 | - | ioctlMapDumb = 0xC01064B3 | |||
| 25 | - | ioctlDestroyDumb = 0xC00464B4 | |||
| 26 | 19 | ||||
| 27 | - | pageFlipEvent = 1 | |||
| 28 | - | pageFlipAsync = 2 | |||
| 29 | eventFlipDone = 2 | 20 | eventFlipDone = 2 | ||
| 30 | modeTypePrefered = 1 << 3 | 21 | modeTypePrefered = 1 << 3 | ||
| 31 | connected = 1 | 22 | connected = 1 | ||
| @@ -72,24 +63,6 @@ type modeCrtc struct { | |||||
| 72 | mode modeInfo | 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 | func ioctl(fd int, req uintptr, arg unsafe.Pointer) error { | 66 | func ioctl(fd int, req uintptr, arg unsafe.Pointer) error { | ||
| 94 | for { | 67 | for { | ||
| 95 | _, _, e := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg)) | 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 | return 0, fmt.Errorf("no free CRTC for %s", c.name) | 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 | func getCrtc(fd int, crtc uint32) (modeCrtc, error) { | 237 | func getCrtc(fd int, crtc uint32) (modeCrtc, error) { | ||
| 319 | c := modeCrtc{crtcID: crtc} | 238 | c := modeCrtc{crtcID: crtc} | ||
| 320 | err := ioctl(fd, ioctlGetCrtc, unsafe.Pointer(&c)) | 239 | err := ioctl(fd, ioctlGetCrtc, unsafe.Pointer(&c)) | ||
| 321 | return c, err | 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 | import ( | 3 | import ( | ||
| 4 | "encoding/binary" | 4 | "encoding/binary" | ||
| 5 | + | "slices" | |||
| 5 | "testing" | 6 | "testing" | ||
| 7 | + | "time" | |||
| 6 | "unsafe" | 8 | "unsafe" | ||
| 9 | + | ||||
| 10 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 7 | ) | 11 | ) | ||
| 8 | 12 | ||||
| 9 | func TestStructSizesMatchIoctls(t *testing.T) { | 13 | func TestStructSizesMatchIoctls(t *testing.T) { | ||
| @@ -13,19 +17,21 @@ func TestStructSizesMatchIoctls(t *testing.T) { | |||||
| 13 | req uintptr | 17 | req uintptr | ||
| 14 | }{ | 18 | }{ | ||
| 15 | {"card_res", unsafe.Sizeof(cardRes{}), ioctlGetResources}, | 19 | {"card_res", unsafe.Sizeof(cardRes{}), ioctlGetResources}, | ||
| 16 | - | {"mode_crtc", unsafe.Sizeof(modeCrtc{}), ioctlSetCrtc}, | |||
| 17 | {"get_encoder", unsafe.Sizeof(getEncoder{}), ioctlGetEncoder}, | 20 | {"get_encoder", unsafe.Sizeof(getEncoder{}), ioctlGetEncoder}, | ||
| 18 | {"get_connector", unsafe.Sizeof(getConnector{}), ioctlGetConnector}, | 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 | {"fb_cmd2", unsafe.Sizeof(fbCmd2{}), ioctlAddFB2}, | 22 | {"fb_cmd2", unsafe.Sizeof(fbCmd2{}), ioctlAddFB2}, | ||
| 24 | {"prime_handle", unsafe.Sizeof(primeHandle{}), ioctlPrimeFDToHandle}, | 23 | {"prime_handle", unsafe.Sizeof(primeHandle{}), ioctlPrimeFDToHandle}, | ||
| 25 | {"gem_close", unsafe.Sizeof(gemClose{}), ioctlGemClose}, | 24 | {"gem_close", unsafe.Sizeof(gemClose{}), ioctlGemClose}, | ||
| 26 | {"obj_get_props", unsafe.Sizeof(objGetProps{}), ioctlObjGetProps}, | 25 | {"obj_get_props", unsafe.Sizeof(objGetProps{}), ioctlObjGetProps}, | ||
| 27 | {"get_prop", unsafe.Sizeof(getProp{}), ioctlGetProp}, | 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 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | 36 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | ||
| 31 | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | 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) { | 97 | + | func TestParseFlips(t *testing.T) { |
| 92 | - | ev := func(typ, crtc uint32) []byte { | 98 | + | ev := func(typ, crtc uint32, user uint64, sec, usec, seq uint32) []byte { |
| 93 | b := make([]byte, 32) | 99 | b := make([]byte, 32) | ||
| 94 | - | binary.LittleEndian.PutUint32(b, typ) | 100 | + | le := binary.LittleEndian |
| 95 | - | binary.LittleEndian.PutUint32(b[4:], 32) | 101 | + | le.PutUint32(b, typ) |
| 96 | - | binary.LittleEndian.PutUint32(b[28:], crtc) | 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 | return b | 108 | return b | ||
| 98 | } | 109 | } | ||
| 99 | - | buf := append(append(ev(eventFlipDone, 41), ev(1, 7)...), ev(eventFlipDone, 42)...) | 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)...) |
| 100 | - | if got := flipCrtcs(buf); len(got) != 2 || got[0] != 41 || got[1] != 42 { | 111 | + | got := parseFlips(buf) |
| 101 | - | t.Fatalf("got %v", got) | 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 | t.Fatalf("truncated: got %v", got) | 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 | "errors" | 5 | "errors" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "maps" | 7 | "maps" | ||
| 8 | + | "os" | |||
| 8 | "slices" | 9 | "slices" | ||
| 9 | "time" | 10 | "time" | ||
| 10 | "unsafe" | 11 | "unsafe" | ||
| @@ -14,73 +15,179 @@ import ( | |||||
| 14 | "golang.org/x/sys/unix" | 15 | "golang.org/x/sys/unix" | ||
| 15 | ) | 16 | ) | ||
| 16 | 17 | ||||
| 17 | - | // Output is one KMS connector flipping between two images. One goroutine | 18 | + | // Output is one KMS connector on a CRTC, driven by atomic commits (ADR |
| 18 | - | // runs it (Run) and closes it. | 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 | type Output struct { | 22 | type Output struct { | ||
| 20 | - | fd int | 23 | + | k kms |
| 21 | - | flipped <-chan int // page flips of this CRTC, from Card.ReadEvents | 24 | + | flipped <-chan flipEvent // commit events of this CRTC, from Card.ReadEvents |
| 22 | crtc uint32 | 25 | crtc uint32 | ||
| 23 | conn connector | 26 | conn connector | ||
| 24 | mode modeInfo | 27 | mode modeInfo | ||
| 25 | saved modeCrtc | 28 | saved modeCrtc | ||
| 26 | - | // fbs are the two images frames alternate between, back the one the | 29 | + | monitor Monitor |
| 27 | - | // next frame draws into. They are renderer images exported as dmabufs | 30 | + | log zerowrap.Logger |
| 28 | - | // (ADR 014: zero copy), or dumb buffers filled by the CPU when the GPU | 31 | + | // Properties: CRTC and connector property IDs by name. |
| 29 | - | // cannot export a scanout-capable image (logged fallback). | 32 | + | crtcProps map[string]uint32 |
| 30 | - | fbs [2]uint32 | 33 | + | connCrtc uint32 // the connector's CRTC_ID property |
| 31 | - | bufs [2]*dumbBuffer // fallback only | 34 | + | modeBlob uint32 |
| 32 | - | back int | 35 | + | // primary shows frames; cursor is the cursor plane (nil: none); |
| 33 | - | pending bool | 36 | + | // stray are other planes that can show on this CRTC, turned off at |
| 34 | - | flipStart time.Time | 37 | + | // every modeset when a previous master left them on it. |
| 35 | - | flips int | 38 | + | primary *plane |
| 36 | - | monitor Monitor | 39 | + | cursor *Cursor |
| 37 | - | cursor *Cursor | 40 | + | stray []*plane |
| 38 | - | log zerowrap.Logger | 41 | + | // fbs are the two renderer images frames alternate between, back the |
| 39 | - | // Direct scanout: client framebuffers by DMABuf ID, GEM handle counts, | 42 | + | // one the next frame draws into (ADR 014: zero copy). |
| 40 | - | // and the buffer on screen and queued (0: the composed image). | 43 | + | fbs [2]uint32 |
| 41 | - | card *Card | 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 | scanout bool | 53 | scanout bool | ||
| 43 | clientFBs map[uint64]*clientFB | 54 | clientFBs map[uint64]*clientFB | ||
| 44 | shown, queued uint64 | 55 | shown, queued uint64 | ||
| 45 | reason string // why the last frame was composed ("" = scanout) | 56 | reason string // why the last frame was composed ("" = scanout) | ||
| 46 | - | unsent ports.OutputPresented | 57 | + | unsent []ports.OutputPresented |
| 47 | - | // Tearing (ADR 006): async flips of scanned-out buffers that ask for | 58 | + | // Tearing (ADR 006): async commits of scanned-out buffers that ask for |
| 48 | - | // them, while the card accepts them. vrrProp is the CRTC's VRR_ENABLED | 59 | + | // them, when the card has DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. asyncFence |
| 49 | - | // property (0: no VRR), turned on while a buffer is scanned out. | 60 | + | // is set when an async commit may carry IN_FENCE_FD (probed once). |
| 50 | - | tearing bool | 61 | + | tearing bool |
| 51 | - | vrrProp uint32 | 62 | + | asyncFence bool |
| 52 | - | vrrOn, async bool | 63 | + | asyncProbed bool |
| 53 | - | // composedSince is when composition took over from scanout. | 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 | composedSince time.Time | 69 | composedSince time.Time | ||
| 55 | // kind is how the images were made; validated once a modeset took them. | 70 | // kind is how the images were made; validated once a modeset took them. | ||
| 56 | kind imageKind | 71 | kind imageKind | ||
| 57 | validated bool | 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 | // CursorLoader returns the image of a cursor at an output scale, at most | 89 | // CursorLoader returns the image of a cursor at an output scale, at most | ||
| 61 | // limit pixels on a side; an empty image hides the cursor. | 90 | // limit pixels on a side; an empty image hides the cursor. | ||
| 62 | type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error) | 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 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | 94 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | ||
| 66 | log := card.log | 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 | var err error | 98 | var err error | ||
| 69 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | 99 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | ||
| 70 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | 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 | o.tearing = card.async && !card.want.NoTearing | 105 | o.tearing = card.async && !card.want.NoTearing | ||
| 73 | if !card.want.NoVRR { | 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 { | 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") |
| 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") | |||
| 80 | return o, nil | 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 | func (o *Output) Cursor() *Cursor { return o.cursor } | 191 | func (o *Output) Cursor() *Cursor { return o.cursor } | ||
| 85 | 192 | ||||
| 86 | // Info describes the output for core and clients. Unknown EDID fields are empty. | 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 | func (o *Output) Width() int { return int(o.mode.HDisplay) } | 204 | func (o *Output) Width() int { return int(o.mode.HDisplay) } | ||
| 98 | func (o *Output) Height() int { return int(o.mode.VDisplay) } | 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 | func (o *Output) modeset() error { | 226 | func (o *Output) modeset() error { | ||
| 102 | o.pending = false | 227 | o.pending = false | ||
| 228 | + | o.pendingFrame = pendingFrame{} | |||
| 103 | o.shown, o.queued = 0, 0 | 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 { | 230 | + | mode := o.mode |
| 105 | - | return fmt.Errorf("set crtc: %w", err) | 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 | o.log.Info().Str("connector", o.conn.name).Msg("modeset") | 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 | return nil | 266 | return nil | ||
| 112 | } | 267 | } | ||
| 113 | 268 | ||||
| 114 | - | // present queues a page flip to the frame just drawn into the back image | 269 | + | // probeAsync checks once whether async commits may carry IN_FENCE_FD. |
| 115 | - | // (copied into it by the CPU on the dumb-buffer fallback). | 270 | + | func (o *Output) probeAsync(r ports.Renderer) { |
| 116 | - | func (o *Output) present(r ports.Renderer) error { | 271 | + | if o.asyncProbed || !o.tearing { |
| 117 | - | if b := o.bufs[o.back]; b != nil { | 272 | + | return |
| 118 | - | r.CopyBGRX(b.mem, int(b.pitch)) | 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 | if o.composedSince.IsZero() { | 302 | if o.composedSince.IsZero() { | ||
| 123 | o.composedSince = time.Now() | 303 | o.composedSince = time.Now() | ||
| 124 | - | } else if time.Since(o.composedSince) > vrrHold { | |||
| 125 | - | o.setVRR(false) | |||
| 126 | } | 304 | } | ||
| 127 | - | o.setAsync(false) | 305 | + | return o.vrrOn && time.Since(o.composedSince) <= vrrHold |
| 128 | - | if err := flip(o.fd, o.crtc, o.fbs[o.back], false); err != nil { | 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 | return err | 385 | return err | ||
| 130 | } | 386 | } | ||
| 131 | - | o.pending, o.flipStart = true, time.Now() | 387 | + | if o.cursor != nil && cur != o.cursor.applied { |
| 132 | - | o.queued = 0 | 388 | + | o.cursor.applied = cur |
| 133 | - | o.back = 1 - o.back | 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 | return nil | 398 | return nil | ||
| 135 | } | 399 | } | ||
| 136 | 400 | ||||
| 137 | - | // tryScanout flips a fullscreen client buffer directly. It reports false, | 401 | + | func boolValue(b bool) uint64 { |
| 138 | - | // with the reason logged on change, when the frame must be composed. | 402 | + | if b { |
| 139 | - | func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (bool, error) { | 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 | reason := "disabled" | 412 | reason := "disabled" | ||
| 141 | - | var c ports.SurfaceContent | |||
| 142 | if o.scanout { | 413 | if o.scanout { | ||
| 143 | c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height()) | 414 | c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height()) | ||
| 144 | } | 415 | } | ||
| 145 | - | var fb uint32 | |||
| 146 | if reason == "" { | 416 | if reason == "" { | ||
| 147 | fb, reason = o.scanoutFB(c.DMABuf, time.Now()) | 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 | o.reason = reason | 421 | o.reason = reason | ||
| 152 | } | 422 | } | ||
| 153 | if reason != "" { | 423 | if reason != "" { | ||
| 154 | - | return false, nil | 424 | + | return 0, c |
| 155 | - | } | |||
| 156 | - | if o.shown == c.DMABuf.ID && o.queued == 0 { | |||
| 157 | - | // Already on screen: nothing to flip. | |||
| 158 | - | return true, nil | |||
| 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 | // Drivers refuse async flips that change the format or modifier: the | 433 | // Drivers refuse async flips that change the format or modifier: the | ||
| 161 | // flip from the composed image into scanout waits for vblank. | 434 | // flip from the composed image into scanout waits for vblank. | ||
| 162 | - | cfb := o.clientFBs[c.DMABuf.ID] | |||
| 163 | async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync | 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 | if err != nil && async && errors.Is(err, unix.EINVAL) { | 439 | if err != nil && async && errors.Is(err, unix.EINVAL) { | ||
| 166 | - | // Refused (e.g. a layout change, or not a fast update): this | 440 | + | // Refused (e.g. not a fast update): this buffer flips at vblank |
| 167 | - | // buffer flips at vblank from now on. | 441 | + | // from now on. |
| 168 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | 442 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | ||
| 169 | - | cfb.noAsync, async = true, false | 443 | + | cfb.noAsync = true |
| 170 | - | err = flip(o.fd, o.crtc, fb, false) | 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 | if err == nil { | 453 | if err == nil { | ||
| 173 | - | o.setAsync(async) | 454 | + | o.queued = c.DMABuf.ID |
| 174 | - | o.setVRR(true) | |||
| 175 | - | o.composedSince = time.Time{} | |||
| 176 | } | 455 | } | ||
| 177 | - | if err != nil { | 456 | + | return true, err |
| 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 | |||
| 191 | } | 457 | } | ||
| 192 | 458 | ||||
| 193 | // vrrHold is how long composition runs before VRR turns off. | 459 | // vrrHold is how long composition runs before VRR turns off. | ||
| 194 | const vrrHold = 500 * time.Millisecond | 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 | // setAsync logs when the output starts or stops tearing. | 462 | // setAsync logs when the output starts or stops tearing. | ||
| 220 | func (o *Output) setAsync(on bool) { | 463 | func (o *Output) setAsync(on bool) { | ||
| 221 | if on != o.async { | 464 | if on != o.async { | ||
| @@ -227,30 +470,62 @@ func (o *Output) setAsync(on bool) { | |||||
| 227 | // Close restores the CRTC state found at startup, when it had a mode, and | 470 | // Close restores the CRTC state found at startup, when it had a mode, and | ||
| 228 | // frees buffers. | 471 | // frees buffers. | ||
| 229 | func (o *Output) Close() { | 472 | func (o *Output) Close() { | ||
| 473 | + | req := &atomicReq{} | |||
| 230 | if o.cursor != nil { | 474 | if o.cursor != nil { | ||
| 231 | - | o.cursor.close() | 475 | + | o.cursor.props(req, o.crtc, cursorState{}) |
| 232 | - | } | 476 | + | } |
| 233 | - | o.setVRR(false) | 477 | + | req.set(o.crtc, o.vrrProp, 0) |
| 234 | - | if o.saved.crtcID != 0 { | 478 | + | s := o.saved |
| 235 | - | ids := []uint32{o.conn.id} | 479 | + | var blob uint32 |
| 236 | - | s := o.saved | 480 | + | if s.crtcID != 0 && s.modeValid != 0 && s.fbID != 0 { |
| 237 | - | if s.modeValid != 0 { | 481 | + | mode := s.mode |
| 238 | - | _ = setCrtc(o.fd, s.crtcID, ids[0], s.fbID, &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 | o.freeImages() | 511 | o.freeImages() | ||
| 242 | o.shown, o.queued = 0, 0 | 512 | o.shown, o.queued = 0, 0 | ||
| 243 | o.dropClientFBs(time.Now(), true) | 513 | o.dropClientFBs(time.Now(), true) | ||
| 244 | } | 514 | } | ||
| 245 | 515 | ||||
| 246 | - | // Run renders scenes and flips until ctx ends. active reports seat enable/disable. | 516 | + | // Run renders scenes and commits them until ctx ends. active reports seat |
| 247 | - | // Each completed page flip is reported on presented (dropped when full). | 517 | + | // enable/disable. What the output shows and read is reported on presented. |
| 248 | 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 { | 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 | r, err := newRenderer(o.Width(), o.Height()) | 519 | r, err := newRenderer(o.Width(), o.Height()) | ||
| 250 | if err != nil { | 520 | if err != nil { | ||
| 251 | return fmt.Errorf("create renderer: %w", err) | 521 | return fmt.Errorf("create renderer: %w", err) | ||
| 252 | } | 522 | } | ||
| 253 | defer r.Close() | 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 | // The seat sends its state first; while switched away (e.g. a monitor | 529 | // The seat sends its state first; while switched away (e.g. a monitor | ||
| 255 | // plugged in on another VT) the modeset waits for the enable. | 530 | // plugged in on another VT) the modeset waits for the enable. | ||
| 256 | enabled := true | 531 | enabled := true | ||
| @@ -258,8 +533,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 258 | case enabled = <-active: | 533 | case enabled = <-active: | ||
| 259 | default: | 534 | default: | ||
| 260 | } | 535 | } | ||
| 261 | - | // Output images, from the best to the fallback (ADR 014). While | 536 | + | // Output images, from the best to the linear fallback (ADR 014). |
| 262 | - | // switched away they are validated by the first modeset on enable. | 537 | + | // While switched away they are validated by the first modeset on enable. |
| 263 | if enabled { | 538 | if enabled { | ||
| 264 | err = o.showImages(r, imagesDriver, nil) | 539 | err = o.showImages(r, imagesDriver, nil) | ||
| 265 | } else { | 540 | } else { | ||
| @@ -268,6 +543,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 268 | if err != nil { | 543 | if err != nil { | ||
| 269 | return err | 544 | return err | ||
| 270 | } | 545 | } | ||
| 546 | + | if enabled { | |||
| 547 | + | o.probeAsync(r) | |||
| 548 | + | } | |||
| 271 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | 549 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | ||
| 272 | var scene ports.Scene | 550 | var scene ports.Scene | ||
| 273 | haveScene, dirty := false, false | 551 | haveScene, dirty := false, false | ||
| @@ -276,36 +554,39 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 276 | frame := 0 | 554 | frame := 0 | ||
| 277 | stats := time.NewTicker(10 * time.Second) | 555 | stats := time.NewTicker(10 * time.Second) | ||
| 278 | defer stats.Stop() | 556 | defer stats.Stop() | ||
| 279 | - | // seen is the latest content Seq per window, reported to wayland with | 557 | + | // seen is the latest content Seq per window. It is reported only while |
| 280 | - | // what the output scans out, after each frame decision. | 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 | seen := map[ports.WindowID]uint64{} | 560 | seen := map[ports.WindowID]uint64{} | ||
| 282 | reportDirty := false | 561 | reportDirty := false | ||
| 283 | - | flipped := false | 562 | + | var cursorWake <-chan struct{} |
| 563 | + | if o.cursor != nil { | |||
| 564 | + | cursorWake = o.cursor.wake | |||
| 565 | + | } | |||
| 284 | for { | 566 | for { | ||
| 285 | - | if flipped || reportDirty { | 567 | + | if reportDirty && !o.pending { |
| 286 | - | o.report(presented, flipped, seen) | 568 | + | o.report(nil, seen) |
| 287 | - | flipped, reportDirty = false, false | 569 | + | reportDirty = false |
| 288 | - | } else { | |||
| 289 | - | o.flushReport(presented) | |||
| 290 | } | 570 | } | ||
| 571 | + | o.flushReport(presented) | |||
| 291 | // An unsent report is retried soon, not only on the next event. | 572 | // An unsent report is retried soon, not only on the next event. | ||
| 292 | var retry <-chan time.Time | 573 | var retry <-chan time.Time | ||
| 293 | - | if o.unsent.Output != "" { | 574 | + | if len(o.unsent) > 0 { |
| 294 | retry = time.After(time.Millisecond) | 575 | retry = time.After(time.Millisecond) | ||
| 295 | } | 576 | } | ||
| 296 | // A composed screen that stopped changing still drops VRR on time. | 577 | // A composed screen that stopped changing still drops VRR on time. | ||
| 297 | var vrrOff <-chan time.Time | 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 | vrrOff = time.After(max(0, vrrHold-time.Since(o.composedSince))) | 580 | vrrOff = time.After(max(0, vrrHold-time.Since(o.composedSince))) | ||
| 300 | } | 581 | } | ||
| 582 | + | stateDirty := false | |||
| 301 | select { | 583 | select { | ||
| 302 | case <-retry: | 584 | case <-retry: | ||
| 303 | continue | 585 | continue | ||
| 304 | case <-vrrOff: | 586 | case <-vrrOff: | ||
| 305 | - | if o.setVRR(false); o.vrrOn { | 587 | + | stateDirty = true |
| 306 | - | o.composedSince = time.Now() // failed: retry later, not in a loop | 588 | + | case <-cursorWake: |
| 307 | - | } | 589 | + | stateDirty = true |
| 308 | - | continue | |||
| 309 | case <-ctx.Done(): | 590 | case <-ctx.Done(): | ||
| 310 | return nil | 591 | return nil | ||
| 311 | case on := <-active: | 592 | case on := <-active: | ||
| @@ -317,7 +598,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 317 | o.log.Warn().Err(err).Msg("resume") | 598 | o.log.Warn().Err(err).Msg("resume") | ||
| 318 | continue | 599 | continue | ||
| 319 | } | 600 | } | ||
| 320 | - | if refused(err) && !o.validated && o.kind < imagesDumb { | 601 | + | if refused(err) && !o.validated && o.kind < imagesLinear { |
| 321 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(o.kind)).Msg("modeset refused the output images") | 602 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(o.kind)).Msg("modeset refused the output images") | ||
| 322 | o.freeImages() | 603 | o.freeImages() | ||
| 323 | err = o.showImages(r, o.kind+1, err) | 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 | return err | 608 | return err | ||
| 328 | } | 609 | } | ||
| 329 | o.validated = true | 610 | o.validated = true | ||
| 611 | + | o.probeAsync(r) | |||
| 330 | dirty = haveScene | 612 | dirty = haveScene | ||
| 331 | - | if o.cursor != nil { | 613 | + | stateDirty = true |
| 332 | - | if err := o.cursor.Reapply(); err != nil { | |||
| 333 | - | o.log.Warn().Err(err).Msg("cursor") | |||
| 334 | - | } | |||
| 335 | - | } | |||
| 336 | } | 614 | } | ||
| 337 | enabled = on | 615 | enabled = on | ||
| 338 | o.log.Info().Bool("enabled", on).Msg("output") | 616 | o.log.Info().Bool("enabled", on).Msg("output") | ||
| 339 | - | case n := <-o.flipped: | 617 | + | case ev := <-o.flipped: |
| 340 | if !o.pending { | 618 | if !o.pending { | ||
| 341 | continue // stale event from before a modeset | 619 | continue // stale event from before a modeset | ||
| 342 | } | 620 | } | ||
| 343 | - | o.pending = false | 621 | + | o.completed(ev, seen) |
| 344 | - | o.flips += n | 622 | + | reportDirty = false |
| 345 | - | o.shown = o.queued | 623 | + | stateDirty = true |
| 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 | - | } | |||
| 351 | case s := <-scenes: | 624 | case s := <-scenes: | ||
| 352 | if o.cursor != nil && loadCursor != nil && s.Scale != cursorScale { | 625 | if o.cursor != nil && loadCursor != nil && s.Scale != cursorScale { | ||
| 353 | cursorScale = s.Scale | 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 | scene, haveScene, dirty = s, true, true | 630 | scene, haveScene, dirty = s, true, true | ||
| 357 | case c := <-cursor: | 631 | case c := <-cursor: | ||
| 358 | want = c | 632 | want = c | ||
| 359 | // Before the first scene the scale is unknown: loaded then. | 633 | // Before the first scene the scale is unknown: loaded then. | ||
| 360 | if o.cursor != nil && loadCursor != nil && cursorScale > 0 { | 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 | case c := <-contents: | 638 | case c := <-contents: | ||
| 365 | if c.Empty() { | 639 | if c.Empty() { | ||
| 366 | delete(surfaces, c.ID) | 640 | delete(surfaces, c.ID) | ||
| @@ -378,50 +652,100 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 378 | ev := o.log.Info().Int("frames", frame).Int("flips", o.flips) | 652 | ev := o.log.Info().Int("frames", frame).Int("flips", o.flips) | ||
| 379 | if o.cursor != nil { | 653 | if o.cursor != nil { | ||
| 380 | cs := o.cursor.TakeStats() | 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 | ev.Msg("stats") | 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 | continue | 668 | continue | ||
| 387 | } | 669 | } | ||
| 388 | start := time.Now() | 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 | if !direct { | 677 | if !direct { | ||
| 391 | r.UseTarget(o.back) | 678 | r.UseTarget(o.back) | ||
| 392 | - | if err := r.Render(scene, surfaces); err != nil { | 679 | + | done, rerr := r.Render(scene, surfaces) |
| 393 | - | return fmt.Errorf("render frame: %w", err) | 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 | if err != nil { | 692 | if err != nil { | ||
| 399 | - | if errors.Is(err, unix.EBUSY) { | 693 | + | if !o.commitFailed(err, &enabled) { |
| 400 | - | // A flip is still in flight; retry after its event. | 694 | + | return fmt.Errorf("page flip: %w", err) |
| 401 | - | o.pending, o.flipStart = true, time.Now() | |||
| 402 | - | continue // dirty stays true | |||
| 403 | } | 695 | } | ||
| 404 | - | if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) { | 696 | + | continue // dirty stays true |
| 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) | |||
| 412 | } | 697 | } | ||
| 413 | dirty = false | 698 | dirty = false | ||
| 414 | - | reportDirty = true | |||
| 415 | frame++ | 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. | 744 | + | // setCursor loads a cursor for scale into the cursor images. |
| 421 | - | func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float64) { | 745 | + | func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64) { |
| 422 | img, err := load(c, scale, o.cursor.Limit()) | 746 | img, err := load(c, scale, o.cursor.Limit()) | ||
| 423 | if err == nil { | 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 | if err != nil { | 750 | if err != nil { | ||
| 427 | o.log.Warn().Err(err).Msg("cursor") | 751 | o.log.Warn().Err(err).Msg("cursor") | ||
| @@ -430,26 +754,47 @@ func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float6 | |||||
| 430 | 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") | 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 | 757 | + | // maxUnsent bounds reports waiting for wayland; past it the oldest flip is |
| 434 | - | // client buffers can be released. It must follow the frame decision: a | 758 | + | // folded into the next one. |
| 435 | - | // buffer is safe once a later content is seen and it is neither shown nor | 759 | + | const maxUnsent = 16 |
| 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 | - | } | |||
| 443 | 760 | ||||
| 444 | - | // flushReport sends the unsent report, if any, without blocking. | 761 | + | // report queues what the output shows and has read, so replaced client |
| 445 | - | func (o *Output) flushReport(presented chan<- ports.OutputPresented) { | 762 | + | // buffers can be released. A flip-less report merges into the newest |
| 446 | - | if o.unsent.Output == "" { | 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 | return | 771 | return | ||
| 448 | } | 772 | } | ||
| 449 | - | select { | 773 | + | o.unsent = append(o.unsent, r) |
| 450 | - | case presented <- o.unsent: | 774 | + | if len(o.unsent) > maxUnsent { |
| 451 | - | o.unsent = ports.OutputPresented{} | 775 | + | old, next := o.unsent[0].Flip, o.unsent[1].Flip |
| 452 | - | default: | 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 | const ( | 804 | const ( | ||
| 460 | imagesDriver imageKind = iota // exported, modifier chosen by the driver | 805 | imagesDriver imageKind = iota // exported, modifier chosen by the driver | ||
| 461 | imagesLinear // exported, linear | 806 | imagesLinear // exported, linear | ||
| 462 | - | imagesDumb // dumb buffers filled by CPU copies | |||
| 463 | ) | 807 | ) | ||
| 464 | 808 | ||||
| 465 | // showImages sets up images from kind on and modesets them. KMS may refuse | 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 | o.validated = true | 820 | o.validated = true | ||
| 477 | return nil | 821 | return nil | ||
| 478 | } | 822 | } | ||
| 479 | - | if !refused(err) { | 823 | + | if !refused(err) || got == imagesLinear { |
| 480 | return err | 824 | return err | ||
| 481 | } | 825 | } | ||
| 482 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images") | 826 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images") | ||
| 483 | o.freeImages() | 827 | o.freeImages() | ||
| 484 | - | if got == imagesDumb { | |||
| 485 | - | return err | |||
| 486 | - | } | |||
| 487 | kind, cause = got+1, err | 828 | kind, cause = got+1, err | ||
| 488 | } | 829 | } | ||
| 489 | } | 830 | } | ||
| @@ -493,17 +834,17 @@ func refused(err error) bool { | |||||
| 493 | return errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) | 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 | func lostMaster(err error) bool { | 838 | func lostMaster(err error) bool { | ||
| 498 | return errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) | 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 | 842 | + | // setupImages gives the output the two renderer images it flips between, |
| 502 | - | // targets exported as dmabufs, or dumb buffers as a logged fallback. A | 843 | + | // exported as dmabufs. There is no CPU fallback (ADR 014): a GPU that |
| 503 | - | // kind that fails falls through to the next one. | 844 | + | // cannot export a scanout-capable image cannot drive the output. |
| 504 | func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) { | 845 | func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) { | ||
| 505 | err := cause | 846 | err := cause | ||
| 506 | - | for ; kind < imagesDumb; kind++ { | 847 | + | for ; kind <= imagesLinear; kind++ { |
| 507 | mods := []uint64(nil) | 848 | mods := []uint64(nil) | ||
| 508 | if kind == imagesLinear { | 849 | if kind == imagesLinear { | ||
| 509 | mods = []uint64{0} | 850 | mods = []uint64{0} | ||
| @@ -513,17 +854,7 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima | |||||
| 513 | } | 854 | } | ||
| 514 | o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export") | 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)") | 857 | + | return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err) |
| 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 | |||
| 527 | } | 858 | } | ||
| 528 | 859 | ||||
| 529 | // exportImages makes the renderer's exported targets the output images. | 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 | for i := range bufs { | 866 | for i := range bufs { | ||
| 536 | if err == nil { | 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 | bufs[i].Planes[0].File.Close() | 870 | bufs[i].Planes[0].File.Close() | ||
| 540 | } | 871 | } | ||
| @@ -545,7 +876,12 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error { | |||||
| 545 | break | 876 | break | ||
| 546 | } | 877 | } | ||
| 547 | r.UseTarget(i) | 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 | if err != nil { | 886 | if err != nil { | ||
| 551 | o.freeImages() | 887 | o.freeImages() | ||
| @@ -556,15 +892,22 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error { | |||||
| 556 | return nil | 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 | func (o *Output) freeImages() { | 907 | func (o *Output) freeImages() { | ||
| 561 | for i, fb := range o.fbs { | 908 | for i, fb := range o.fbs { | ||
| 562 | - | if b := o.bufs[i]; b != nil { | 909 | + | if fb != 0 { |
| 563 | - | b.destroy(o.fd) | 910 | + | _ = o.k.rmFB(fb) |
| 564 | - | o.bufs[i] = nil | |||
| 565 | - | } else if fb != 0 { | |||
| 566 | - | v := fb | |||
| 567 | - | _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v)) | |||
| 568 | } | 911 | } | ||
| 569 | o.fbs[i] = 0 | 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 | "golang.org/x/sys/unix" | 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 | const ( | 11 | const ( | ||
| 12 | ioctlObjGetProps = 0xC02064B9 // DRM_IOWR('d', 0xB9, struct drm_mode_obj_get_properties) | 12 | ioctlObjGetProps = 0xC02064B9 // DRM_IOWR('d', 0xB9, struct drm_mode_obj_get_properties) | ||
| 13 | ioctlGetProp = 0xC04064AA // DRM_IOWR('d', 0xAA, struct drm_mode_get_property) | 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 | objCrtc = 0xcccccccc | 15 | objCrtc = 0xcccccccc | ||
| 17 | objConnector = 0xc0c0c0c0 | 16 | objConnector = 0xc0c0c0c0 | ||
| @@ -30,12 +29,6 @@ type getProp struct { | |||||
| 30 | countValues, countEn uint32 | 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 | // objProps returns an object's property IDs and values by name. | 32 | // objProps returns an object's property IDs and values by name. | ||
| 40 | func objProps(fd int, obj, typ uint32) (map[string][2]uint64, error) { | 33 | func objProps(fd int, obj, typ uint32) (map[string][2]uint64, error) { | ||
| 41 | q := objGetProps{objID: obj, objType: typ} | 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 | // vrrProperty returns the CRTC's VRR_ENABLED property when the connector | 58 | // vrrProperty returns the CRTC's VRR_ENABLED property when the connector | ||
| 66 | // is VRR capable, else 0. | 59 | // is VRR capable, else 0. | ||
| 67 | - | func vrrProperty(fd int, conn, crtc uint32) uint32 { | 60 | + | func vrrProperty(k kms, conn, crtc uint32) uint32 { |
| 68 | - | cp, err := objProps(fd, conn, objConnector) | 61 | + | cp, err := k.objProps(conn, objConnector) |
| 69 | if err != nil || cp["vrr_capable"][1] != 1 { | 62 | if err != nil || cp["vrr_capable"][1] != 1 { | ||
| 70 | return 0 | 63 | return 0 | ||
| 71 | } | 64 | } | ||
| 72 | - | rp, err := objProps(fd, crtc, objCrtc) | 65 | + | rp, err := k.objProps(crtc, objCrtc) |
| 73 | if err != nil { | 66 | if err != nil { | ||
| 74 | return 0 | 67 | return 0 | ||
| 75 | } | 68 | } | ||
| 76 | return uint32(rp["VRR_ENABLED"][0]) | 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 | fbModifiers = 1 << 1 | 24 | fbModifiers = 1 << 1 | ||
| 25 | capAddFB2Modifiers = 0x10 | 25 | capAddFB2Modifiers = 0x10 | ||
| 26 | - | capAsyncPageFlip = 0x7 | |||
| 27 | modInvalid = 0x00ffffffffffffff | 26 | modInvalid = 0x00ffffffffffffff | ||
| 28 | scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused | 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 | fb := &clientFB{last: now} | 103 | fb := &clientFB{last: now} | ||
| 105 | o.clientFBs[b.ID] = fb | 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 | if err != nil { | 111 | if err != nil { | ||
| 108 | o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed") | 112 | o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed") | ||
| 109 | fb.failed = "import_failed" | 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 | // scanoutFormat is the framebuffer format for a client format. The primary | 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 | 121 | + | // plane shows no alpha: alpha formats are scanned out as their X variant, |
| 118 | - | // composed image (XRGB8888): alpha formats are scanned out as their X | 122 | + | // others are refused. |
| 119 | - | // variant, others are refused. | |||
| 120 | func scanoutFormat(f uint32) (uint32, bool) { | 123 | func scanoutFormat(f uint32) (uint32, bool) { | ||
| 121 | switch f { | 124 | switch f { | ||
| 122 | case fourccXRGB, fourccARGB: | 125 | case fourccXRGB, fourccARGB: | ||
| @@ -130,27 +133,23 @@ const ( | |||||
| 130 | fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24 | 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 | 136 | + | // addFB imports a dmabuf as a framebuffer of the given fourcc: client buffers, the renderer's |
| 134 | - | // renderer's exported output images. The GEM handle belongs to the card fd | 137 | + | // output images and cursor images. The GEM handle belongs to the card fd |
| 135 | // and is shared by every import of the same buffer, so import, ADDFB2 and | 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 | // framebuffer keeps its own reference to the buffer. | 140 | // framebuffer keeps its own reference to the buffer. | ||
| 138 | - | func (c *Card) addFB(b *ports.DMABuf) (uint32, error) { | 141 | + | func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) { |
| 139 | - | format, ok := scanoutFormat(b.Format) | |||
| 140 | - | if !ok { | |||
| 141 | - | return 0, errors.New("format not scanned out") | |||
| 142 | - | } | |||
| 143 | raw, err := b.Planes[0].File.SyscallConn() | 142 | raw, err := b.Planes[0].File.SyscallConn() | ||
| 144 | if err != nil { | 143 | if err != nil { | ||
| 145 | return 0, err | 144 | return 0, err | ||
| 146 | } | 145 | } | ||
| 147 | - | c.gemMu.Lock() | 146 | + | k.gemMu.Lock() |
| 148 | - | defer c.gemMu.Unlock() | 147 | + | defer k.gemMu.Unlock() |
| 149 | var p primeHandle | 148 | var p primeHandle | ||
| 150 | var ioErr error | 149 | var ioErr error | ||
| 151 | if err := raw.Control(func(fd uintptr) { | 150 | if err := raw.Control(func(fd uintptr) { | ||
| 152 | p.fd = int32(fd) | 151 | p.fd = int32(fd) | ||
| 153 | - | ioErr = ioctl(c.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) | 152 | + | ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) |
| 154 | }); err != nil { | 153 | }); err != nil { | ||
| 155 | return 0, err | 154 | return 0, err | ||
| 156 | } | 155 | } | ||
| @@ -159,14 +158,14 @@ func (c *Card) addFB(b *ports.DMABuf) (uint32, error) { | |||||
| 159 | } | 158 | } | ||
| 160 | defer func() { | 159 | defer func() { | ||
| 161 | g := gemClose{handle: p.handle} | 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 | cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format} | 163 | cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format} | ||
| 165 | cmd.handles[0], cmd.pitches[0], cmd.offsets[0] = p.handle, b.Planes[0].Stride, b.Planes[0].Offset | 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 | cmd.flags, cmd.modifiers[0] = fbModifiers, b.Modifier | 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 | return 0, errors.Join(errors.New("add fb2"), err) | 169 | return 0, errors.Join(errors.New("add fb2"), err) | ||
| 171 | } | 170 | } | ||
| 172 | return cmd.fbID, nil | 171 | return cmd.fbID, nil | ||
| @@ -183,8 +182,7 @@ func (o *Output) dropClientFBs(now time.Time, all bool) { | |||||
| 183 | continue | 182 | continue | ||
| 184 | } | 183 | } | ||
| 185 | if fb.fbID != 0 { | 184 | if fb.fbID != 0 { | ||
| 186 | - | v := fb.fbID | 185 | + | _ = o.k.rmFB(fb.fbID) |
| 187 | - | _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v)) | |||
| 188 | } | 186 | } | ||
| 189 | delete(o.clientFBs, id) | 187 | delete(o.clientFBs, id) | ||
| 190 | } | 188 | } | ||
M internal/adapters/headless/output.go +24 -1
| @@ -2,6 +2,7 @@ package headless | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "errors" | |||
| 5 | "fmt" | 6 | "fmt" | ||
| 6 | "image" | 7 | "image" | ||
| 7 | "image/png" | 8 | "image/png" | ||
| @@ -12,6 +13,7 @@ import ( | |||||
| 12 | 13 | ||||
| 13 | "github.com/bnema/nefertty/internal/ports" | 14 | "github.com/bnema/nefertty/internal/ports" | ||
| 14 | "github.com/bnema/zerowrap" | 15 | "github.com/bnema/zerowrap" | ||
| 16 | + | "golang.org/x/sys/unix" | |||
| 15 | ) | 17 | ) | ||
| 16 | 18 | ||||
| 17 | type Options struct { | 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 | start := time.Now() | 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 | return fmt.Errorf("render frame: %w", err) | 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 | frame++ | 136 | frame++ | ||
| 125 | pending = opts.report(pending, seen) | 137 | pending = opts.report(pending, seen) | ||
| 126 | if opts.ScreenshotDir != "" { | 138 | if opts.ScreenshotDir != "" { | ||
| @@ -174,3 +186,14 @@ func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | |||||
| 174 | return r | 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 | func recordingRenderer(t *testing.T, err error) (*portsmocks.MockRenderer, *frames) { | 35 | func recordingRenderer(t *testing.T, err error) (*portsmocks.MockRenderer, *frames) { | ||
| 36 | r := portsmocks.NewMockRenderer(t) | 36 | r := portsmocks.NewMockRenderer(t) | ||
| 37 | f := &frames{} | 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 | f.mu.Lock() | 39 | f.mu.Lock() | ||
| 40 | defer f.mu.Unlock() | 40 | defer f.mu.Unlock() | ||
| 41 | f.scenes = append(f.scenes, s) | 41 | f.scenes = append(f.scenes, s) | ||
| 42 | f.contents = append(f.contents, maps.Clone(c)) | 42 | f.contents = append(f.contents, maps.Clone(c)) | ||
| 43 | - | return err | 43 | + | return nil, err |
| 44 | }).Maybe() | 44 | }).Maybe() | ||
| 45 | r.EXPECT().Pixels().Return(image.NewRGBA(image.Rect(0, 0, 2, 2))).Maybe() | 45 | r.EXPECT().Pixels().Return(image.NewRGBA(image.Rect(0, 0, 2, 2))).Maybe() | ||
| 46 | r.EXPECT().Close().Return().Once() | 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 | buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}} | 81 | buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}} | ||
| 82 | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}} | 82 | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}} | ||
| 83 | contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, DMABuf: buf}} | 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 | t.Fatal(err) | 85 | t.Fatal(err) | ||
| 86 | } | 86 | } | ||
| 87 | px := r.Pixels() | 87 | px := r.Pixels() | ||
| @@ -98,7 +98,7 @@ func TestRendererImportsDMABuf(t *testing.T) { | |||||
| 98 | } | 98 | } | ||
| 99 | // Frames without the buffer: its import is released after importTTL. | 99 | // Frames without the buffer: its import is released after importTTL. | ||
| 100 | for range importTTL + 1 { | 100 | for range importTTL + 1 { | ||
| 101 | - | if err := r.Render(scene, nil); err != nil { | 101 | + | if _, err := r.Render(scene, nil); err != nil { |
| 102 | t.Fatal(err) | 102 | t.Fatal(err) | ||
| 103 | } | 103 | } | ||
| 104 | } | 104 | } | ||
M internal/adapters/vulkan/renderer.go +11 -16
| @@ -30,7 +30,7 @@ type Renderer struct { | |||||
| 30 | current int | 30 | current int | ||
| 31 | renderMods []uint64 | 31 | renderMods []uint64 | ||
| 32 | // last is the target of the last frame; readback copies it into | 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 | last *target | 34 | last *target | ||
| 35 | readBack bool | 35 | readBack bool | ||
| 36 | buffer vk.Buffer | 36 | buffer vk.Buffer | ||
| @@ -55,6 +55,8 @@ type Renderer struct { | |||||
| 55 | frame uint64 | 55 | frame uint64 | ||
| 56 | // fillRegions is reused by Render for the bands of solid fills. | 56 | // fillRegions is reused by Render for the bands of solid fills. | ||
| 57 | fillRegions []vk.BufferImageCopy | 57 | fillRegions []vk.BufferImageCopy | ||
| 58 | + | // cursors are the exported cursor images (CursorBuffers). | |||
| 59 | + | cursors [2]*cursorImage | |||
| 58 | // blend draws translucent client pixels (blend.go); align is the | 60 | // blend draws translucent client pixels (blend.go); align is the | ||
| 59 | // device's storage buffer offset alignment. | 61 | // device's storage buffer offset alignment. | ||
| 60 | blend blendPipeline | 62 | blend blendPipeline | ||
| @@ -370,7 +372,7 @@ func (r *Renderer) ensureStaging(size int) error { | |||||
| 370 | } | 372 | } | ||
| 371 | 373 | ||||
| 372 | func (r *Renderer) Clear(rgb [3]uint8) error { | 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 | // upload is one piece of a frame: a solid fill, client pixels staged from | 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 | // Scene rects are logical; everything below works in physical pixels. | 438 | // Scene rects are logical; everything below works in physical pixels. | ||
| 432 | scale := s.Scale | 439 | scale := s.Scale | ||
| 433 | if scale <= 0 { | 440 | if scale <= 0 { | ||
| @@ -810,19 +817,6 @@ func (r *Renderer) Pixels() *image.RGBA { | |||||
| 810 | return out | 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 | // Missing from the bindings: VK_QUEUE_FAMILY_IGNORED, VK_WHOLE_SIZE. | 820 | // Missing from the bindings: VK_QUEUE_FAMILY_IGNORED, VK_WHOLE_SIZE. | ||
| 827 | const ( | 821 | const ( | ||
| 828 | queueFamilyIgnored = ^uint32(0) | 822 | queueFamilyIgnored = ^uint32(0) | ||
| @@ -887,6 +881,7 @@ func (r *Renderer) Close() { | |||||
| 887 | _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device)) | 881 | _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device)) | ||
| 888 | } | 882 | } | ||
| 889 | r.dropTargets() | 883 | r.dropTargets() | ||
| 884 | + | r.dropCursors() | |||
| 890 | for id, im := range r.imports { | 885 | for id, im := range r.imports { | ||
| 891 | r.release(im) | 886 | r.release(im) | ||
| 892 | delete(r.imports, id) | 887 | delete(r.imports, id) | ||
M internal/adapters/vulkan/renderer_test.go +12 -12
| @@ -48,7 +48,7 @@ func TestRendererRender(t *testing.T) { | |||||
| 48 | bg := color.RGBA{16, 32, 48, 255} | 48 | bg := color.RGBA{16, 32, 48, 255} | ||
| 49 | check := func(s ports.Scene, expected map[image.Point]color.RGBA) { | 49 | check := func(s ports.Scene, expected map[image.Point]color.RGBA) { | ||
| 50 | t.Helper() | 50 | t.Helper() | ||
| 51 | - | if err := r.Render(s, nil); err != nil { | 51 | + | if _, err := r.Render(s, nil); err != nil { |
| 52 | t.Fatal(err) | 52 | t.Fatal(err) | ||
| 53 | } | 53 | } | ||
| 54 | for p, want := range expected { | 54 | for p, want := range expected { | ||
| @@ -92,7 +92,7 @@ func TestRendererContents(t *testing.T) { | |||||
| 92 | if c != nil { | 92 | if c != nil { | ||
| 93 | contents = map[ports.WindowID]ports.SurfaceContent{1: *c} | 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 | t.Fatal(err) | 96 | t.Fatal(err) | ||
| 97 | } | 97 | } | ||
| 98 | } | 98 | } | ||
| @@ -127,7 +127,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) { | |||||
| 127 | {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}}, | 127 | {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}}, | ||
| 128 | {ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}}, | 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 | t.Fatal(err) | 131 | t.Fatal(err) | ||
| 132 | } | 132 | } | ||
| 133 | c := windowColor(2) | 133 | c := windowColor(2) | ||
| @@ -143,7 +143,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) { | |||||
| 143 | opaque := shmContent(t, 16, 16, 64, px) | 143 | opaque := shmContent(t, 16, 16, 64, px) | ||
| 144 | opaque.Opaque = true | 144 | opaque.Opaque = true | ||
| 145 | contents := map[ports.WindowID]ports.SurfaceContent{1: *opaque} | 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 | t.Fatal(err) | 147 | t.Fatal(err) | ||
| 148 | } | 148 | } | ||
| 149 | for _, tc := range []struct { | 149 | for _, tc := range []struct { | ||
| @@ -174,7 +174,7 @@ func TestRendererLayers(t *testing.T) { | |||||
| 174 | window := color.RGBA{wc[0], wc[1], wc[2], 255} | 174 | window := color.RGBA{wc[0], wc[1], wc[2], 255} | ||
| 175 | check := func(want color.RGBA) { | 175 | check := func(want color.RGBA) { | ||
| 176 | t.Helper() | 176 | t.Helper() | ||
| 177 | - | if err := r.Render(scene, contents); err != nil { | 177 | + | if _, err := r.Render(scene, contents); err != nil { |
| 178 | t.Fatal(err) | 178 | t.Fatal(err) | ||
| 179 | } | 179 | } | ||
| 180 | if got := r.Pixels().RGBAAt(5, 2); got != want { | 180 | if got := r.Pixels().RGBAAt(5, 2); got != want { | ||
| @@ -218,7 +218,7 @@ func TestRendererScale(t *testing.T) { | |||||
| 218 | scaled := shmContent(t, 20, 20, 80, solid(20, 20, red)) | 218 | scaled := shmContent(t, 20, 20, 80, solid(20, 20, red)) | ||
| 219 | scaled.ID, scaled.LogicalW, scaled.LogicalH = 1, 10, 10 | 219 | scaled.ID, scaled.LogicalW, scaled.LogicalH = 1, 10, 10 | ||
| 220 | content := map[ports.WindowID]ports.SurfaceContent{1: *scaled} | 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 | t.Fatal(err) | 222 | t.Fatal(err) | ||
| 223 | } | 223 | } | ||
| 224 | px := r.Pixels() | 224 | px := r.Pixels() | ||
| @@ -236,7 +236,7 @@ func TestRendererScale(t *testing.T) { | |||||
| 236 | // Scale 1.5 with an integer-scale client (buffer scale 2): the 20x20 | 236 | // Scale 1.5 with an integer-scale client (buffer scale 2): the 20x20 | ||
| 237 | // buffer is 10x10 logical and downscaled to 15x15 physical at (6,6). | 237 | // buffer is 10x10 logical and downscaled to 15x15 physical at (6,6). | ||
| 238 | s.Scale = 1.5 | 238 | s.Scale = 1.5 | ||
| 239 | - | if err := r.Render(s, content); err != nil { | 239 | + | if _, err := r.Render(s, content); err != nil { |
| 240 | t.Fatal(err) | 240 | t.Fatal(err) | ||
| 241 | } | 241 | } | ||
| 242 | px = r.Pixels() | 242 | px = r.Pixels() | ||
| @@ -252,7 +252,7 @@ func TestRendererScale(t *testing.T) { | |||||
| 252 | odd := shmContent(t, 16, 15, 64, solid(16, 15, red)) | 252 | odd := shmContent(t, 16, 15, 64, solid(16, 15, red)) | ||
| 253 | odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10 | 253 | odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10 | ||
| 254 | content[1] = *odd | 254 | content[1] = *odd | ||
| 255 | - | if err := r.Render(s, content); err != nil { | 255 | + | if _, err := r.Render(s, content); err != nil { |
| 256 | t.Fatal(err) | 256 | t.Fatal(err) | ||
| 257 | } | 257 | } | ||
| 258 | if got := r.Pixels().At(21, 10); got != (color.RGBA{0, 0, 0, 255}) { | 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 | {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(t, 30, 20, blue)}, | 309 | {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(t, 30, 20, blue)}, | ||
| 310 | {X: 5, Y: 5, SurfaceContent: solidContent(t, 4, 4, green)}, | 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 | t.Fatal(err) | 313 | t.Fatal(err) | ||
| 314 | } | 314 | } | ||
| 315 | px := r.Pixels() | 315 | px := r.Pixels() | ||
| @@ -327,7 +327,7 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) { | |||||
| 327 | } | 327 | } | ||
| 328 | // A root with no buffer of its own still shows its children. | 328 | // A root with no buffer of its own still shows its children. | ||
| 329 | only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(t, 20, 10, blue)}}} | 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 | t.Fatal(err) | 331 | t.Fatal(err) | ||
| 332 | } | 332 | } | ||
| 333 | if got := r.Pixels().At(20, 15); got != blue { | 333 | if got := r.Pixels().At(20, 15); got != blue { | ||
| @@ -345,7 +345,7 @@ func TestRendererTallFill(t *testing.T) { | |||||
| 345 | } | 345 | } | ||
| 346 | defer r.Close() | 346 | defer r.Close() | ||
| 347 | scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}} | 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 | t.Fatal(err) | 349 | t.Fatal(err) | ||
| 350 | } | 350 | } | ||
| 351 | c := windowColor(1) | 351 | c := windowColor(1) | ||
| @@ -403,7 +403,7 @@ func TestRendererAlphaBlend(t *testing.T) { | |||||
| 403 | {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}}, | 403 | {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}}, | ||
| 404 | {ID: 4, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 8, W: 16, H: 8}}, | 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 | t.Fatal(err) | 407 | t.Fatal(err) | ||
| 408 | } | 408 | } | ||
| 409 | px := r.Pixels() | 409 | px := r.Pixels() | ||
M internal/adapters/vulkan/targets.go +2 -2
| @@ -17,7 +17,7 @@ import ( | |||||
| 17 | // turns into KMS framebuffers. Render then composes straight into the | 17 | // turns into KMS framebuffers. Render then composes straight into the | ||
| 18 | // selected target and the output flips to it; nothing is copied by the | 18 | // selected target and the output flips to it; nothing is copied by the | ||
| 19 | // CPU. The internal image is the target until then (headless, tests). | 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 | // target is an image frames are drawn into. | 22 | // target is an image frames are drawn into. | ||
| 23 | type target struct { | 23 | type target struct { | ||
| @@ -107,7 +107,7 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) { | |||||
| 107 | r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp) | 107 | r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp) | ||
| 108 | need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit) | 108 | need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit) | ||
| 109 | // Targets are drawn on too (blend.go): a transfer-only modifier is | 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 | for _, m := range mods[:list.DrmFormatModifierCount] { | 111 | for _, m := range mods[:list.DrmFormatModifierCount] { | ||
| 112 | if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) { | 112 | if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) { | ||
| 113 | r.renderMods = append(r.renderMods, m.DrmFormatModifier) | 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 | colors := []string{"#ff0000", "#0000ff"} | 31 | colors := []string{"#ff0000", "#0000ff"} | ||
| 32 | for i, c := range colors { | 32 | for i, c := range colors { | ||
| 33 | r.UseTarget(i) | 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 | t.Fatal(err) | 35 | t.Fatal(err) | ||
| 36 | } | 36 | } | ||
| 37 | } | 37 | } | ||
| @@ -55,7 +55,7 @@ func TestExportTargets(t *testing.T) { | |||||
| 55 | } | 55 | } | ||
| 56 | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 32}}}} | 56 | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 32}}}} | ||
| 57 | contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 32, Opaque: true, DMABuf: &b}} | 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 | t.Fatal(err) | 59 | t.Fatal(err) | ||
| 60 | } | 60 | } | ||
| 61 | if got := viewer.Pixels().At(10, 10); got != (color.RGBA{255, 0, 0, 255}) { | 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 | case <-s.display.Stopped(): | 34 | case <-s.display.Stopped(): | ||
| 35 | return | 35 | return | ||
| 36 | case p := <-presented: | 36 | case p := <-presented: | ||
| 37 | - | flipped[p.Output] = flipped[p.Output] || p.Flip | 37 | + | flipped[p.Output] = flipped[p.Output] || p.Flip != nil |
| 38 | reports = append(reports, p) | 38 | reports = append(reports, p) | ||
| 39 | case <-s.frameReady: | 39 | case <-s.frameReady: | ||
| 40 | case <-timer.C: | 40 | case <-timer.C: | ||
| @@ -45,7 +45,7 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 45 | for { | 45 | for { | ||
| 46 | select { | 46 | select { | ||
| 47 | case p := <-presented: | 47 | case p := <-presented: | ||
| 48 | - | flipped[p.Output] = flipped[p.Output] || p.Flip | 48 | + | flipped[p.Output] = flipped[p.Output] || p.Flip != nil |
| 49 | reports = append(reports, p) | 49 | reports = append(reports, p) | ||
| 50 | default: | 50 | default: | ||
| 51 | break drain | 51 | break drain | ||
M internal/mocks/ports/devices_mock.go +128 -31
| @@ -6,6 +6,7 @@ package portsmocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "image" | 8 | "image" | ||
| 9 | + | "os" | |||
| 9 | 10 | ||||
| 10 | "github.com/bnema/nefertty/internal/ports" | 11 | "github.com/bnema/nefertty/internal/ports" | ||
| 11 | mock "github.com/stretchr/testify/mock" | 12 | mock "github.com/stretchr/testify/mock" | ||
| @@ -80,49 +81,65 @@ func (_c *MockRenderer_Close_Call) RunAndReturn(run func()) *MockRenderer_Close_ | |||||
| 80 | return _c | 81 | return _c | ||
| 81 | } | 82 | } | ||
| 82 | 83 | ||||
| 83 | - | // CopyBGRX provides a mock function for the type MockRenderer | 84 | + | // CursorBuffers provides a mock function for the type MockRenderer |
| 84 | - | func (_mock *MockRenderer) CopyBGRX(dst []byte, pitch int) { | 85 | + | func (_mock *MockRenderer) CursorBuffers(size int) ([2]ports.DMABuf, error) { |
| 85 | - | _mock.Called(dst, pitch) | 86 | + | ret := _mock.Called(size) |
| 86 | - | return | 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' | 112 | + | // MockRenderer_CursorBuffers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CursorBuffers' |
| 90 | - | type MockRenderer_CopyBGRX_Call struct { | 113 | + | type MockRenderer_CursorBuffers_Call struct { |
| 91 | *mock.Call | 114 | *mock.Call | ||
| 92 | } | 115 | } | ||
| 93 | 116 | ||||
| 94 | - | // CopyBGRX is a helper method to define mock.On call | 117 | + | // CursorBuffers is a helper method to define mock.On call |
| 95 | - | // - dst []byte | 118 | + | // - size int |
| 96 | - | // - pitch int | 119 | + | func (_e *MockRenderer_Expecter) CursorBuffers(size any) *MockRenderer_CursorBuffers_Call { |
| 97 | - | func (_e *MockRenderer_Expecter) CopyBGRX(dst any, pitch any) *MockRenderer_CopyBGRX_Call { | 120 | + | return &MockRenderer_CursorBuffers_Call{Call: _e.mock.On("CursorBuffers", size)} |
| 98 | - | return &MockRenderer_CopyBGRX_Call{Call: _e.mock.On("CopyBGRX", dst, pitch)} | |||
| 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 | _c.Call.Run(func(args mock.Arguments) { | 124 | _c.Call.Run(func(args mock.Arguments) { | ||
| 103 | - | var arg0 []byte | 125 | + | var arg0 int |
| 104 | if args[0] != nil { | 126 | if args[0] != nil { | ||
| 105 | - | arg0 = args[0].([]byte) | 127 | + | arg0 = args[0].(int) |
| 106 | - | } | |||
| 107 | - | var arg1 int | |||
| 108 | - | if args[1] != nil { | |||
| 109 | - | arg1 = args[1].(int) | |||
| 110 | } | 128 | } | ||
| 111 | run( | 129 | run( | ||
| 112 | arg0, | 130 | arg0, | ||
| 113 | - | arg1, | |||
| 114 | ) | 131 | ) | ||
| 115 | }) | 132 | }) | ||
| 116 | return _c | 133 | return _c | ||
| 117 | } | 134 | } | ||
| 118 | 135 | ||||
| 119 | - | func (_c *MockRenderer_CopyBGRX_Call) Return() *MockRenderer_CopyBGRX_Call { | 136 | + | func (_c *MockRenderer_CursorBuffers_Call) Return(dMABufs [2]ports.DMABuf, err error) *MockRenderer_CursorBuffers_Call { |
| 120 | - | _c.Call.Return() | 137 | + | _c.Call.Return(dMABufs, err) |
| 121 | return _c | 138 | return _c | ||
| 122 | } | 139 | } | ||
| 123 | 140 | ||||
| 124 | - | func (_c *MockRenderer_CopyBGRX_Call) RunAndReturn(run func(dst []byte, pitch int)) *MockRenderer_CopyBGRX_Call { | 141 | + | func (_c *MockRenderer_CursorBuffers_Call) RunAndReturn(run func(size int) ([2]ports.DMABuf, error)) *MockRenderer_CursorBuffers_Call { |
| 125 | - | _c.Run(run) | 142 | + | _c.Call.Return(run) |
| 126 | return _c | 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 | // Render provides a mock function for the type MockRenderer | 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 | ret := _mock.Called(scene, windowIDToSurfaceContent) | 262 | ret := _mock.Called(scene, windowIDToSurfaceContent) | ||
| 246 | 263 | ||||
| 247 | if len(ret) == 0 { | 264 | if len(ret) == 0 { | ||
| 248 | panic("no return value specified for Render") | 265 | panic("no return value specified for Render") | ||
| 249 | } | 266 | } | ||
| 250 | 267 | ||||
| 251 | - | var r0 error | 268 | + | var r0 *os.File |
| 252 | - | if returnFunc, ok := ret.Get(0).(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error); ok { | 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 | r0 = returnFunc(scene, windowIDToSurfaceContent) | 274 | r0 = returnFunc(scene, windowIDToSurfaceContent) | ||
| 254 | } else { | 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 | // MockRenderer_Render_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Render' | 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 | return _c | 315 | return _c | ||
| 288 | } | 316 | } | ||
| 289 | 317 | ||||
| 290 | - | func (_c *MockRenderer_Render_Call) Return(err error) *MockRenderer_Render_Call { | 318 | + | func (_c *MockRenderer_Render_Call) Return(done *os.File, err error) *MockRenderer_Render_Call { |
| 291 | - | _c.Call.Return(err) | 319 | + | _c.Call.Return(done, err) |
| 292 | return _c | 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 | _c.Call.Return(run) | 324 | _c.Call.Return(run) | ||
| 297 | return _c | 325 | return _c | ||
| 298 | } | 326 | } | ||
| @@ -337,6 +365,75 @@ func (_c *MockRenderer_UseTarget_Call) RunAndReturn(run func(i int)) *MockRender | |||||
| 337 | return _c | 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 | // 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. | 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 | // The first argument is typically a *testing.T value. | 438 | // The first argument is typically a *testing.T value. | ||
| 342 | func NewMockSeat(t interface { | 439 | func NewMockSeat(t interface { | ||
M internal/ports/devices.go +14 -6
| @@ -1,12 +1,17 @@ | |||||
| 1 | package ports | 1 | package ports | ||
| 2 | 2 | ||||
| 3 | - | import "image" | 3 | + | import ( |
| 4 | + | "image" | |||
| 5 | + | "os" | |||
| 6 | + | ) | |||
| 4 | 7 | ||||
| 5 | // Renderer draws scenes for an output. One goroutine owns it. Frames go | 8 | // Renderer draws scenes for an output. One goroutine owns it. Frames go | ||
| 6 | // to its own image until ExportTargets gives it scanout images; then each | 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 | type Renderer interface { | 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 | // ExportTargets allocates n images of the renderer's size that the | 15 | // ExportTargets allocates n images of the renderer's size that the | ||
| 11 | // display can scan out, with one of the given XRGB8888 modifiers | 16 | // display can scan out, with one of the given XRGB8888 modifiers | ||
| 12 | // (none: any the device exports), and returns them as dmabufs. | 17 | // (none: any the device exports), and returns them as dmabufs. | ||
| @@ -14,11 +19,14 @@ type Renderer interface { | |||||
| 14 | ExportTargets(n int, modifiers []uint64) ([]DMABuf, error) | 19 | ExportTargets(n int, modifiers []uint64) ([]DMABuf, error) | ||
| 15 | // UseTarget selects the exported image the next Render draws into. | 20 | // UseTarget selects the exported image the next Render draws into. | ||
| 16 | UseTarget(i int) | 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 | // Pixels reads the last frame back (headless screenshots, tests). | 28 | // Pixels reads the last frame back (headless screenshots, tests). | ||
| 18 | Pixels() *image.RGBA | 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 | Close() | 30 | Close() | ||
| 23 | } | 31 | } | ||
| 24 | 32 | ||||
M internal/ports/messages.go +27 -6
| @@ -1,6 +1,9 @@ | |||||
| 1 | package ports | 1 | package ports | ||
| 2 | 2 | ||||
| 3 | - | import "os" | 3 | + | import ( |
| 4 | + | "os" | |||
| 5 | + | "time" | |||
| 6 | + | ) | |||
| 4 | 7 | ||||
| 5 | // ClientEvent carries wayland → core notifications. | 8 | // ClientEvent carries wayland → core notifications. | ||
| 6 | type ClientEvent interface{ clientEvent() } | 9 | type ClientEvent interface{ clientEvent() } | ||
| @@ -315,17 +318,35 @@ func (OutputAdded) outputEvent() {} | |||||
| 315 | // OutputPresented carries output → wayland what an output shows and reads. | 318 | // OutputPresented carries output → wayland what an output shows and reads. | ||
| 316 | // Flip is set when a page flip completed: frame callbacks of the surfaces | 319 | // Flip is set when a page flip completed: frame callbacks of the surfaces | ||
| 317 | // on it are due. Shown and Queued are the DMABuf IDs scanned out directly | 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 | 321 | + | // (0: a composed image); Seen is the latest content Seq per window the |
| 319 | - | // window. A replaced client buffer is released once every output that | 322 | + | // output finished reading (the GPU is done with it). A replaced client |
| 320 | - | // reports has seen a later content of its window and neither shows nor | 323 | + | // buffer is released once every output that reports has seen a later |
| 321 | - | // queues it. | 324 | + | // content of its window and neither shows nor queues it. |
| 322 | type OutputPresented struct { | 325 | type OutputPresented struct { | ||
| 323 | Output string | 326 | Output string | ||
| 324 | - | Flip bool | 327 | + | Flip *FlipInfo |
| 325 | Shown, Queued uint64 | 328 | Shown, Queued uint64 | ||
| 326 | Seen map[WindowID]uint64 | 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 | // OutputRemoved carries output → core an unplugged display. | 350 | // OutputRemoved carries output → core an unplugged display. | ||
| 330 | type OutputRemoved struct{ Name string } | 351 | type OutputRemoved struct{ Name string } | ||
| 331 | 352 | ||||